Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions server/public/admin-account-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -1688,12 +1688,12 @@ <h2>Delete Workspace</h2>
if (a.enrichment.employee_count_range || a.enrichment.employee_count) profileHtml += signalRow('Employees', a.enrichment.employee_count_range || a.enrichment.employee_count);
if (a.enrichment.founded_year) profileHtml += signalRow('Founded', a.enrichment.founded_year);
if (a.enrichment.city || a.enrichment.country) profileHtml += signalRow('Location', [a.enrichment.city, a.enrichment.country].filter(Boolean).join(', '));
if (a.enrichment.linkedin_url) profileHtml += signalRow('LinkedIn', `<a href="${escapeHtml(a.enrichment.linkedin_url)}" target="_blank">View Profile</a>`);
if (a.enrichment.linkedin_url) profileHtml += linkedInSignalRow(a.enrichment.linkedin_url);
if (a.enrichment.description) {
profileHtml += `<div style="margin-top: var(--space-3); font-size: var(--text-sm); color: var(--color-text-secondary);">${escapeHtml(a.enrichment.description)}</div>`;
}
if (a.enrichment.enriched_at) {
profileHtml += `<div style="margin-top: var(--space-2); font-size: var(--text-xs); color: var(--color-text-muted);">Updated ${formatDate(a.enrichment.enriched_at)} via ${a.enrichment.source || 'enrichment'}</div>`;
profileHtml += enrichmentAttribution(a.enrichment.enriched_at, a.enrichment.source);
}
document.getElementById('companyProfileContent').innerHTML = profileHtml;

Expand Down Expand Up @@ -2824,7 +2824,29 @@ <h3 style="font-size: var(--text-sm); font-weight: 600; margin-bottom: var(--spa

// Helper functions
function signalRow(label, value) {
return `<div class="signal-item"><span class="signal-label">${label}</span><span class="signal-value">${value}</span></div>`;
return `<div class="signal-item"><span class="signal-label">${escapeHtml(label)}</span><span class="signal-value">${escapeHtml(value)}</span></div>`;
}

function getSafeLinkedInUrl(value) {
if (typeof value !== 'string' || value.length > 2048) return null;
try {
const parsed = new URL(value);
const isLinkedInHost = parsed.hostname === 'linkedin.com' || parsed.hostname.endsWith('.linkedin.com');
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || !isLinkedInHost) return null;
return parsed.href;
} catch {
return null;
}
}

function linkedInSignalRow(value) {
const safeUrl = getSafeLinkedInUrl(value);
if (!safeUrl) return signalRow('LinkedIn', value);
return `<div class="signal-item"><span class="signal-label">LinkedIn</span><span class="signal-value"><a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">View profile</a></span></div>`;
}

function enrichmentAttribution(enrichedAt, source) {
return `<div style="margin-top: var(--space-2); font-size: var(--text-xs); color: var(--color-text-muted);">Updated ${escapeHtml(formatDate(enrichedAt))} via ${escapeHtml(source || 'enrichment')}</div>`;
}

function formatCompanyType(type) {
Expand Down
142 changes: 117 additions & 25 deletions server/public/admin-addie.html
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,9 @@ <h2 id="knowledge-modal-title">Add Knowledge</h2>
: '';

// Tags for curated content
const tags = doc.relevance_tags?.length ? doc.relevance_tags.join(', ') : '';
const tags = Array.isArray(doc.relevance_tags) && doc.relevance_tags.length
? doc.relevance_tags.map(tag => String(tag)).join(', ')
: '';

// Fetch status badge for curated content
const statusBadge = isCurated && doc.fetch_status
Expand All @@ -2222,14 +2224,12 @@ <h2 id="knowledge-modal-title">Add Knowledge</h2>

// Slack-specific info
const slackInfo = isSlack && doc.slack_channel_name
? `<span class="badge badge-info">#${doc.slack_channel_name}</span><span style="color: var(--color-text-muted);">@${doc.slack_username || 'unknown'}</span>`
? `<span class="badge badge-info">#${escapeHtml(String(doc.slack_channel_name))}</span><span style="color: var(--color-text-muted);">@${escapeHtml(String(doc.slack_username || 'unknown'))}</span>`
: '';

// URL link
const urlLink = doc.source_url || doc.fetch_url || doc.slack_permalink;
const urlHtml = urlLink
? `<a href="${escapeHtml(urlLink)}" target="_blank" onclick="event.stopPropagation();" style="color: var(--color-brand);">${isSlack ? 'View in Slack' : 'Source'}</a>`
: '';
const urlHtml = renderSafeHttpLink(urlLink, isSlack ? 'View in Slack' : 'Source', true);

// Content preview
const contentPreview = doc.summary
Expand Down Expand Up @@ -2282,7 +2282,7 @@ <h3>${escapeHtml(doc.title)}</h3>
</div>
${contentPreview ? `<div class="knowledge-excerpt">${contentPreview}</div>` : ''}
${addieNotes}
${tags ? `<div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--space-1);">Tags: ${tags}</div>` : ''}
${tags ? `<div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--space-1);">Tags: ${escapeHtml(tags)}</div>` : ''}
</div>
${actions ? `<div class="knowledge-actions" onclick="event.stopPropagation();">${actions}</div>` : ''}
</div>
Expand Down Expand Up @@ -2325,15 +2325,25 @@ <h3>${escapeHtml(doc.title)}</h3>

document.getElementById('resource-edit-id').value = r.id;
document.getElementById('resource-title').textContent = r.title;
document.getElementById('resource-url').innerHTML = `<a href="${escapeHtml(r.source_url || r.fetch_url)}" target="_blank">${escapeHtml(r.source_url || r.fetch_url)}</a>`;
const resourceUrl = r.source_url || r.fetch_url || '';
const resourceUrlEl = document.getElementById('resource-url');
const safeResourceLink = renderSafeHttpLink(resourceUrl, resourceUrl);
if (safeResourceLink) {
resourceUrlEl.innerHTML = safeResourceLink;
} else {
// Preserve legacy values for admins to diagnose without activating them.
resourceUrlEl.textContent = resourceUrl || 'No URL';
}
document.getElementById('resource-summary').textContent = r.summary || 'Not yet fetched';

// Display key insights
const insightsEl = document.getElementById('resource-insights');
if (r.key_insights && r.key_insights.length > 0) {
insightsEl.innerHTML = r.key_insights.map(i =>
`<div style="margin-bottom: var(--space-1);"><span class="badge badge-${i.importance === 'high' ? 'error' : i.importance === 'medium' ? 'warning' : 'info'}">${i.importance}</span> ${escapeHtml(i.insight)}</div>`
).join('');
insightsEl.innerHTML = r.key_insights.map(i => {
const importance = ['high', 'medium', 'low'].includes(i.importance) ? i.importance : 'low';
const badge = importance === 'high' ? 'error' : importance === 'medium' ? 'warning' : 'info';
return `<div style="margin-bottom: var(--space-1);"><span class="badge badge-${badge}">${importance}</span> ${escapeHtml(i.insight)}</div>`;
}).join('');
} else {
insightsEl.textContent = 'No insights yet';
}
Expand Down Expand Up @@ -2735,8 +2745,8 @@ <h3>${escapeHtml(doc.title)}</h3>
if (data.tags && data.tags.length > 0) {
tagsContainer.innerHTML = data.tags.map(t => `
<div class="tag-stat">
<span>${t.tag.replace('_', ' ')}</span>
<span class="tag-stat-count">${t.count}</span>
<span>${escapeHtml(String(t.tag ?? '').replace('_', ' '))}</span>
<span class="tag-stat-count">${escapeHtml(String(t.count ?? ''))}</span>
</div>
`).join('');
} else {
Expand Down Expand Up @@ -2927,6 +2937,31 @@ <h3>${escapeHtml(doc.title)}</h3>
return div.innerHTML.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

function getSafeHttpUrl(value) {
if (typeof value !== 'string' || !value.trim()) return null;
try {
const parsed = new URL(value.trim());
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
if (parsed.username || parsed.password) return null;
return parsed.href;
} catch {
return null;
}
}

function renderSafeHttpLink(value, label, stopPropagation = false) {
const safeUrl = getSafeHttpUrl(value);
if (!safeUrl) return '';
const anchor = document.createElement('a');
anchor.href = safeUrl;
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.textContent = String(label || safeUrl);
anchor.style.color = 'var(--color-brand)';
if (stopPropagation) anchor.setAttribute('onclick', 'event.stopPropagation();');
return anchor.outerHTML;
}

function copyThreadId(el) {
const threadId = el.dataset.threadId;
navigator.clipboard.writeText(threadId);
Expand Down Expand Up @@ -3126,7 +3161,10 @@ <h4>${isContentSuggestion ? 'Content Description' : 'Suggested Content'}</h4>
<h4>External Sources to Reference</h4>
<div class="suggestion-detail-content">
<ul style="margin: 0; padding-left: var(--space-4);">
${s.external_sources.map(url => `<li><a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a></li>`).join('')}
${s.external_sources.map(url => {
const link = renderSafeHttpLink(url, url);
return `<li>${link || escapeHtml(String(url ?? ''))}</li>`;
}).join('')}
</ul>
</div>
</div>
Expand Down Expand Up @@ -3438,17 +3476,21 @@ <h4>Pattern Identified</h4>
const linksEl = document.getElementById('extracted-links');
if (extractedLinks.length > 0) {
linksEl.style.display = 'block';
document.getElementById('links-list').innerHTML = extractedLinks.map(link => `
const safeExtractedLinks = extractedLinks.flatMap(link => {
const safeUrl = getSafeHttpUrl(link.url);
return safeUrl ? [{ ...link, url: safeUrl }] : [];
});
document.getElementById('links-list').innerHTML = safeExtractedLinks.map(link => `
<div class="link-item">
<span class="link-source">${link.source}</span>
<a href="${escapeHtml(link.url)}" target="_blank" rel="noopener">${escapeHtml(link.url)}</a>
<span class="link-source">${escapeHtml(String(link.source || ''))}</span>
${renderSafeHttpLink(link.url, link.url)}
</div>
`).join('');

// Populate URL dropdown for perspectives form
const urlSelect = document.getElementById('perspectives-url');
urlSelect.innerHTML = '<option value="">Select or enter URL...</option>' +
extractedLinks.map(link => `<option value="${escapeHtml(link.url)}">${escapeHtml(link.url)}</option>`).join('');
safeExtractedLinks.map(link => `<option value="${escapeHtml(link.url)}">${escapeHtml(link.url)}</option>`).join('');
} else {
linksEl.style.display = 'none';
}
Expand Down Expand Up @@ -3546,7 +3588,7 @@ <h4>Pattern Identified</h4>
<span class="thumbs">${msg.rating >= 3 ? '👍' : '👎'}</span>
<span class="rating-source">(${escapeHtml(msg.rating_source || 'unknown')})</span>
${msg.rating_notes ? `<span title="${escapeHtml(msg.rating_notes)}">"${escapeHtml(msg.rating_notes.substring(0, 30))}${msg.rating_notes.length > 30 ? '...' : ''}"</span>` : ''}
${msg.feedback_tags && msg.feedback_tags.length > 0 ? `<span>[${msg.feedback_tags.join(', ')}]</span>` : ''}
${msg.feedback_tags && msg.feedback_tags.length > 0 ? `<span>[${escapeHtml(msg.feedback_tags.map(tag => String(tag)).join(', '))}]</span>` : ''}
</div>
` : '';

Expand All @@ -3570,7 +3612,7 @@ <h4>Pattern Identified</h4>
</div>
<textarea class="feedback-notes"
id="feedback-notes-${msg.message_id}"
placeholder="What's good or bad about this response?">${msg.rating_notes || ''}</textarea>
placeholder="What's good or bad about this response?">${escapeHtml(msg.rating_notes || '')}</textarea>
<div class="feedback-actions">
<button class="btn btn-small btn-primary" onclick="saveMessageFeedback('${msg.message_id}')">Save</button>
</div>
Expand Down Expand Up @@ -3704,6 +3746,46 @@ <h4>Pattern Identified</h4>
`;
}

function linkifyBareUrls(html) {
const template = document.createElement('template');
template.innerHTML = html;
const walker = document.createTreeWalker(template.content, 4);
const textNodes = [];
let node;

while ((node = walker.nextNode())) textNodes.push(node);

textNodes.forEach(textNode => {
if (textNode.parentElement?.closest('a, code, pre')) return;

const value = textNode.nodeValue || '';
const urlPattern = /https?:\/\/[^\s<>"]+/g;
let match;
let cursor = 0;
let fragment = null;

while ((match = urlPattern.exec(value))) {
fragment ||= document.createDocumentFragment();
fragment.append(document.createTextNode(value.slice(cursor, match.index)));

const anchor = document.createElement('a');
anchor.href = match[0];
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.textContent = match[0];
fragment.append(anchor);
cursor = match.index + match[0].length;
}

if (fragment) {
fragment.append(document.createTextNode(value.slice(cursor)));
textNode.replaceWith(fragment);
}
});

return template.innerHTML;
}

function renderMarkdown(text) {
// Replace Slack user mentions with display names before escaping HTML
// Format: <@U1234567> -> @DisplayName
Expand All @@ -3713,7 +3795,11 @@ <h4>Pattern Identified</h4>
});

// Escape HTML first
text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
text = text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');

// Code blocks
text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
Expand All @@ -3728,10 +3814,15 @@ <h4>Pattern Identified</h4>
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');

// Links - make them clickable
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');

// Bare URLs (not already in markdown links)
text = text.replace(/(?<!href="|>)(https?:\/\/[^\s<>"]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, href) => {
try {
const parsed = new URL(href.replace(/&amp;/g, '&'));
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return label;
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${label}</a>`;
} catch {
return label;
}
});

// Line breaks to paragraphs
const paragraphs = text.split(/\n\n+/);
Expand All @@ -3750,7 +3841,8 @@ <h4>Pattern Identified</h4>
return '<p>' + p.replace(/\n/g, '<br>') + '</p>';
}).join('');

return text;
// Linkify text nodes only, so generated Markdown/code HTML is never reprocessed.
return linkifyBareUrls(text);
}

function renderExecutionPlan(toolExecutions, totalLatency) {
Expand Down
46 changes: 34 additions & 12 deletions server/public/admin-brands.html
Original file line number Diff line number Diff line change
Expand Up @@ -726,17 +726,24 @@ <h2 id="viewModalTitle">Brand Details</h2>
}
}

function getSafeResearchLogoUrl(value) {
if (typeof value !== 'string' || value.length > 2048) return null;
try {
const parsed = new URL(value);
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null;
return parsed.href;
} catch {
return null;
}
}

function displayResearchResults(data) {
const container = document.getElementById('researchContent');
const manifest = data.manifest || {};
const safeLogoUrl = getSafeResearchLogoUrl(manifest.logos?.[0]?.url);

let html = '<div class="brand-preview">';

// Logo
if (manifest.logos && manifest.logos.length > 0) {
html += `<img src="${manifest.logos[0].url}" alt="Logo" onerror="this.style.display='none'">`;
}

html += `
<div>
<strong>${escapeHtml(manifest.name || data.domain)}</strong><br>
Expand Down Expand Up @@ -768,6 +775,15 @@ <h2 id="viewModalTitle">Brand Details</h2>
}

container.innerHTML = html;
if (safeLogoUrl) {
const logo = document.createElement('img');
logo.src = safeLogoUrl;
logo.alt = 'Logo';
logo.addEventListener('error', () => {
logo.style.display = 'none';
});
container.querySelector('.brand-preview')?.prepend(logo);
}
document.getElementById('researchResults').style.display = 'block';
document.getElementById('researchError').style.display = 'none';
}
Expand Down Expand Up @@ -886,19 +902,25 @@ <h2 id="viewModalTitle">Brand Details</h2>
async function viewBrand(domain) {
document.getElementById('viewModal').style.display = 'block';
document.getElementById('viewModalTitle').textContent = domain;
document.getElementById('viewContent').innerHTML = '<div class="loading">Loading...</div>';
const viewContent = document.getElementById('viewContent');
viewContent.innerHTML = '<div class="loading">Loading...</div>';

try {
const response = await fetch(`/api/brands/resolve?domain=${encodeURIComponent(domain)}`);
const data = await response.json();

let html = '<pre style="background: var(--color-bg-subtle); padding: var(--space-4); border-radius: var(--radius-md); overflow: auto;">';
html += JSON.stringify(data, null, 2);
html += '</pre>';

document.getElementById('viewContent').innerHTML = html;
const preview = document.createElement('pre');
preview.style.background = 'var(--color-bg-subtle)';
preview.style.padding = 'var(--space-4)';
preview.style.borderRadius = 'var(--radius-md)';
preview.style.overflow = 'auto';
preview.textContent = JSON.stringify(data, null, 2);
viewContent.replaceChildren(preview);
} catch (error) {
document.getElementById('viewContent').innerHTML = `<div class="error-message">${escapeHtml(error.message)}</div>`;
const errorMessage = document.createElement('div');
errorMessage.className = 'error-message';
errorMessage.textContent = error.message;
viewContent.replaceChildren(errorMessage);
}
}

Expand Down
Loading
Loading