From 534bda8e4fd11529feb8756389ced04ab772d761 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 12:20:56 +0000 Subject: [PATCH 1/4] feat: add Security Advisories panel with government travel advisory feeds Adds a new panel aggregating travel/security advisories from official government foreign affairs agencies (US State Dept, AU DFAT Smartraveller, UK FCDO, NZ MFAT). Advisories are categorized by severity level (Do Not Travel, Reconsider, Caution, Normal) with filter tabs by source country. Includes summary counts, auto-refresh, and persistent caching via the existing data-freshness system. https://claude.ai/code/session_011JRfLvQvaFnyEcbvcUeWmd --- api/rss-proxy.js | 4 + src/App.ts | 1 + src/app/data-loader.ts | 25 +++ src/app/panel-layout.ts | 8 + src/components/SecurityAdvisoriesPanel.ts | 207 +++++++++++++++++++++ src/components/index.ts | 1 + src/config/panels.ts | 3 +- src/locales/en.json | 25 ++- src/services/data-freshness.ts | 5 +- src/services/security-advisories.ts | 214 ++++++++++++++++++++++ src/styles/panels.css | 38 ++++ 11 files changed, 528 insertions(+), 3 deletions(-) create mode 100644 src/components/SecurityAdvisoriesPanel.ts create mode 100644 src/services/security-advisories.ts diff --git a/api/rss-proxy.js b/api/rss-proxy.js index 3a6ef35bb..ff7dfbcf5 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -279,6 +279,10 @@ const ALLOWED_DOMAINS = [ 'seekingalpha.com', 'www.coindesk.com', 'cointelegraph.com', + // Security advisories — government travel advisory feeds + 'travel.state.gov', + 'www.smartraveller.gov.au', + 'www.safetravel.govt.nz', // Happy variant — positive news sources 'www.goodnewsnetwork.org', 'www.positive.news', diff --git a/src/App.ts b/src/App.ts index e5541e1fc..71aeb0ffe 100644 --- a/src/App.ts +++ b/src/App.ts @@ -262,6 +262,7 @@ export class App { openCountryStory: (code, name) => this.countryIntel.openCountryStory(code, name), loadAllData: () => this.dataLoader.loadAllData(), updateMonitorResults: () => this.dataLoader.updateMonitorResults(), + loadSecurityAdvisories: () => this.dataLoader.loadSecurityAdvisories(), }); this.eventHandlers = new EventHandlerManager(this.state, { diff --git a/src/app/data-loader.ts b/src/app/data-loader.ts index b30eb65c2..351231be4 100644 --- a/src/app/data-loader.ts +++ b/src/app/data-loader.ts @@ -69,6 +69,7 @@ import { dataFreshness, type DataSourceId } from '@/services/data-freshness'; import { fetchConflictEvents, fetchUcdpClassifications, fetchHapiSummary, fetchUcdpEvents, deduplicateAgainstAcled } from '@/services/conflict'; import { fetchUnhcrPopulation } from '@/services/displacement'; import { fetchClimateAnomalies } from '@/services/climate'; +import { fetchSecurityAdvisories } from '@/services/security-advisories'; import { enrichEventsWithExposure } from '@/services/population-exposure'; import { debounce, getCircuitBreakerCooldownInfo } from '@/utils'; import { isFeatureAvailable } from '@/services/runtime-config'; @@ -95,6 +96,7 @@ import { PopulationExposurePanel, TradePolicyPanel, SupplyChainPanel, + SecurityAdvisoriesPanel, } from '@/components'; import { SatelliteFiresPanel } from '@/components/SatelliteFiresPanel'; import { classifyNewsItem } from '@/services/positive-classifier'; @@ -1051,6 +1053,18 @@ export class DataLoaderManager implements AppModule { } })()); + // Security advisories + tasks.push((async () => { + try { + const result = await fetchSecurityAdvisories(); + if (result.ok) { + (this.ctx.panels['security-advisories'] as SecurityAdvisoriesPanel)?.setData(result.advisories); + } + } catch (error) { + console.error('[Intelligence] Security advisories fetch failed:', error); + } + })()); + await Promise.allSettled(tasks); try { @@ -1865,4 +1879,15 @@ export class DataLoaderManager implements AppModule { // EIA failure does not break the existing World Bank gauge } } + + async loadSecurityAdvisories(): Promise { + try { + const result = await fetchSecurityAdvisories(); + if (result.ok) { + (this.ctx.panels['security-advisories'] as SecurityAdvisoriesPanel)?.setData(result.advisories); + } + } catch (error) { + console.error('[App] Security advisories fetch failed:', error); + } + } } diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index 474cecc2d..c137e446f 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -33,6 +33,7 @@ import { InvestmentsPanel, TradePolicyPanel, SupplyChainPanel, + SecurityAdvisoriesPanel, } from '@/components'; import { SatelliteFiresPanel } from '@/components/SatelliteFiresPanel'; import { PositiveNewsFeedPanel } from '@/components/PositiveNewsFeedPanel'; @@ -63,6 +64,7 @@ export interface PanelLayoutCallbacks { openCountryStory: (code: string, name: string) => void; loadAllData: () => Promise; updateMonitorResults: () => void; + loadSecurityAdvisories?: () => Promise; } export class PanelLayoutManager implements AppModule { @@ -539,6 +541,12 @@ export class PanelLayoutManager implements AppModule { const populationExposurePanel = new PopulationExposurePanel(); this.ctx.panels['population-exposure'] = populationExposurePanel; + + const securityAdvisoriesPanel = new SecurityAdvisoriesPanel(); + securityAdvisoriesPanel.setRefreshHandler(() => { + void this.callbacks.loadSecurityAdvisories?.(); + }); + this.ctx.panels['security-advisories'] = securityAdvisoriesPanel; } if (SITE_VARIANT === 'finance') { diff --git a/src/components/SecurityAdvisoriesPanel.ts b/src/components/SecurityAdvisoriesPanel.ts new file mode 100644 index 000000000..b52aedd5d --- /dev/null +++ b/src/components/SecurityAdvisoriesPanel.ts @@ -0,0 +1,207 @@ +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; +import { t } from '@/services/i18n'; +import type { SecurityAdvisory } from '@/services/security-advisories'; + +type AdvisoryFilter = 'all' | 'critical' | 'US' | 'AU' | 'UK' | 'NZ'; + +export class SecurityAdvisoriesPanel extends Panel { + private advisories: SecurityAdvisory[] = []; + private activeFilter: AdvisoryFilter = 'all'; + private refreshInterval: ReturnType | null = null; + private onRefreshRequest?: () => void; + + constructor() { + super({ + id: 'security-advisories', + title: t('panels.securityAdvisories'), + showCount: true, + trackActivity: true, + infoTooltip: t('components.securityAdvisories.infoTooltip'), + }); + this.showLoading(t('components.securityAdvisories.loading')); + } + + public setData(advisories: SecurityAdvisory[]): void { + const prevCount = this.advisories.length; + this.advisories = advisories; + this.setCount(advisories.length); + + if (prevCount > 0 && advisories.length > prevCount) { + this.setNewBadge(advisories.length - prevCount); + } + + this.render(); + } + + private getFiltered(): SecurityAdvisory[] { + switch (this.activeFilter) { + case 'critical': + return this.advisories.filter(a => a.level === 'do-not-travel' || a.level === 'reconsider'); + case 'US': + case 'AU': + case 'UK': + case 'NZ': + return this.advisories.filter(a => a.sourceCountry === this.activeFilter); + default: + return this.advisories; + } + } + + private getLevelClass(level?: SecurityAdvisory['level']): string { + switch (level) { + case 'do-not-travel': return 'sa-level-dnt'; + case 'reconsider': return 'sa-level-reconsider'; + case 'caution': return 'sa-level-caution'; + case 'normal': return 'sa-level-normal'; + default: return 'sa-level-info'; + } + } + + private getLevelLabel(level?: SecurityAdvisory['level']): string { + switch (level) { + case 'do-not-travel': return t('components.securityAdvisories.levels.doNotTravel'); + case 'reconsider': return t('components.securityAdvisories.levels.reconsider'); + case 'caution': return t('components.securityAdvisories.levels.caution'); + case 'normal': return t('components.securityAdvisories.levels.normal'); + default: return t('components.securityAdvisories.levels.info'); + } + } + + private getSourceFlag(sourceCountry: string): string { + switch (sourceCountry) { + case 'US': return '\u{1F1FA}\u{1F1F8}'; + case 'AU': return '\u{1F1E6}\u{1F1FA}'; + case 'UK': return '\u{1F1EC}\u{1F1E7}'; + case 'NZ': return '\u{1F1F3}\u{1F1FF}'; + default: return '\u{1F310}'; + } + } + + private formatTime(date: Date): string { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (minutes < 1) return t('components.securityAdvisories.time.justNow'); + if (minutes < 60) return t('components.securityAdvisories.time.minutesAgo', { count: String(minutes) }); + if (hours < 24) return t('components.securityAdvisories.time.hoursAgo', { count: String(hours) }); + if (days < 7) return t('components.securityAdvisories.time.daysAgo', { count: String(days) }); + return date.toLocaleDateString(); + } + + private render(): void { + if (this.advisories.length === 0) { + this.setContent(`
${t('common.noDataAvailable')}
`); + return; + } + + const filtered = this.getFiltered(); + + // Summary counts + const dntCount = this.advisories.filter(a => a.level === 'do-not-travel').length; + const reconsiderCount = this.advisories.filter(a => a.level === 'reconsider').length; + const cautionCount = this.advisories.filter(a => a.level === 'caution').length; + + const summaryHtml = ` +
+
+ ${dntCount} + ${t('components.securityAdvisories.levels.doNotTravel')} +
+
+ ${reconsiderCount} + ${t('components.securityAdvisories.levels.reconsider')} +
+
+ ${cautionCount} + ${t('components.securityAdvisories.levels.caution')} +
+
+ `; + + const filtersHtml = ` +
+ + + + + + +
+ `; + + const displayed = filtered.slice(0, 30); + let itemsHtml: string; + + if (displayed.length === 0) { + itemsHtml = `
${t('components.securityAdvisories.noMatching')}
`; + } else { + itemsHtml = displayed.map(a => { + const levelCls = this.getLevelClass(a.level); + const levelLabel = this.getLevelLabel(a.level); + const flag = this.getSourceFlag(a.sourceCountry); + + return `
+
+ ${levelLabel} + ${flag} ${escapeHtml(a.source)} +
+ ${escapeHtml(a.title)} +
${this.formatTime(a.pubDate)}
+
`; + }).join(''); + } + + const footerHtml = ` + + `; + + this.content.innerHTML = ` +
+ ${summaryHtml} + ${filtersHtml} +
${itemsHtml}
+ ${footerHtml} +
+ `; + + this.attachEventListeners(); + } + + private attachEventListeners(): void { + // Filter buttons + const filters = this.content.querySelectorAll('.sa-filter'); + filters.forEach(btn => { + btn.addEventListener('click', () => { + this.activeFilter = (btn as HTMLElement).dataset.filter as AdvisoryFilter; + this.render(); + }); + }); + + // Refresh button + const refreshBtn = this.content.querySelector('.sa-refresh-btn'); + if (refreshBtn) { + refreshBtn.addEventListener('click', () => { + this.showLoading(t('components.securityAdvisories.loading')); + this.onRefreshRequest?.(); + }); + } + } + + public setRefreshHandler(handler: () => void): void { + this.onRefreshRequest = handler; + } + + public destroy(): void { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + super.destroy(); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 14919ecdd..bd1db6b65 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,3 +41,4 @@ export * from './InvestmentsPanel'; export * from './UnifiedSettings'; export * from './TradePolicyPanel'; export * from './SupplyChainPanel'; +export * from './SecurityAdvisoriesPanel'; diff --git a/src/config/panels.ts b/src/config/panels.ts index 5710378fd..84d60c9ce 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -50,6 +50,7 @@ const FULL_PANELS: Record = { displacement: { name: 'UNHCR Displacement', enabled: true, priority: 2 }, climate: { name: 'Climate Anomalies', enabled: true, priority: 2 }, 'population-exposure': { name: 'Population Exposure', enabled: true, priority: 2 }, + 'security-advisories': { name: 'Security Advisories', enabled: true, priority: 2 }, }; const FULL_MAP_LAYERS: MapLayers = { @@ -587,7 +588,7 @@ export const PANEL_CATEGORY_MAP: RecordPopulation Exposure Estimates Estimated population within event impact radius. Based on WorldPop country density data.
  • Conflict: 50km radius
  • Earthquake: 100km radius
  • Flood: 100km radius
  • Wildfire: 30km radius
" }, + "securityAdvisories": { + "loading": "Fetching travel advisories...", + "noMatching": "No advisories match this filter", + "critical": "Critical", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Refresh", + "levels": { + "doNotTravel": "Do Not Travel", + "reconsider": "Reconsider Travel", + "caution": "Exercise Caution", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "just now", + "minutesAgo": "{{count}}m ago", + "hoursAgo": "{{count}}h ago", + "daysAgo": "{{count}}d ago" + }, + "infoTooltip": "Security Advisories
Travel advisories and security alerts from government foreign affairs agencies:

Sources:
\uD83C\uDDFA\uD83C\uDDF8 US State Dept Travel Advisories
\uD83C\uDDE6\uD83C\uDDFA AU DFAT Smartraveller
\uD83C\uDDEC\uD83C\uDDE7 UK FCDO Travel Advice
\uD83C\uDDF3\uD83C\uDDFF NZ MFAT SafeTravel

Levels:
\uD83D\uDFE5 Do Not Travel
\uD83D\uDFE7 Reconsider Travel
\uD83D\uDFE8 Exercise Caution
\uD83D\uDFE9 Normal Precautions" + }, "satelliteFires": { "noData": "No fire data available", "region": "Region", @@ -2020,6 +2042,7 @@ "currentVariant": "(current)", "retry": "Retry", "retrying": "Retrying...", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "All" } } diff --git a/src/services/data-freshness.ts b/src/services/data-freshness.ts index 316b25666..bc6265e78 100644 --- a/src/services/data-freshness.ts +++ b/src/services/data-freshness.ts @@ -35,7 +35,8 @@ export type DataSourceId = | 'giving' // Global giving activity data | 'bis' // BIS central bank data | 'wto_trade' // WTO trade policy data - | 'supply_chain'; // Supply chain disruption intelligence + | 'supply_chain' // Supply chain disruption intelligence + | 'security_advisories'; // Government travel/security advisories export type FreshnessStatus = 'fresh' | 'stale' | 'very_stale' | 'no_data' | 'disabled' | 'error'; @@ -101,6 +102,7 @@ const SOURCE_METADATA: Record = { bis: 'Central bank policy data may be stale—BIS feed unavailable', wto_trade: 'Trade policy intelligence unavailable—WTO data not updating', supply_chain: 'Supply chain disruption status unavailable—chokepoint monitoring offline', + security_advisories: 'Government travel advisory data unavailable—security alerts may be missed', }; /** diff --git a/src/services/security-advisories.ts b/src/services/security-advisories.ts new file mode 100644 index 000000000..4f9e914be --- /dev/null +++ b/src/services/security-advisories.ts @@ -0,0 +1,214 @@ +import { proxyUrl } from '@/utils'; +import { getPersistentCache, setPersistentCache } from './persistent-cache'; +import { dataFreshness } from './data-freshness'; + +export interface SecurityAdvisory { + title: string; + link: string; + pubDate: Date; + source: string; + sourceCountry: string; + level?: 'do-not-travel' | 'reconsider' | 'caution' | 'normal' | 'info'; + country?: string; +} + +export interface SecurityAdvisoriesFetchResult { + ok: boolean; + advisories: SecurityAdvisory[]; + cachedAt?: string; +} + +interface AdvisoryFeed { + name: string; + sourceCountry: string; + url: string; + parseLevel?: (title: string) => SecurityAdvisory['level']; +} + +const US_LEVEL_RE = /Level (\d)/i; + +function parseUsLevel(title: string): SecurityAdvisory['level'] { + const m = title.match(US_LEVEL_RE); + if (!m) return 'info'; + switch (m[1]) { + case '4': return 'do-not-travel'; + case '3': return 'reconsider'; + case '2': return 'caution'; + case '1': return 'normal'; + default: return 'info'; + } +} + +function parseAuLevel(title: string): SecurityAdvisory['level'] { + const lower = title.toLowerCase(); + if (lower.includes('do not travel')) return 'do-not-travel'; + if (lower.includes('reconsider')) return 'reconsider'; + if (lower.includes('exercise a high degree of caution') || lower.includes('high degree')) return 'caution'; + return 'info'; +} + +const ADVISORY_FEEDS: AdvisoryFeed[] = [ + // United States (State Dept) + { + name: 'US State Dept', + sourceCountry: 'US', + url: 'https://travel.state.gov/_res/rss/TAsTWs.xml', + parseLevel: parseUsLevel, + }, + // Australia (DFAT Smartraveller) — all destinations + { + name: 'AU Smartraveller', + sourceCountry: 'AU', + url: 'https://www.smartraveller.gov.au/countries/documents/index.rss', + parseLevel: parseAuLevel, + }, + // Australia — Do Not Travel specifically + { + name: 'AU DNT', + sourceCountry: 'AU', + url: 'https://www.smartraveller.gov.au/countries/documents/do-not-travel.rss', + parseLevel: () => 'do-not-travel', + }, + // Australia — Reconsider + { + name: 'AU Reconsider', + sourceCountry: 'AU', + url: 'https://www.smartraveller.gov.au/countries/documents/reconsider-your-need-to-travel.rss', + parseLevel: () => 'reconsider', + }, + // New Zealand MFAT + { + name: 'NZ MFAT', + sourceCountry: 'NZ', + url: 'https://www.safetravel.govt.nz/news/feed', + parseLevel: parseAuLevel, + }, + // UK FCDO — GOV.UK travel advice atom feed + { + name: 'UK FCDO', + sourceCountry: 'UK', + url: 'https://www.gov.uk/foreign-travel-advice.atom', + }, +]; + +const CACHE_KEY = 'security-advisories'; +const CACHE_TTL = 15 * 60 * 1000; // 15 minutes +let lastFetch = 0; +let cachedResult: SecurityAdvisory[] | null = null; + +function parseFeedXml( + text: string, + feed: AdvisoryFeed, +): SecurityAdvisory[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(text, 'text/xml'); + + const parseError = doc.querySelector('parsererror'); + if (parseError) return []; + + // Try RSS items first, then Atom entries + let items = doc.querySelectorAll('item'); + const isAtom = items.length === 0; + if (isAtom) items = doc.querySelectorAll('entry'); + + return Array.from(items).slice(0, 15).map(item => { + const title = item.querySelector('title')?.textContent?.trim() || ''; + let link = ''; + if (isAtom) { + const linkEl = item.querySelector('link[href]'); + link = linkEl?.getAttribute('href') || ''; + } else { + link = item.querySelector('link')?.textContent?.trim() || ''; + } + + const pubDateStr = isAtom + ? (item.querySelector('updated')?.textContent || item.querySelector('published')?.textContent || '') + : (item.querySelector('pubDate')?.textContent || ''); + const parsed = pubDateStr ? new Date(pubDateStr) : new Date(); + const pubDate = Number.isNaN(parsed.getTime()) ? new Date() : parsed; + + const level = feed.parseLevel ? feed.parseLevel(title) : 'info'; + + return { + title, + link, + pubDate, + source: feed.name, + sourceCountry: feed.sourceCountry, + level, + }; + }); +} + +function toSerializable(items: SecurityAdvisory[]): Array & { pubDate: string }> { + return items.map(item => ({ ...item, pubDate: item.pubDate.toISOString() })); +} + +function fromSerializable(items: Array & { pubDate: string }>): SecurityAdvisory[] { + return items.map(item => ({ ...item, pubDate: new Date(item.pubDate) })); +} + +export async function fetchSecurityAdvisories( + signal?: AbortSignal, +): Promise { + const now = Date.now(); + + // Return in-memory cache if fresh + if (cachedResult && now - lastFetch < CACHE_TTL) { + return { ok: true, advisories: cachedResult }; + } + + const allAdvisories: SecurityAdvisory[] = []; + const feedResults = await Promise.allSettled( + ADVISORY_FEEDS.map(async (feed) => { + try { + const response = await fetch(proxyUrl(feed.url), signal ? { signal } : undefined); + if (!response.ok) { + console.warn(`[SecurityAdvisories] ${feed.name} HTTP ${response.status}`); + return []; + } + const text = await response.text(); + return parseFeedXml(text, feed); + } catch (e) { + if (e instanceof DOMException && e.name === 'AbortError') throw e; + console.warn(`[SecurityAdvisories] ${feed.name} failed:`, e); + return []; + } + }), + ); + + for (const result of feedResults) { + if (result.status === 'fulfilled') { + allAdvisories.push(...result.value); + } + } + + // Deduplicate by title (AU feeds can overlap) + const seen = new Set(); + const deduped = allAdvisories.filter(a => { + const key = a.title.toLowerCase().trim(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + // Sort by date descending + deduped.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()); + + // Cache + cachedResult = deduped; + lastFetch = now; + void setPersistentCache(CACHE_KEY, toSerializable(deduped)); + + if (deduped.length > 0) { + dataFreshness.recordUpdate('security_advisories', deduped.length); + } + + return { ok: true, advisories: deduped }; +} + +export async function loadCachedAdvisories(): Promise { + const entry = await getPersistentCache & { pubDate: string }>>(CACHE_KEY); + if (!entry?.data?.length) return null; + return fromSerializable(entry.data); +} diff --git a/src/styles/panels.css b/src/styles/panels.css index dc482619f..571fbae60 100644 --- a/src/styles/panels.css +++ b/src/styles/panels.css @@ -221,3 +221,41 @@ .giving-receiver-list li:last-child { border-bottom: none; } .giving-inst-content { } .giving-inst-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; } + +/* ---------------------------------------------------------- + Security Advisories Panel + ---------------------------------------------------------- */ +.sa-panel-content { font-size: 12px; } +.sa-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 8px; } +.sa-summary-item { background: var(--overlay-subtle); border: 1px solid var(--border); border-radius: 4px; padding: 8px 6px; text-align: center; } +.sa-summary-count { display: block; font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; } +.sa-summary-label { display: block; font-size: 8px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; } +.sa-summary-item.sa-level-dnt .sa-summary-count { color: var(--semantic-critical); } +.sa-summary-item.sa-level-reconsider .sa-summary-count { color: var(--semantic-high); } +.sa-summary-item.sa-level-caution .sa-summary-count { color: var(--semantic-elevated); } +.sa-filters { display: flex; gap: 2px; margin-bottom: 6px; flex-wrap: wrap; } +.sa-filter { background: transparent; border: 1px solid var(--border-strong); color: var(--text-dim); padding: 3px 10px; font-size: 10px; cursor: pointer; border-radius: 3px; transition: all 0.15s; } +.sa-filter:hover { border-color: var(--text-faint); color: var(--text-secondary); } +.sa-filter-active { background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: var(--accent); color: var(--accent); } +.sa-list { display: flex; flex-direction: column; gap: 4px; } +.sa-item { padding: 8px; border-radius: 4px; border-left: 3px solid var(--border); background: var(--overlay-subtle); } +.sa-item.sa-level-dnt { border-left-color: var(--semantic-critical); } +.sa-item.sa-level-reconsider { border-left-color: var(--semantic-high); } +.sa-item.sa-level-caution { border-left-color: var(--semantic-elevated); } +.sa-item.sa-level-normal { border-left-color: var(--semantic-normal); } +.sa-item.sa-level-info { border-left-color: var(--text-muted); } +.sa-item-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; } +.sa-badge { font-size: 8px; font-weight: 700; padding: 2px 6px; border-radius: 3px; letter-spacing: 0.5px; text-transform: uppercase; } +.sa-badge.sa-level-dnt { background: color-mix(in srgb, var(--semantic-critical) 20%, transparent); color: var(--semantic-critical); } +.sa-badge.sa-level-reconsider { background: color-mix(in srgb, var(--semantic-high) 15%, transparent); color: var(--semantic-high); } +.sa-badge.sa-level-caution { background: color-mix(in srgb, var(--semantic-elevated) 12%, transparent); color: var(--semantic-elevated); } +.sa-badge.sa-level-normal { background: color-mix(in srgb, var(--semantic-normal) 10%, transparent); color: var(--semantic-normal); } +.sa-badge.sa-level-info { background: color-mix(in srgb, var(--text-muted) 10%, transparent); color: var(--text-muted); } +.sa-source { font-size: 10px; color: var(--text-muted); margin-left: auto; } +.sa-title { display: block; color: var(--text-secondary); text-decoration: none; font-size: 11px; line-height: 1.35; } +.sa-title:hover { color: var(--accent); text-decoration: underline; } +.sa-time { font-size: 9px; color: var(--text-muted); margin-top: 3px; } +.sa-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--border-subtle); } +.sa-footer-source { font-size: 9px; color: var(--text-muted); } +.sa-refresh-btn { background: transparent; border: 1px solid var(--border); color: var(--text-dim); padding: 3px 10px; font-size: 10px; cursor: pointer; border-radius: 3px; } +.sa-refresh-btn:hover { border-color: var(--accent); color: var(--accent); } From 82d90878316b671f785b5bc8df213bdfa08d5b41 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 12:21:35 +0000 Subject: [PATCH 2/4] chore: update package-lock.json https://claude.ai/code/session_011JRfLvQvaFnyEcbvcUeWmd --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a6f1f69e..665af289c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "world-monitor", - "version": "2.5.12", + "version": "2.5.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "world-monitor", - "version": "2.5.12", + "version": "2.5.19", "license": "AGPL-3.0-only", "dependencies": { "@deck.gl/aggregation-layers": "^9.2.6", From 48691be6cb08efbd752fbd4441419b6ad4305818 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Fri, 27 Feb 2026 16:38:00 +0400 Subject: [PATCH 3/4] fix: event delegation, localization, and cleanup for SecurityAdvisories panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 fixes: - Use event delegation on this.content (bound once in constructor) instead of direct addEventListener after each innerHTML replacement — prevents memory leaks and stale listener issues on re-render - Use setContent() consistently instead of mixing with this.content.innerHTML - Add securityAdvisories translations to all 16 non-English locale files (panels name, component strings, common.all key) - Revert unrelated package-lock.json version bump P2 fixes: - Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the shared method instead of inlining duplicate fetch+set logic - Add Accept header to fetch calls for better content negotiation --- package-lock.json | 4 +-- src/app/data-loader.ts | 11 +----- src/components/SecurityAdvisoriesPanel.ts | 41 +++++++++-------------- src/locales/ar.json | 27 +++++++++++++-- src/locales/de.json | 27 +++++++++++++-- src/locales/el.json | 27 +++++++++++++-- src/locales/es.json | 27 +++++++++++++-- src/locales/fr.json | 27 +++++++++++++-- src/locales/it.json | 27 +++++++++++++-- src/locales/ja.json | 27 +++++++++++++-- src/locales/nl.json | 27 +++++++++++++-- src/locales/pl.json | 27 +++++++++++++-- src/locales/pt.json | 27 +++++++++++++-- src/locales/ru.json | 27 +++++++++++++-- src/locales/sv.json | 27 +++++++++++++-- src/locales/th.json | 27 +++++++++++++-- src/locales/tr.json | 27 +++++++++++++-- src/locales/vi.json | 27 +++++++++++++-- src/locales/zh.json | 27 +++++++++++++-- src/services/security-advisories.ts | 5 ++- 20 files changed, 423 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 665af289c..9a6f1f69e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "world-monitor", - "version": "2.5.19", + "version": "2.5.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "world-monitor", - "version": "2.5.19", + "version": "2.5.12", "license": "AGPL-3.0-only", "dependencies": { "@deck.gl/aggregation-layers": "^9.2.6", diff --git a/src/app/data-loader.ts b/src/app/data-loader.ts index 351231be4..8e27d2683 100644 --- a/src/app/data-loader.ts +++ b/src/app/data-loader.ts @@ -1054,16 +1054,7 @@ export class DataLoaderManager implements AppModule { })()); // Security advisories - tasks.push((async () => { - try { - const result = await fetchSecurityAdvisories(); - if (result.ok) { - (this.ctx.panels['security-advisories'] as SecurityAdvisoriesPanel)?.setData(result.advisories); - } - } catch (error) { - console.error('[Intelligence] Security advisories fetch failed:', error); - } - })()); + tasks.push(this.loadSecurityAdvisories()); await Promise.allSettled(tasks); diff --git a/src/components/SecurityAdvisoriesPanel.ts b/src/components/SecurityAdvisoriesPanel.ts index b52aedd5d..ea2ca0e66 100644 --- a/src/components/SecurityAdvisoriesPanel.ts +++ b/src/components/SecurityAdvisoriesPanel.ts @@ -20,6 +20,20 @@ export class SecurityAdvisoriesPanel extends Panel { infoTooltip: t('components.securityAdvisories.infoTooltip'), }); this.showLoading(t('components.securityAdvisories.loading')); + + this.content.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const filterBtn = target.closest('.sa-filter'); + if (filterBtn) { + this.activeFilter = (filterBtn.dataset.filter || 'all') as AdvisoryFilter; + this.render(); + return; + } + if (target.closest('.sa-refresh-btn')) { + this.showLoading(t('components.securityAdvisories.loading')); + this.onRefreshRequest?.(); + } + }); } public setData(advisories: SecurityAdvisory[]): void { @@ -100,7 +114,6 @@ export class SecurityAdvisoriesPanel extends Panel { const filtered = this.getFiltered(); - // Summary counts const dntCount = this.advisories.filter(a => a.level === 'do-not-travel').length; const reconsiderCount = this.advisories.filter(a => a.level === 'reconsider').length; const cautionCount = this.advisories.filter(a => a.level === 'caution').length; @@ -162,36 +175,14 @@ export class SecurityAdvisoriesPanel extends Panel { `; - this.content.innerHTML = ` + this.setContent(`
${summaryHtml} ${filtersHtml}
${itemsHtml}
${footerHtml}
- `; - - this.attachEventListeners(); - } - - private attachEventListeners(): void { - // Filter buttons - const filters = this.content.querySelectorAll('.sa-filter'); - filters.forEach(btn => { - btn.addEventListener('click', () => { - this.activeFilter = (btn as HTMLElement).dataset.filter as AdvisoryFilter; - this.render(); - }); - }); - - // Refresh button - const refreshBtn = this.content.querySelector('.sa-refresh-btn'); - if (refreshBtn) { - refreshBtn.addEventListener('click', () => { - this.showLoading(t('components.securityAdvisories.loading')); - this.onRefreshRequest?.(); - }); - } + `); } public setRefreshHandler(handler: () => void): void { diff --git a/src/locales/ar.json b/src/locales/ar.json index d80f4ccb8..09b88dbce 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -203,7 +203,8 @@ "techReadiness": "مؤشر الجاهزية التقنية", "gccInvestments": "استثمارات دول الخليج", "geoHubs": "مراكز جيوسياسية", - "liveWebcams": "كاميرات مباشرة" + "liveWebcams": "كاميرات مباشرة", + "securityAdvisories": "تنبيهات أمنية" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "آسيا", "regionMiddleEast": "الشرق الأوسط", "regionAfrica": "أفريقيا" + }, + "securityAdvisories": { + "loading": "جاري جلب التحذيرات الأمنية...", + "noMatching": "لا توجد تحذيرات مطابقة لهذا الفلتر", + "critical": "حرج", + "sources": "وزارة الخارجية الأمريكية، DFAT الأسترالية، FCDO البريطانية، MFAT النيوزيلندية", + "refresh": "تحديث", + "levels": { + "doNotTravel": "لا تسافر", + "reconsider": "أعد التفكير في السفر", + "caution": "توخي الحذر", + "normal": "عادي", + "info": "معلومات" + }, + "time": { + "justNow": "الآن", + "minutesAgo": "منذ {{count}} دقيقة", + "hoursAgo": "منذ {{count}} ساعة", + "daysAgo": "منذ {{count}} يوم" + }, + "infoTooltip": "تنبيهات أمنية
تحذيرات السفر والتنبيهات الأمنية من الوكالات الحكومية." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "إغلاق", "currentVariant": "(الحالي)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "الكل" } } diff --git a/src/locales/de.json b/src/locales/de.json index 7c7eaee78..f13ac23b8 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -203,7 +203,8 @@ "techHubs": "Heiße Tech-Hubs", "gccInvestments": "GCC-Investitionen", "geoHubs": "Geopolitical Hotspots", - "liveWebcams": "Live-Webcams" + "liveWebcams": "Live-Webcams", + "securityAdvisories": "Sicherheitshinweise" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Asien", "regionMiddleEast": "Naher Osten", "regionAfrica": "Afrika" + }, + "securityAdvisories": { + "loading": "Reisehinweise werden geladen...", + "noMatching": "Keine Hinweise für diesen Filter", + "critical": "Kritisch", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Aktualisieren", + "levels": { + "doNotTravel": "Nicht reisen", + "reconsider": "Reise überdenken", + "caution": "Vorsicht", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "gerade eben", + "minutesAgo": "vor {{count}} Min.", + "hoursAgo": "vor {{count}} Std.", + "daysAgo": "vor {{count}} Tagen" + }, + "infoTooltip": "Sicherheitshinweise
Reisewarnungen und Sicherheitshinweise von Regierungsbehörden." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Schließen", "currentVariant": "(aktuell)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Alle" } } diff --git a/src/locales/el.json b/src/locales/el.json index c1ed5a7db..e39903aac 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -203,7 +203,8 @@ "techReadiness": "Δείκτης Τεχνολογικής Ετοιμότητας", "gccInvestments": "Επενδύσεις GCC", "geoHubs": "Γεωπολιτικά Κέντρα", - "liveWebcams": "Ζωντανές Κάμερες" + "liveWebcams": "Ζωντανές Κάμερες", + "securityAdvisories": "Προειδοποιήσεις Ασφαλείας" }, "modals": { "search": { @@ -1312,6 +1313,27 @@ "regionAsia": "Ασία", "regionMiddleEast": "Μέση Ανατολή", "regionAfrica": "Αφρική" + }, + "securityAdvisories": { + "loading": "Φόρτωση ταξιδιωτικών οδηγιών...", + "noMatching": "Δεν βρέθηκαν οδηγίες για αυτό το φίλτρο", + "critical": "Κρίσιμο", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Ανανέωση", + "levels": { + "doNotTravel": "Μην ταξιδεύετε", + "reconsider": "Επανεξετάστε", + "caution": "Προσοχή", + "normal": "Κανονικό", + "info": "Πληροφορία" + }, + "time": { + "justNow": "μόλις τώρα", + "minutesAgo": "{{count}} λεπτά πριν", + "hoursAgo": "{{count}} ώρες πριν", + "daysAgo": "{{count}} ημέρες πριν" + }, + "infoTooltip": "Προειδοποιήσεις Ασφαλείας
Ταξιδιωτικές οδηγίες και προειδοποιήσεις ασφαλείας." } }, "popups": { @@ -1949,6 +1971,7 @@ "close": "Κλείσιμο", "currentVariant": "(τρέχον)", "retry": "Επανάληψη", - "refresh": "Ανανέωση" + "refresh": "Ανανέωση", + "all": "Όλα" } } diff --git a/src/locales/es.json b/src/locales/es.json index 11ca7ddd4..582cf09c2 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -203,7 +203,8 @@ "techHubs": "Centros tecnológicos de moda", "gccInvestments": "Inversiones del CCG", "geoHubs": "Geopolitical Hotspots", - "liveWebcams": "Cámaras en Vivo" + "liveWebcams": "Cámaras en Vivo", + "securityAdvisories": "Alertas de Seguridad" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Asia", "regionMiddleEast": "Oriente Medio", "regionAfrica": "África" + }, + "securityAdvisories": { + "loading": "Cargando alertas de viaje...", + "noMatching": "No hay alertas para este filtro", + "critical": "Crítico", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Actualizar", + "levels": { + "doNotTravel": "No viajar", + "reconsider": "Reconsiderar viaje", + "caution": "Precaución", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "ahora", + "minutesAgo": "hace {{count}} min", + "hoursAgo": "hace {{count}} h", + "daysAgo": "hace {{count}} d" + }, + "infoTooltip": "Alertas de Seguridad
Avisos de viaje y alertas de seguridad de agencias gubernamentales." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Cerrar", "currentVariant": "(actual)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Todos" } } diff --git a/src/locales/fr.json b/src/locales/fr.json index cd43b917f..9f992865c 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -203,7 +203,8 @@ "techHubs": "Pôles Technologiques", "gccInvestments": "Investissements du CCG", "geoHubs": "Centres géopolitiques", - "liveWebcams": "Webcams en Direct" + "liveWebcams": "Webcams en Direct", + "securityAdvisories": "Avis de Sécurité" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Asie", "regionMiddleEast": "Moyen-Orient", "regionAfrica": "Afrique" + }, + "securityAdvisories": { + "loading": "Chargement des avis de voyage...", + "noMatching": "Aucun avis pour ce filtre", + "critical": "Critique", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Actualiser", + "levels": { + "doNotTravel": "Ne pas voyager", + "reconsider": "Reconsidérer le voyage", + "caution": "Prudence", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "à l'instant", + "minutesAgo": "il y a {{count}} min", + "hoursAgo": "il y a {{count}} h", + "daysAgo": "il y a {{count}} j" + }, + "infoTooltip": "Avis de Sécurité
Avis aux voyageurs et alertes de sécurité des agences gouvernementales." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Fermer", "currentVariant": "(actuel)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Tous" } } diff --git a/src/locales/it.json b/src/locales/it.json index 44ff9451a..0ec0534f0 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -203,7 +203,8 @@ "techHubs": "Hub tecnologici caldi", "gccInvestments": "Investimenti GCC", "geoHubs": "Hotspot geopolitici", - "liveWebcams": "Webcam in Diretta" + "liveWebcams": "Webcam in Diretta", + "securityAdvisories": "Avvisi di Sicurezza" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Asia", "regionMiddleEast": "Medio Oriente", "regionAfrica": "Africa" + }, + "securityAdvisories": { + "loading": "Caricamento avvisi di viaggio...", + "noMatching": "Nessun avviso per questo filtro", + "critical": "Critico", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Aggiorna", + "levels": { + "doNotTravel": "Non viaggiare", + "reconsider": "Riconsiderare il viaggio", + "caution": "Cautela", + "normal": "Normale", + "info": "Info" + }, + "time": { + "justNow": "adesso", + "minutesAgo": "{{count}} min fa", + "hoursAgo": "{{count}} ore fa", + "daysAgo": "{{count}} giorni fa" + }, + "infoTooltip": "Avvisi di Sicurezza
Avvisi di viaggio e allerte di sicurezza dalle agenzie governative." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Chiudi", "currentVariant": "(corrente)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Tutti" } } diff --git a/src/locales/ja.json b/src/locales/ja.json index 006c10ff2..05cf2fa7d 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -203,7 +203,8 @@ "techReadiness": "テック準備度指数", "gccInvestments": "GCC投資", "geoHubs": "地政学ハブ", - "liveWebcams": "ライブカメラ" + "liveWebcams": "ライブカメラ", + "securityAdvisories": "セキュリティアドバイザリー" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "アジア", "regionMiddleEast": "中東", "regionAfrica": "アフリカ" + }, + "securityAdvisories": { + "loading": "渡航情報を取得中...", + "noMatching": "該当する情報なし", + "critical": "重大", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "更新", + "levels": { + "doNotTravel": "渡航中止", + "reconsider": "渡航再検討", + "caution": "注意", + "normal": "通常", + "info": "情報" + }, + "time": { + "justNow": "たった今", + "minutesAgo": "{{count}}分前", + "hoursAgo": "{{count}}時間前", + "daysAgo": "{{count}}日前" + }, + "infoTooltip": "セキュリティアドバイザリー
各国政府の渡航情報と安全警告。" } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "閉じる", "currentVariant": "(現在)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "すべて" } } diff --git a/src/locales/nl.json b/src/locales/nl.json index ebb127368..b9706b460 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -62,7 +62,8 @@ "geoHubs": "Geopolitical Hotspots", "polymarket": "Voorspellingen", "climate": "Klimaatafwijkingen", - "liveWebcams": "Live Webcams" + "liveWebcams": "Live Webcams", + "securityAdvisories": "Veiligheidsadviezen" }, "modals": { "search": { @@ -1144,6 +1145,27 @@ "regionAsia": "Azië", "regionMiddleEast": "Midden-Oosten", "regionAfrica": "Afrika" + }, + "securityAdvisories": { + "loading": "Reisadviezen laden...", + "noMatching": "Geen adviezen voor dit filter", + "critical": "Kritiek", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Vernieuwen", + "levels": { + "doNotTravel": "Niet reizen", + "reconsider": "Reis heroverwegen", + "caution": "Voorzichtigheid", + "normal": "Normaal", + "info": "Info" + }, + "time": { + "justNow": "zojuist", + "minutesAgo": "{{count}} min geleden", + "hoursAgo": "{{count}} uur geleden", + "daysAgo": "{{count}} dagen geleden" + }, + "infoTooltip": "Veiligheidsadviezen
Reisadviezen en veiligheidswaarschuwingen van overheidsinstanties." } }, "popups": { @@ -1781,7 +1803,8 @@ "close": "Sluiten", "currentVariant": "(huidig)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Alle" }, "header": { "world": "WERELD", diff --git a/src/locales/pl.json b/src/locales/pl.json index b53f43681..5aaa23b6f 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -203,7 +203,8 @@ "techHubs": "Gorące centra technologiczne", "gccInvestments": "Inwestycje GCC", "geoHubs": "Geopolitical Hotspots", - "liveWebcams": "Kamery na Żywo" + "liveWebcams": "Kamery na Żywo", + "securityAdvisories": "Ostrzeżenia Bezpieczeństwa" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Azja", "regionMiddleEast": "Bliski Wschód", "regionAfrica": "Afryka" + }, + "securityAdvisories": { + "loading": "Ładowanie ostrzeżeń podróżnych...", + "noMatching": "Brak ostrzeżeń dla tego filtra", + "critical": "Krytyczny", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Odśwież", + "levels": { + "doNotTravel": "Nie podróżuj", + "reconsider": "Rozważ podróż", + "caution": "Ostrożność", + "normal": "Normalny", + "info": "Info" + }, + "time": { + "justNow": "właśnie", + "minutesAgo": "{{count}} min temu", + "hoursAgo": "{{count}} godz. temu", + "daysAgo": "{{count}} dni temu" + }, + "infoTooltip": "Ostrzeżenia Bezpieczeństwa
Ostrzeżenia podróżne i alerty bezpieczeństwa z agencji rządowych." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Zamknij", "currentVariant": "(bieżący)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Wszystkie" } } diff --git a/src/locales/pt.json b/src/locales/pt.json index 85ffd3eb8..4e1f41fbf 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -62,7 +62,8 @@ "geoHubs": "Geopolitical Hotspots", "polymarket": "Previsões", "climate": "Anomalias Climáticas", - "liveWebcams": "Câmeras ao Vivo" + "liveWebcams": "Câmeras ao Vivo", + "securityAdvisories": "Alertas de Segurança" }, "modals": { "search": { @@ -1144,6 +1145,27 @@ "regionAsia": "Ásia", "regionMiddleEast": "Oriente Médio", "regionAfrica": "África" + }, + "securityAdvisories": { + "loading": "Carregando alertas de viagem...", + "noMatching": "Nenhum alerta para este filtro", + "critical": "Crítico", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Atualizar", + "levels": { + "doNotTravel": "Não viajar", + "reconsider": "Reconsiderar viagem", + "caution": "Cautela", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "agora", + "minutesAgo": "há {{count}} min", + "hoursAgo": "há {{count}} h", + "daysAgo": "há {{count}} d" + }, + "infoTooltip": "Alertas de Segurança
Avisos de viagem e alertas de segurança de agências governamentais." } }, "popups": { @@ -1781,7 +1803,8 @@ "close": "Fechar", "currentVariant": "(atual)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Todos" }, "header": { "world": "MUNDO", diff --git a/src/locales/ru.json b/src/locales/ru.json index 955bf5aa4..d16c11335 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -203,7 +203,8 @@ "techReadiness": "Индекс технологической готовности", "gccInvestments": "Инвестиции стран Залива", "geoHubs": "Геополитические хабы", - "liveWebcams": "Веб-камеры" + "liveWebcams": "Веб-камеры", + "securityAdvisories": "Предупреждения безопасности" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Азия", "regionMiddleEast": "Ближний Восток", "regionAfrica": "Африка" + }, + "securityAdvisories": { + "loading": "Загрузка предупреждений...", + "noMatching": "Нет предупреждений для этого фильтра", + "critical": "Критический", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Обновить", + "levels": { + "doNotTravel": "Не путешествовать", + "reconsider": "Пересмотреть поездку", + "caution": "Осторожность", + "normal": "Нормально", + "info": "Инфо" + }, + "time": { + "justNow": "только что", + "minutesAgo": "{{count}} мин. назад", + "hoursAgo": "{{count}} ч. назад", + "daysAgo": "{{count}} дн. назад" + }, + "infoTooltip": "Предупреждения безопасности
Рекомендации по поездкам и предупреждения от государственных ведомств." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Закрыть", "currentVariant": "(текущий)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Все" } } diff --git a/src/locales/sv.json b/src/locales/sv.json index 9a7df4661..acff392df 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -62,7 +62,8 @@ "geoHubs": "Geopolitical Hotspots", "polymarket": "Förutsägelser", "climate": "Klimatanomalier", - "liveWebcams": "Webbkameror" + "liveWebcams": "Webbkameror", + "securityAdvisories": "Säkerhetsvarningar" }, "modals": { "search": { @@ -1144,6 +1145,27 @@ "regionAsia": "Asien", "regionMiddleEast": "Mellanöstern", "regionAfrica": "Afrika" + }, + "securityAdvisories": { + "loading": "Hämtar resevarningar...", + "noMatching": "Inga varningar för detta filter", + "critical": "Kritisk", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Uppdatera", + "levels": { + "doNotTravel": "Res inte", + "reconsider": "Överväg resa", + "caution": "Försiktighet", + "normal": "Normal", + "info": "Info" + }, + "time": { + "justNow": "just nu", + "minutesAgo": "{{count}} min sedan", + "hoursAgo": "{{count}} tim sedan", + "daysAgo": "{{count}} dagar sedan" + }, + "infoTooltip": "Säkerhetsvarningar
Resevarningar och säkerhetsmeddelanden från myndigheters utrikesdepartement." } }, "popups": { @@ -1781,7 +1803,8 @@ "close": "Stäng", "currentVariant": "(aktuell)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Alla" }, "header": { "world": "VÄRLD", diff --git a/src/locales/th.json b/src/locales/th.json index b07ceba1f..57ea78e1c 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -203,7 +203,8 @@ "techReadiness": "ดัชนีความพร้อมด้านเทคโนโลยี", "gccInvestments": "การลงทุน GCC", "geoHubs": "ศูนย์กลางภูมิรัฐศาสตร์", - "liveWebcams": "เว็บแคมสด" + "liveWebcams": "เว็บแคมสด", + "securityAdvisories": "คำเตือนด้านความปลอดภัย" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "เอเชีย", "regionMiddleEast": "ตะวันออกกลาง", "regionAfrica": "แอฟริกา" + }, + "securityAdvisories": { + "loading": "กำลังโหลดคำเตือนการเดินทาง...", + "noMatching": "ไม่พบคำเตือนสำหรับตัวกรองนี้", + "critical": "วิกฤต", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "รีเฟรช", + "levels": { + "doNotTravel": "ห้ามเดินทาง", + "reconsider": "ทบทวนการเดินทาง", + "caution": "ระวัง", + "normal": "ปกติ", + "info": "ข้อมูล" + }, + "time": { + "justNow": "เมื่อสักครู่", + "minutesAgo": "{{count}} นาทีที่แล้ว", + "hoursAgo": "{{count}} ชั่วโมงที่แล้ว", + "daysAgo": "{{count}} วันที่แล้ว" + }, + "infoTooltip": "คำเตือนด้านความปลอดภัย
คำเตือนการเดินทางและความปลอดภัยจากหน่วยงานรัฐบาล." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "ปิด", "currentVariant": "(ปัจจุบัน)", "retry": "ลองใหม่", - "refresh": "รีเฟรช" + "refresh": "รีเฟรช", + "all": "ทั้งหมด" } } diff --git a/src/locales/tr.json b/src/locales/tr.json index d79be2396..7c3dd0e7d 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -203,7 +203,8 @@ "techReadiness": "Teknoloji Hazirlik Endeksi", "gccInvestments": "GCC Yatirimlari", "geoHubs": "Jeopolitik Merkezler", - "liveWebcams": "Canli Web Kameralari" + "liveWebcams": "Canli Web Kameralari", + "securityAdvisories": "Güvenlik Uyarıları" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Asya", "regionMiddleEast": "Orta Doğu", "regionAfrica": "Afrika" + }, + "securityAdvisories": { + "loading": "Seyahat uyarıları yükleniyor...", + "noMatching": "Bu filtre için uyarı yok", + "critical": "Kritik", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Yenile", + "levels": { + "doNotTravel": "Seyahat etmeyin", + "reconsider": "Seyahati yeniden düşünün", + "caution": "Dikkat", + "normal": "Normal", + "info": "Bilgi" + }, + "time": { + "justNow": "şimdi", + "minutesAgo": "{{count}} dk önce", + "hoursAgo": "{{count}} sa önce", + "daysAgo": "{{count}} gün önce" + }, + "infoTooltip": "Güvenlik Uyarıları
Hükümet kurumlarından seyahat uyarıları ve güvenlik bildirimleri." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Kapat", "currentVariant": "(mevcut)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "Tümü" } } diff --git a/src/locales/vi.json b/src/locales/vi.json index 8b9ddc995..d07a03d48 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -203,7 +203,8 @@ "techReadiness": "Chỉ số Sẵn sàng Công nghệ", "gccInvestments": "Đầu tư GCC", "geoHubs": "Trung tâm Địa chính trị", - "liveWebcams": "Webcam Trực tiếp" + "liveWebcams": "Webcam Trực tiếp", + "securityAdvisories": "Cảnh báo An ninh" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "Châu Á", "regionMiddleEast": "Trung Đông", "regionAfrica": "Châu Phi" + }, + "securityAdvisories": { + "loading": "Đang tải cảnh báo du lịch...", + "noMatching": "Không có cảnh báo cho bộ lọc này", + "critical": "Nghiêm trọng", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "Làm mới", + "levels": { + "doNotTravel": "Không du lịch", + "reconsider": "Cân nhắc lại", + "caution": "Thận trọng", + "normal": "Bình thường", + "info": "Thông tin" + }, + "time": { + "justNow": "vừa xong", + "minutesAgo": "{{count}} phút trước", + "hoursAgo": "{{count}} giờ trước", + "daysAgo": "{{count}} ngày trước" + }, + "infoTooltip": "Cảnh báo An ninh
Cảnh báo du lịch và an ninh từ các cơ quan chính phủ." } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "Đóng", "currentVariant": "(hiện tại)", "retry": "Thử lại", - "refresh": "Làm mới" + "refresh": "Làm mới", + "all": "Tất cả" } } diff --git a/src/locales/zh.json b/src/locales/zh.json index 1a79e3781..2a4588430 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -203,7 +203,8 @@ "techReadiness": "科技就绪指数", "gccInvestments": "GCC投资", "geoHubs": "地缘政治枢纽", - "liveWebcams": "实时摄像头" + "liveWebcams": "实时摄像头", + "securityAdvisories": "安全警告" }, "modals": { "search": { @@ -1285,6 +1286,27 @@ "regionAsia": "亚洲", "regionMiddleEast": "中东", "regionAfrica": "非洲" + }, + "securityAdvisories": { + "loading": "正在加载旅行警告...", + "noMatching": "没有匹配的警告", + "critical": "严重", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "refresh": "刷新", + "levels": { + "doNotTravel": "禁止出行", + "reconsider": "重新考虑出行", + "caution": "谨慎", + "normal": "正常", + "info": "信息" + }, + "time": { + "justNow": "刚刚", + "minutesAgo": "{{count}}分钟前", + "hoursAgo": "{{count}}小时前", + "daysAgo": "{{count}}天前" + }, + "infoTooltip": "安全警告
来自各国政府的旅行警告和安全提示。" } }, "popups": { @@ -1922,6 +1944,7 @@ "close": "关闭", "currentVariant": "(当前)", "retry": "Retry", - "refresh": "Refresh" + "refresh": "Refresh", + "all": "全部" } } diff --git a/src/services/security-advisories.ts b/src/services/security-advisories.ts index 4f9e914be..f6caacb50 100644 --- a/src/services/security-advisories.ts +++ b/src/services/security-advisories.ts @@ -162,7 +162,10 @@ export async function fetchSecurityAdvisories( const feedResults = await Promise.allSettled( ADVISORY_FEEDS.map(async (feed) => { try { - const response = await fetch(proxyUrl(feed.url), signal ? { signal } : undefined); + const response = await fetch(proxyUrl(feed.url), { + headers: { Accept: 'application/rss+xml, application/xml, text/xml, */*' }, + ...(signal ? { signal } : {}), + }); if (!response.ok) { console.warn(`[SecurityAdvisories] ${feed.name} HTTP ${response.status}`); return []; From 58cfa8d20005ff99c7ca7080134deaa107ac4ae4 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Fri, 27 Feb 2026 18:36:50 +0400 Subject: [PATCH 4/4] feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds Adds 21 new advisory RSS feeds: - 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM) - CDC Travel Notices - 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications) - 2 WHO feeds (global news, Africa emergencies) Panel gains a Health filter tab for CDC/ECDC/WHO sources. All new domains added to RSS proxy allowlist. i18n "health" key added across all 17 locales. --- api/rss-proxy.js | 19 +++++++++++++++++++ src/components/SecurityAdvisoriesPanel.ts | 7 ++++++- src/locales/ar.json | 3 ++- src/locales/de.json | 3 ++- src/locales/el.json | 3 ++- src/locales/en.json | 3 ++- src/locales/es.json | 3 ++- src/locales/fr.json | 3 ++- src/locales/it.json | 3 ++- src/locales/ja.json | 3 ++- src/locales/nl.json | 3 ++- src/locales/pl.json | 3 ++- src/locales/pt.json | 3 ++- src/locales/ru.json | 3 ++- src/locales/sv.json | 3 ++- src/locales/th.json | 3 ++- src/locales/tr.json | 3 ++- src/locales/vi.json | 3 ++- src/locales/zh.json | 3 ++- src/services/security-advisories.ts | 23 +++++++++++++++++++++++ 20 files changed, 82 insertions(+), 18 deletions(-) diff --git a/api/rss-proxy.js b/api/rss-proxy.js index ff7dfbcf5..2fd77ae07 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -283,6 +283,25 @@ const ALLOWED_DOMAINS = [ 'travel.state.gov', 'www.smartraveller.gov.au', 'www.safetravel.govt.nz', + // US Embassy security alerts + 'th.usembassy.gov', + 'ae.usembassy.gov', + 'de.usembassy.gov', + 'ua.usembassy.gov', + 'mx.usembassy.gov', + 'in.usembassy.gov', + 'pk.usembassy.gov', + 'co.usembassy.gov', + 'pl.usembassy.gov', + 'bd.usembassy.gov', + 'it.usembassy.gov', + 'do.usembassy.gov', + 'mm.usembassy.gov', + // Health advisories + 'wwwnc.cdc.gov', + 'www.ecdc.europa.eu', + 'www.who.int', + 'www.afro.who.int', // Happy variant — positive news sources 'www.goodnewsnetwork.org', 'www.positive.news', diff --git a/src/components/SecurityAdvisoriesPanel.ts b/src/components/SecurityAdvisoriesPanel.ts index ea2ca0e66..a1021d210 100644 --- a/src/components/SecurityAdvisoriesPanel.ts +++ b/src/components/SecurityAdvisoriesPanel.ts @@ -3,7 +3,7 @@ import { escapeHtml } from '@/utils/sanitize'; import { t } from '@/services/i18n'; import type { SecurityAdvisory } from '@/services/security-advisories'; -type AdvisoryFilter = 'all' | 'critical' | 'US' | 'AU' | 'UK' | 'NZ'; +type AdvisoryFilter = 'all' | 'critical' | 'US' | 'AU' | 'UK' | 'NZ' | 'health'; export class SecurityAdvisoriesPanel extends Panel { private advisories: SecurityAdvisory[] = []; @@ -52,6 +52,8 @@ export class SecurityAdvisoriesPanel extends Panel { switch (this.activeFilter) { case 'critical': return this.advisories.filter(a => a.level === 'do-not-travel' || a.level === 'reconsider'); + case 'health': + return this.advisories.filter(a => a.sourceCountry === 'EU' || a.sourceCountry === 'INT'); case 'US': case 'AU': case 'UK': @@ -88,6 +90,8 @@ export class SecurityAdvisoriesPanel extends Panel { case 'AU': return '\u{1F1E6}\u{1F1FA}'; case 'UK': return '\u{1F1EC}\u{1F1E7}'; case 'NZ': return '\u{1F1F3}\u{1F1FF}'; + case 'EU': return '\u{1F1EA}\u{1F1FA}'; + case 'INT': return '\u{1F3E5}'; default: return '\u{1F310}'; } } @@ -143,6 +147,7 @@ export class SecurityAdvisoriesPanel extends Panel { + `; diff --git a/src/locales/ar.json b/src/locales/ar.json index 09b88dbce..19827081b 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -1291,7 +1291,8 @@ "loading": "جاري جلب التحذيرات الأمنية...", "noMatching": "لا توجد تحذيرات مطابقة لهذا الفلتر", "critical": "حرج", - "sources": "وزارة الخارجية الأمريكية، DFAT الأسترالية، FCDO البريطانية، MFAT النيوزيلندية", + "health": "صحة", + "sources": "وزارة الخارجية الأمريكية، DFAT الأسترالية، FCDO البريطانية، MFAT النيوزيلندية, CDC, ECDC, WHO, US Embassies", "refresh": "تحديث", "levels": { "doNotTravel": "لا تسافر", diff --git a/src/locales/de.json b/src/locales/de.json index f13ac23b8..fb8d2f1cb 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1291,7 +1291,8 @@ "loading": "Reisehinweise werden geladen...", "noMatching": "Keine Hinweise für diesen Filter", "critical": "Kritisch", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Gesundheit", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Aktualisieren", "levels": { "doNotTravel": "Nicht reisen", diff --git a/src/locales/el.json b/src/locales/el.json index e39903aac..c7f088bff 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -1318,7 +1318,8 @@ "loading": "Φόρτωση ταξιδιωτικών οδηγιών...", "noMatching": "Δεν βρέθηκαν οδηγίες για αυτό το φίλτρο", "critical": "Κρίσιμο", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Υγεία", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Ανανέωση", "levels": { "doNotTravel": "Μην ταξιδεύετε", diff --git a/src/locales/en.json b/src/locales/en.json index 168cbf86c..393095115 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1087,7 +1087,8 @@ "loading": "Fetching travel advisories...", "noMatching": "No advisories match this filter", "critical": "Critical", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Health", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Refresh", "levels": { "doNotTravel": "Do Not Travel", diff --git a/src/locales/es.json b/src/locales/es.json index 582cf09c2..e018a360b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1291,7 +1291,8 @@ "loading": "Cargando alertas de viaje...", "noMatching": "No hay alertas para este filtro", "critical": "Crítico", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Salud", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Actualizar", "levels": { "doNotTravel": "No viajar", diff --git a/src/locales/fr.json b/src/locales/fr.json index 9f992865c..969f6faa3 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1291,7 +1291,8 @@ "loading": "Chargement des avis de voyage...", "noMatching": "Aucun avis pour ce filtre", "critical": "Critique", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Santé", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Actualiser", "levels": { "doNotTravel": "Ne pas voyager", diff --git a/src/locales/it.json b/src/locales/it.json index 0ec0534f0..cb1dde5d9 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1291,7 +1291,8 @@ "loading": "Caricamento avvisi di viaggio...", "noMatching": "Nessun avviso per questo filtro", "critical": "Critico", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Salute", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Aggiorna", "levels": { "doNotTravel": "Non viaggiare", diff --git a/src/locales/ja.json b/src/locales/ja.json index 05cf2fa7d..d2fa82cc9 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1291,7 +1291,8 @@ "loading": "渡航情報を取得中...", "noMatching": "該当する情報なし", "critical": "重大", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "健康", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "更新", "levels": { "doNotTravel": "渡航中止", diff --git a/src/locales/nl.json b/src/locales/nl.json index b9706b460..2b5faf4ad 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1150,7 +1150,8 @@ "loading": "Reisadviezen laden...", "noMatching": "Geen adviezen voor dit filter", "critical": "Kritiek", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Gezondheid", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Vernieuwen", "levels": { "doNotTravel": "Niet reizen", diff --git a/src/locales/pl.json b/src/locales/pl.json index 5aaa23b6f..f78c4abce 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -1291,7 +1291,8 @@ "loading": "Ładowanie ostrzeżeń podróżnych...", "noMatching": "Brak ostrzeżeń dla tego filtra", "critical": "Krytyczny", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Zdrowie", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Odśwież", "levels": { "doNotTravel": "Nie podróżuj", diff --git a/src/locales/pt.json b/src/locales/pt.json index 4e1f41fbf..19eb876ed 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1150,7 +1150,8 @@ "loading": "Carregando alertas de viagem...", "noMatching": "Nenhum alerta para este filtro", "critical": "Crítico", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Saúde", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Atualizar", "levels": { "doNotTravel": "Não viajar", diff --git a/src/locales/ru.json b/src/locales/ru.json index d16c11335..58ba906ae 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1291,7 +1291,8 @@ "loading": "Загрузка предупреждений...", "noMatching": "Нет предупреждений для этого фильтра", "critical": "Критический", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Здоровье", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Обновить", "levels": { "doNotTravel": "Не путешествовать", diff --git a/src/locales/sv.json b/src/locales/sv.json index acff392df..90e6d49b6 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -1150,7 +1150,8 @@ "loading": "Hämtar resevarningar...", "noMatching": "Inga varningar för detta filter", "critical": "Kritisk", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Hälsa", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Uppdatera", "levels": { "doNotTravel": "Res inte", diff --git a/src/locales/th.json b/src/locales/th.json index 57ea78e1c..763c7992e 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1291,7 +1291,8 @@ "loading": "กำลังโหลดคำเตือนการเดินทาง...", "noMatching": "ไม่พบคำเตือนสำหรับตัวกรองนี้", "critical": "วิกฤต", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "สุขภาพ", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "รีเฟรช", "levels": { "doNotTravel": "ห้ามเดินทาง", diff --git a/src/locales/tr.json b/src/locales/tr.json index 7c3dd0e7d..e0e0b742a 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1291,7 +1291,8 @@ "loading": "Seyahat uyarıları yükleniyor...", "noMatching": "Bu filtre için uyarı yok", "critical": "Kritik", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Sağlık", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Yenile", "levels": { "doNotTravel": "Seyahat etmeyin", diff --git a/src/locales/vi.json b/src/locales/vi.json index d07a03d48..4d3e35acc 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1291,7 +1291,8 @@ "loading": "Đang tải cảnh báo du lịch...", "noMatching": "Không có cảnh báo cho bộ lọc này", "critical": "Nghiêm trọng", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "Sức khỏe", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "Làm mới", "levels": { "doNotTravel": "Không du lịch", diff --git a/src/locales/zh.json b/src/locales/zh.json index 2a4588430..a9f405cda 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1291,7 +1291,8 @@ "loading": "正在加载旅行警告...", "noMatching": "没有匹配的警告", "critical": "严重", - "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT", + "health": "健康", + "sources": "US State Dept, AU DFAT, UK FCDO, NZ MFAT, CDC, ECDC, WHO, US Embassies", "refresh": "刷新", "levels": { "doNotTravel": "禁止出行", diff --git a/src/services/security-advisories.ts b/src/services/security-advisories.ts index f6caacb50..6fb90bbdd 100644 --- a/src/services/security-advisories.ts +++ b/src/services/security-advisories.ts @@ -89,6 +89,29 @@ const ADVISORY_FEEDS: AdvisoryFeed[] = [ sourceCountry: 'UK', url: 'https://www.gov.uk/foreign-travel-advice.atom', }, + // US Embassy security alerts (per-country) + { name: 'US Embassy Thailand', sourceCountry: 'US', url: 'https://th.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy UAE', sourceCountry: 'US', url: 'https://ae.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Germany', sourceCountry: 'US', url: 'https://de.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Ukraine', sourceCountry: 'US', url: 'https://ua.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Mexico', sourceCountry: 'US', url: 'https://mx.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy India', sourceCountry: 'US', url: 'https://in.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Pakistan', sourceCountry: 'US', url: 'https://pk.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Colombia', sourceCountry: 'US', url: 'https://co.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Poland', sourceCountry: 'US', url: 'https://pl.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Bangladesh', sourceCountry: 'US', url: 'https://bd.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Italy', sourceCountry: 'US', url: 'https://it.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Dominican Republic', sourceCountry: 'US', url: 'https://do.usembassy.gov/category/alert/feed/' }, + { name: 'US Embassy Myanmar', sourceCountry: 'US', url: 'https://mm.usembassy.gov/category/alert/feed/' }, + // Health advisories + { name: 'CDC Travel Notices', sourceCountry: 'US', url: 'https://wwwnc.cdc.gov/travel/rss/notices.xml' }, + { name: 'ECDC Epidemiological Updates', sourceCountry: 'EU', url: 'https://www.ecdc.europa.eu/en/taxonomy/term/1310/feed' }, + { name: 'ECDC Threats Report', sourceCountry: 'EU', url: 'https://www.ecdc.europa.eu/en/taxonomy/term/1505/feed' }, + { name: 'ECDC Risk Assessments', sourceCountry: 'EU', url: 'https://www.ecdc.europa.eu/en/taxonomy/term/1295/feed' }, + { name: 'ECDC Avian Influenza', sourceCountry: 'EU', url: 'https://www.ecdc.europa.eu/en/taxonomy/term/323//feed' }, + { name: 'ECDC Publications', sourceCountry: 'EU', url: 'https://www.ecdc.europa.eu/en/taxonomy/term/1244/feed' }, + { name: 'WHO News', sourceCountry: 'INT', url: 'https://www.who.int/rss-feeds/news-english.xml' }, + { name: 'WHO Africa Emergencies', sourceCountry: 'INT', url: 'https://www.afro.who.int/rss/emergencies.xml' }, ]; const CACHE_KEY = 'security-advisories';