Skip to content
Closed
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
45 changes: 43 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
"@xenova/transformers": "^2.17.2",
"d3": "^7.9.0",
"deck.gl": "^9.2.6",
"i18next": "^25.8.10",
"i18next-browser-languagedetector": "^8.2.1",
"maplibre-gl": "^5.16.0",
"onnxruntime-web": "^1.23.2",
"topojson-client": "^3.1.0",
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"app",
"dmg",
"nsis",
"msi"
"msi",
"appimage"
],
"category": "Productivity",
"shortDescription": "World Monitor desktop app (supports World and Tech variants)",
Expand Down Expand Up @@ -60,4 +61,4 @@
"hardenedRuntime": true
}
}
}
}
173 changes: 100 additions & 73 deletions src/App.ts

Large diffs are not rendered by default.

33 changes: 11 additions & 22 deletions src/components/CIIPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { getCSSColor } from '@/utils';
import { calculateCII, type CountryScore } from '@/services/country-instability';
import { t } from '../services/i18n';

export class CIIPanel extends Panel {
private scores: CountryScore[] = [];
Expand All @@ -11,22 +12,10 @@ export class CIIPanel extends Panel {
constructor() {
super({
id: 'cii',
title: 'Country Instability Index',
showCount: true,
trackActivity: true,
infoTooltip: `<strong>CII Methodology</strong>
Score (0-100) per country based on:
<ul>
<li>40% baseline geopolitical risk</li>
<li><strong>U</strong>nrest: protests, fatalities, internet outages</li>
<li><strong>S</strong>ecurity: military flights/vessels over territory</li>
<li><strong>I</strong>nformation: news velocity and focal point correlation</li>
<li>Hotspot proximity boost (strategic locations)</li>
</ul>
<em>U:S:I values show component scores.</em>
Focal Point Detection correlates news entities with map signals for accurate scoring.`,
title: t('panels.cii'),
infoTooltip: t('components.cii.infoTooltip'),
});
this.showLoading('Scanning intelligence feeds');
this.showLoading(t('common.loading'));
}

public setShareStoryHandler(handler: (code: string, name: string) => void): void {
Expand Down Expand Up @@ -54,7 +43,7 @@ export class CIIPanel extends Panel {
}

private getTrendArrow(trend: CountryScore['trend'], change: number): string {
if (trend === 'rising') return `<span class="trend-up">โ†‘${change > 0 ? change : ''}</span>`;
if (trend === 'rising') return `< span class= "trend-up" >โ†‘${change > 0 ? change : ''} </span>`;
if (trend === 'falling') return `<span class="trend-down">โ†“${Math.abs(change)}</span>`;
return '<span class="trend-stable">โ†’</span>';
}
Expand All @@ -72,16 +61,16 @@ export class CIIPanel extends Panel {
<span class="cii-name">${escapeHtml(country.name)}</span>
<span class="cii-score">${country.score}</span>
${trend}
<button class="cii-share-btn" data-code="${escapeHtml(country.code)}" data-name="${escapeHtml(country.name)}" title="Share story"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v7a2 2 0 002 2h12a2 2 0 002-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg></button>
<button class="cii-share-btn" data-code="${escapeHtml(country.code)}" data-name="${escapeHtml(country.name)}" title="${t('common.shareStory')}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v7a2 2 0 002 2h12a2 2 0 002-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg></button>
</div>
<div class="cii-bar-container">
<div class="cii-bar" style="width: ${barWidth}%; background: ${color};"></div>
</div>
<div class="cii-components">
<span title="Unrest">U:${country.components.unrest}</span>
<span title="Conflict">C:${country.components.conflict}</span>
<span title="Security">S:${country.components.security}</span>
<span title="Information">I:${country.components.information}</span>
<span title="${t('common.unrest')}">U:${country.components.unrest}</span>
<span title="${t('common.conflict')}">C:${country.components.conflict}</span>
<span title="${t('common.security')}">S:${country.components.security}</span>
<span title="${t('common.information')}">I:${country.components.information}</span>
</div>
</div>
`;
Expand Down Expand Up @@ -131,7 +120,7 @@ export class CIIPanel extends Panel {
this.bindShareButtons();
} catch (error) {
console.error('[CIIPanel] Refresh error:', error);
this.showError('Failed to calculate CII');
this.showError(t('common.failedCII'));
}
}

Expand Down
50 changes: 27 additions & 23 deletions src/components/CascadePanel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { getCSSColor } from '@/utils';
import {
buildDependencyGraph,
Expand All @@ -22,18 +23,10 @@ export class CascadePanel extends Panel {
constructor() {
super({
id: 'cascade',
title: 'Infrastructure Cascade',
title: t('panels.cascade'),
showCount: true,
trackActivity: true,
infoTooltip: `<strong>Cascade Analysis</strong>
Models infrastructure dependencies:
<ul>
<li>Subsea cables, pipelines, ports, chokepoints</li>
<li>Select infrastructure to simulate failure</li>
<li>Shows affected countries and capacity loss</li>
<li>Identifies redundant routes</li>
</ul>
Data from TeleGeography and industry sources.`,
infoTooltip: t('components.cascade.infoTooltip'),
});
this.init();
}
Expand All @@ -47,7 +40,7 @@ export class CascadePanel extends Panel {
this.render();
} catch (error) {
console.error('[CascadePanel] Init error:', error);
this.showError('Failed to build dependency graph');
this.showError(t('common.failedDependencyGraph'));
}
}

Expand Down Expand Up @@ -80,6 +73,16 @@ export class CascadePanel extends Panel {
}
}

private getFilterLabel(filter: Exclude<NodeFilter, 'all'>): string {
const labels: Record<Exclude<NodeFilter, 'all'>, string> = {
cable: t('components.cascade.filters.cables'),
pipeline: t('components.cascade.filters.pipelines'),
port: t('components.cascade.filters.ports'),
chokepoint: t('components.cascade.filters.chokepoints'),
};
return labels[filter];
}

private getFilteredNodes(): InfrastructureNode[] {
if (!this.graph) return [];
const nodes: InfrastructureNode[] = [];
Expand All @@ -95,9 +98,9 @@ export class CascadePanel extends Panel {

private renderSelector(): string {
const nodes = this.getFilteredNodes();
const filterButtons = ['cable', 'pipeline', 'port', 'chokepoint'].map(f =>
const filterButtons = ['cable', 'pipeline', 'port', 'chokepoint'].map((f) =>
`<button class="cascade-filter-btn ${this.filter === f ? 'active' : ''}" data-filter="${f}">
${this.getNodeTypeEmoji(f)} ${f.charAt(0).toUpperCase() + f.slice(1)}s
${this.getNodeTypeEmoji(f)} ${this.getFilterLabel(f as Exclude<NodeFilter, 'all'>)}
</button>`
).join('');

Expand All @@ -106,16 +109,17 @@ export class CascadePanel extends Panel {
${escapeHtml(n.name)}
</option>`
).join('');
const selectedType = t(`components.cascade.filterType.${this.filter}`);

return `
<div class="cascade-selector">
<div class="cascade-filters">${filterButtons}</div>
<select class="cascade-select" ${nodes.length === 0 ? 'disabled' : ''}>
<option value="">Select ${this.filter}...</option>
<option value="">${t('components.cascade.selectPrompt', { type: selectedType })}</option>
${nodeOptions}
</select>
<button class="cascade-analyze-btn" ${!this.selectedNode ? 'disabled' : ''}>
Analyze Impact
${t('components.cascade.analyzeImpact')}
</button>
</div>
`;
Expand All @@ -131,16 +135,16 @@ export class CascadePanel extends Panel {
<div class="cascade-country" style="border-left: 3px solid ${this.getImpactColor(c.impactLevel)}">
<span class="cascade-emoji">${this.getImpactEmoji(c.impactLevel)}</span>
<span class="cascade-country-name">${escapeHtml(c.countryName)}</span>
<span class="cascade-impact">${c.impactLevel}</span>
${c.affectedCapacity > 0 ? `<span class="cascade-capacity">${Math.round(c.affectedCapacity * 100)}% capacity</span>` : ''}
<span class="cascade-impact">${t(`components.cascade.impactLevels.${c.impactLevel}`)}</span>
${c.affectedCapacity > 0 ? `<span class="cascade-capacity">${t('components.cascade.capacityPercent', { percent: String(Math.round(c.affectedCapacity * 100)) })}</span>` : ''}
</div>
`).join('')
: '<div class="empty-state">No country impacts detected</div>';
: `<div class="empty-state">${t('components.cascade.noCountryImpacts')}</div>`;

const redundanciesHtml = redundancies && redundancies.length > 0
? `
<div class="cascade-section">
<div class="cascade-section-title">Alternative Routes</div>
<div class="cascade-section-title">${t('components.cascade.alternativeRoutes')}</div>
${redundancies.map(r => `
<div class="cascade-redundancy">
<span class="cascade-redundancy-name">${escapeHtml(r.name)}</span>
Expand All @@ -156,10 +160,10 @@ export class CascadePanel extends Panel {
<div class="cascade-source">
<span class="cascade-emoji">${this.getNodeTypeEmoji(source.type)}</span>
<span class="cascade-source-name">${escapeHtml(source.name)}</span>
<span class="cascade-source-type">${source.type}</span>
<span class="cascade-source-type">${t(`components.cascade.filterType.${source.type}`)}</span>
</div>
<div class="cascade-section">
<div class="cascade-section-title">Countries Affected (${countriesAffected.length})</div>
<div class="cascade-section-title">${t('components.cascade.countriesAffected', { count: String(countriesAffected.length) })}</div>
<div class="cascade-countries">${countriesHtml}</div>
</div>
${redundanciesHtml}
Expand All @@ -181,15 +185,15 @@ export class CascadePanel extends Panel {
<span>โš“ ${stats.ports}</span>
<span>๐ŸŒŠ ${stats.chokepoints}</span>
<span>๐Ÿณ๏ธ ${stats.countries}</span>
<span>๐Ÿ“Š ${stats.edges} links</span>
<span>๐Ÿ“Š ${stats.edges} ${t('components.cascade.links')}</span>
</div>
`;

this.content.innerHTML = `
<div class="cascade-panel">
${statsHtml}
${this.renderSelector()}
${this.cascadeResult ? this.renderCascadeResult() : '<div class="cascade-hint">Select infrastructure to analyze cascade impact</div>'}
${this.cascadeResult ? this.renderCascadeResult() : `<div class="cascade-hint">${t('components.cascade.selectInfrastructureHint')}</div>`}
</div>
`;

Expand Down
26 changes: 10 additions & 16 deletions src/components/ClimateAnomalyPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import type { ClimateAnomaly } from '@/types';
import { getSeverityIcon, formatDelta } from '@/services/climate';
import { t } from '@/services/i18n';

export class ClimateAnomalyPanel extends Panel {
private anomalies: ClimateAnomaly[] = [];
Expand All @@ -10,19 +11,12 @@ export class ClimateAnomalyPanel extends Panel {
constructor() {
super({
id: 'climate',
title: 'Climate Anomalies',
title: t('panels.climate'),
showCount: true,
trackActivity: true,
infoTooltip: `<strong>Climate Anomaly Monitor</strong>
Temperature and precipitation deviations from 30-day baseline.
Data from Open-Meteo (ERA5 reanalysis).
<ul>
<li><strong>Extreme</strong>: >5ยฐC or >80mm/day deviation</li>
<li><strong>Moderate</strong>: >3ยฐC or >40mm/day deviation</li>
</ul>
Monitors 15 conflict/disaster-prone zones.`,
infoTooltip: t('components.climate.infoTooltip'),
});
this.showLoading('Loading climate data');
this.showLoading(t('common.loadingClimateData'));
}

public setZoneClickHandler(handler: (lat: number, lon: number) => void): void {
Expand All @@ -37,7 +31,7 @@ export class ClimateAnomalyPanel extends Panel {

private renderContent(): void {
if (this.anomalies.length === 0) {
this.setContent('<div class="panel-empty">No significant anomalies detected</div>');
this.setContent(`<div class="panel-empty">${t('components.climate.noAnomalies')}</div>`);
return;
}

Expand All @@ -57,7 +51,7 @@ export class ClimateAnomalyPanel extends Panel {
<td class="climate-zone"><span class="climate-icon">${icon}</span>${escapeHtml(a.zone)}</td>
<td class="climate-num ${tempClass}">${formatDelta(a.tempDelta, 'ยฐC')}</td>
<td class="climate-num ${precipClass}">${formatDelta(a.precipDelta, 'mm')}</td>
<td><span class="climate-badge ${sevClass}">${a.severity.toUpperCase()}</span></td>
<td><span class="climate-badge ${sevClass}">${t(`components.climate.severity.${a.severity}`)}</span></td>
</tr>`;
}).join('');

Expand All @@ -66,10 +60,10 @@ export class ClimateAnomalyPanel extends Panel {
<table class="climate-table">
<thead>
<tr>
<th>Zone</th>
<th>Temp</th>
<th>Precip</th>
<th>Severity</th>
<th>${t('components.climate.zone')}</th>
<th>${t('components.climate.temp')}</th>
<th>${t('components.climate.precip')}</th>
<th>${t('components.climate.severityLabel')}</th>
</tr>
</thead>
<tbody>${rows}</tbody>
Expand Down
Loading