Merge remote-tracking branch 'origin/codex/production-intelligence-terminal' into codex/issue-6-terminal-actions-hardening
# Conflicts: # README.md # server.mjs # test/fetch-utils.test.mjs
This commit is contained in:
@@ -83,16 +83,48 @@ const geoKeywords = {
|
||||
'IMF':[38.9,-77],'World Bank':[38.9,-77],'UN':[40.7,-74],
|
||||
};
|
||||
|
||||
function geoTagText(text) {
|
||||
function escapeRegex(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function geoKeywordRegex(keyword) {
|
||||
const flags = keyword.length <= 3 && keyword === keyword.toUpperCase() ? 'u' : 'iu';
|
||||
return new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegex(keyword)}(?=$|[^\\p{L}\\p{N}])`, flags);
|
||||
}
|
||||
|
||||
const geoKeywordEntries = Object.entries(geoKeywords)
|
||||
.sort((a, b) => b[0].length - a[0].length)
|
||||
.map(([keyword, coords]) => ({ keyword, coords, pattern: geoKeywordRegex(keyword) }));
|
||||
|
||||
export function geoTagText(text) {
|
||||
if (!text) return null;
|
||||
for (const [keyword, [lat, lon]] of Object.entries(geoKeywords)) {
|
||||
if (text.includes(keyword)) {
|
||||
for (const { keyword, coords, pattern } of geoKeywordEntries) {
|
||||
if (pattern.test(text)) {
|
||||
const [lat, lon] = coords;
|
||||
return { lat, lon, region: keyword };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stableHash(value) {
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
hash ^= value.charCodeAt(i);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function stableGeoJitter(key, axis) {
|
||||
const bucket = stableHash(`${axis}:${key}`) / 0xffffffff;
|
||||
return (bucket - 0.5) * 2;
|
||||
}
|
||||
|
||||
function newsGeoKey(item) {
|
||||
return `${item.source || ''}|${item.title || ''}|${item.date || ''}|${item.url || ''}`;
|
||||
}
|
||||
|
||||
function sanitizeExternalUrl(raw) {
|
||||
if (!raw) return undefined;
|
||||
try {
|
||||
@@ -235,8 +267,8 @@ export async function fetchAllNews() {
|
||||
source: item.source,
|
||||
date: item.date,
|
||||
url: item.url,
|
||||
lat: geo.lat + (Math.random() - 0.5) * 2,
|
||||
lon: geo.lon + (Math.random() - 0.5) * 2,
|
||||
lat: geo.lat + stableGeoJitter(newsGeoKey(item), 'lat'),
|
||||
lon: geo.lon + stableGeoJitter(newsGeoKey(item), 'lon'),
|
||||
region: geo.region
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1674,6 +1674,14 @@ function renderRight(){
|
||||
deltaRows.push(`<div class="delta-row"><span class="delta-badge down">▼</span><span class="delta-label">${s.label||s.key}</span><span class="delta-val">${s.from}→${s.to} (${val})</span></div>`);
|
||||
}
|
||||
const deltaHtml = hasDelta ? deltaRows.join('') : `<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">${t('delta.noChanges','No changes since last sweep')}</div>`;
|
||||
const scenarioItems = (D.scenarios?.items || []).filter(s => s.enabled || s.state !== 'dormant').slice(0,4);
|
||||
const scenarioHtml = scenarioItems.length ? scenarioItems.map(s => `
|
||||
<div class="signal-row">
|
||||
<strong>${s.name} <span class="delta-badge ${s.changed?'new':''}">${(s.state||'dormant').toUpperCase()}</span></strong>
|
||||
<p>${s.description || ''}</p>
|
||||
<div class="layer-sub">${s.confidence || 0}% confidence · score ${s.score || 0}${s.lastTriggerTime ? ' · ' + getAge(s.lastTriggerTime) : ''}</div>
|
||||
</div>
|
||||
`).join('') : `<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">No active scenario watchlist items</div>`;
|
||||
|
||||
document.getElementById('rightRail').innerHTML=`
|
||||
<div class="g-panel right-actions">
|
||||
@@ -1690,6 +1698,10 @@ function renderRight(){
|
||||
<div class="sec-head"><h3>${t('panels.crossSourceSignals','Cross-Source Signals')}</h3><span class="badge">${t('badges.worldview','WORLDVIEW')}</span></div>
|
||||
${signals}
|
||||
</div>
|
||||
<div class="g-panel right-scenarios">
|
||||
<div class="sec-head"><h3>Scenario Watchlist</h3><span class="badge">${D.scenarios?.available===false?'CONFIG':'LIVE'}</span></div>
|
||||
${scenarioHtml}
|
||||
</div>
|
||||
${mobile ? '' : buildOsintPanel('right-osint', 260)}
|
||||
<div class="g-panel right-core">
|
||||
<div class="sec-head"><h3>${t('panels.signalCore','Signal Core')}</h3><span class="badge">${t('badges.hotMetrics','HOT METRICS')}</span></div>
|
||||
|
||||
Reference in New Issue
Block a user