File size: 7,387 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import { Panel } from './Panel';
import { getRpcBaseUrl } from '@/services/rpc-client';
import { getHydratedData } from '@/services/bootstrap';

import type { GetEnergyCrisisPoliciesResponse, EnergyCrisisPolicy } from '@/generated/client/worldmonitor/economic/v1/service_client';
import { escapeHtml, unsafeRawHtml } from '@/utils/sanitize';
import { EconomicServiceClient } from '@/services/generated-rpc-clients';

type PolicyData = GetEnergyCrisisPoliciesResponse;

const CATEGORY_LABELS: Record<string, string> = {
  conservation: 'Energy Conservation',
  consumer_support: 'Consumer Support',
};

const SECTOR_LABELS: Record<string, string> = {
  transport: 'Transport',
  buildings: 'Buildings',
  industry: 'Industry',
  electricity: 'Electricity',
  agriculture: 'Agriculture',
  general: 'General',
};

const STATUS_CLASS: Record<string, string> = {
  active: 'ecp-status-active',
  planned: 'ecp-status-planned',
  ended: 'ecp-status-ended',
};

export class EnergyCrisisPanel extends Panel {
  private data: PolicyData | null = null;
  private loading = true;
  private error: string | null = null;
  private activeFilter: string = 'all';

  constructor() {
    super({
      id: 'energy-crisis',
      title: 'Energy Crisis Tracker',
      showCount: true,
      trackActivity: true,
      defaultRowSpan: 2,
      infoTooltip: 'IEA 2026 Energy Crisis Policy Response Tracker. Tracks government measures to conserve energy and support consumers in response to Middle East conflict and Strait of Hormuz supply disruptions.',
    });
    this.showLoading('Loading energy crisis policies...');
  }

  public async fetchData(): Promise<void> {
    const hydrated = getHydratedData('energyCrisisPolicies') as PolicyData | undefined;
    if (hydrated?.policies?.length) {
      this.data = hydrated;
      this.error = null;
      this.loading = false;
      this.setCount(hydrated.policies.length);
      this.render();
      void this.refreshFromRpc();
      return;
    }
    await this.refreshFromRpc();
  }

  private async refreshFromRpc(): Promise<void> {
    try {
      const client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
      const fresh = await client.getEnergyCrisisPolicies({ countryCode: '', category: '' });
      if (!this.element?.isConnected) return;
      if (fresh.policies?.length || !this.data) {
        this.data = fresh;
        this.error = null;
        this.loading = false;
        this.setCount(fresh.policies.length);
        this.render();
      }
    } catch (err) {
      if (this.isAbortError(err)) return;
      if (!this.element?.isConnected) return;
      if (!this.data) {
        console.warn('[EnergyCrisis] Fetch error:', err);
        this.error = 'Energy crisis data unavailable';
        this.loading = false;
        this.render();
      }
    }
  }

  private getFilteredPolicies(): EnergyCrisisPolicy[] {
    if (!this.data?.policies) return [];
    if (this.activeFilter === 'all') return this.data.policies;
    return this.data.policies.filter(p => p.category === this.activeFilter);
  }

  private buildSummary(): { conservationCount: number; supportCount: number; countryCount: number } {
    const policies = this.data?.policies ?? [];
    const conservationCount = policies.filter(p => p.category === 'conservation').length;
    const supportCount = policies.filter(p => p.category === 'consumer_support').length;
    const countryCount = new Set(policies.map(p => p.countryCode)).size;
    return { conservationCount, supportCount, countryCount };
  }

  private render(): void {
    if (this.loading) {
      this.showLoading('Loading energy crisis policies...');
      return;
    }

    if (this.error || !this.data) {
      this.showError(this.error || 'No data available', () => void this.fetchData());
      return;
    }

    if (!this.data.policies?.length) {
      this.setSafeContent(unsafeRawHtml('<div class="panel-empty">No energy crisis policies tracked.</div>', 'legacy Panel.setContent() migration'));
      return;
    }

    const summary = this.buildSummary();
    const filtered = this.getFilteredPolicies();

    const summaryHtml = `
      <div class="ecp-summary">
        <div class="ecp-summary-card">
          <span class="ecp-summary-value">${summary.countryCount}</span>
          <span class="ecp-summary-label">Countries</span>
        </div>
        <div class="ecp-summary-card ecp-summary-conservation">
          <span class="ecp-summary-value">${summary.conservationCount}</span>
          <span class="ecp-summary-label">Conservation</span>
        </div>
        <div class="ecp-summary-card ecp-summary-support">
          <span class="ecp-summary-value">${summary.supportCount}</span>
          <span class="ecp-summary-label">Consumer Support</span>
        </div>
      </div>
    `;

    const filterHtml = `
      <div class="ecp-filters">
        <button class="ecp-filter-btn ${this.activeFilter === 'all' ? 'ecp-filter-active' : ''}" data-filter="all">All</button>
        <button class="ecp-filter-btn ${this.activeFilter === 'conservation' ? 'ecp-filter-active' : ''}" data-filter="conservation">Conservation</button>
        <button class="ecp-filter-btn ${this.activeFilter === 'consumer_support' ? 'ecp-filter-active' : ''}" data-filter="consumer_support">Consumer Support</button>
      </div>
    `;

    const policyRows = filtered.map(p => {
      const categoryLabel = CATEGORY_LABELS[p.category] || p.category;
      const sectorLabel = SECTOR_LABELS[p.sector] || p.sector;
      const statusClass = STATUS_CLASS[p.status] || '';
      const categoryClass = p.category === 'conservation' ? 'ecp-cat-conservation' : 'ecp-cat-support';

      return `
        <div class="ecp-policy-row">
          <div class="ecp-policy-header">
            <span class="ecp-country">${escapeHtml(p.country)}</span>
            <span class="ecp-pill ${categoryClass}">${escapeHtml(categoryLabel)}</span>
            <span class="ecp-pill ecp-pill-sector">${escapeHtml(sectorLabel)}</span>
            <span class="ecp-pill ${statusClass}">${escapeHtml(p.status)}</span>
          </div>
          <div class="ecp-measure">${escapeHtml(p.measure)}</div>
          <div class="ecp-date">${escapeHtml(p.dateAnnounced)}</div>
        </div>
      `;
    }).join('');

    const sourceUrl = this.data.sourceUrl || 'https://www.iea.org/data-and-statistics/data-tools/2026-energy-crisis-policy-response-tracker';
    const footer = [
      this.data.updatedAt ? `Updated ${new Date(this.data.updatedAt).toLocaleDateString()}` : '',
      'Source: IEA',
    ].filter(Boolean).join(' · ');

    this.setSafeContent(unsafeRawHtml(`
      <div class="ecp-container">
        ${summaryHtml}
        ${filterHtml}
        <div class="ecp-policy-list">${policyRows}</div>
        <div class="ecp-footer">
          <span>${escapeHtml(footer)}</span>
          <a href="${escapeHtml(sourceUrl)}" target="_blank" rel="noopener noreferrer" class="ecp-source-link">IEA Tracker ↗</a>
        </div>
      </div>
    `, 'legacy Panel.setContent() migration'));

    this.content?.querySelectorAll('.ecp-filter-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const filter = (e.currentTarget as HTMLElement).dataset.filter || 'all';
        this.activeFilter = filter;
        this.render();
      });
    });
  }
}