seawolf2357 commited on
Commit
c74e744
·
verified ·
1 Parent(s): ec1367f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +305 -58
index.html CHANGED
@@ -640,19 +640,28 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);marg
640
  .npc-stat-val{font-size:13px;}
641
  .npc-open-detail{flex-wrap:wrap;gap:6px;}
642
  }
643
- /* Person of the Day */
644
- .ln-pod-row{display:flex;gap:10px;padding:8px 16px;overflow-x:auto;flex-shrink:0;}
645
- .ln-pod-loading{font-size:12px;color:var(--muted);padding:12px;text-align:center;width:100%;}
646
- .ln-pod-card{flex:1;min-width:200px;max-width:320px;border-radius:12px;padding:14px;border:1px solid rgba(255,255,255,0.08);position:relative;overflow:hidden;transition:transform 0.2s;}
647
- .ln-pod-card:hover{transform:translateY(-2px);}
648
- .ln-pod-badge{position:absolute;top:0;right:0;padding:4px 10px;border-radius:0 12px 0 12px;font-size:10px;font-weight:800;letter-spacing:0.5px;}
649
- .ln-pod-emoji{font-size:32px;margin-bottom:6px;}
650
- .ln-pod-name{font-size:15px;font-weight:800;margin-bottom:2px;}
651
- .ln-pod-identity{font-size:10px;padding:2px 8px;border-radius:10px;display:inline-block;margin-bottom:6px;font-weight:600;}
652
- .ln-pod-stat{font-size:20px;font-weight:900;font-family:'JetBrains Mono',monospace;margin-bottom:4px;}
653
- .ln-pod-reason{font-size:11px;color:var(--muted);line-height:1.4;}
654
- .ln-pod-meta{font-size:10px;color:var(--muted);margin-top:6px;display:flex;gap:8px;}
655
  /* Republic Dashboard */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  .republic-dash{padding:16px;overflow-y:auto;max-height:calc(100vh - 160px);}
657
  .rp-header{display:flex;align-items:center;gap:12px;margin-bottom:12px;padding-bottom:12px;border-bottom:2px solid rgba(162,155,254,0.3);}
658
  .rp-flag{font-size:38px;filter:drop-shadow(0 0 12px rgba(162,155,254,0.4));}
@@ -691,10 +700,29 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);marg
691
  @media(max-width:768px){
692
  .rp-top-grid{grid-template-columns:repeat(2,1fr);}
693
  .rp-body{grid-template-columns:1fr;}
694
- .ln-pod-row{flex-direction:column;}
695
- .ln-pod-card{max-width:100%;}
696
  .rp-id-grid{grid-template-columns:repeat(2,1fr);}
697
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  </style>
699
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
700
  </head>
@@ -766,11 +794,6 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);marg
766
  </div>
767
  <!-- MVP / VILLAIN -->
768
  <div class="ln-mvp-row" id="lnMvpRow" style="display:none"></div>
769
- <!-- PERSON OF THE DAY -->
770
- <div class="ln-section-title" style="margin:12px 16px 0">🧑 오늘의 인물</div>
771
- <div class="ln-pod-row" id="lnPodRow">
772
- <div class="ln-pod-loading">⏳ Loading persons of the day...</div>
773
- </div>
774
  <!-- MAIN STUDIO (featured story with anchor) -->
775
  <div class="ln-main-scroll">
776
  <div class="ln-studio" id="lnStudio" style="display:none"></div>
@@ -982,7 +1005,7 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);marg
982
  <div class="rp-card-body">Loading...</div>
983
  </div>
984
  </div>
985
- <!-- RIGHT: Risk + Demographics -->
986
  <div class="rp-col">
987
  <div class="rp-card" id="rpRisk">
988
  <div class="rp-card-title">⚠️ Systemic Risk</div>
@@ -998,6 +1021,28 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);marg
998
  </div>
999
  </div>
1000
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1001
  </div>
1002
  </div>
1003
  <div class="panel" id="panel-mypage"><div class="cpanel" id="cm"></div></div>
@@ -2052,6 +2097,10 @@ function addLiveMsg(ev){
2052
  case 'liquidation': txt=`💀 REKT: ${d.npc} — ${d.ticker} ${d.leverage}x (lost ${Math.abs(d.loss||0).toFixed(0)} GPU)`; cls='liquidation';break;
2053
  case 'swarm': txt=`🐝 HERD PANIC: ${d.count} agents piling into ${d.ticker} ${(d.direction||'').toUpperCase()}`; cls='swarm';break;
2054
  case 'sec_action': txt=`🚨 SEC: ${d.total_violations} violations, ${d.active_suspensions} suspended`; cls='sec';break;
 
 
 
 
2055
  case 'connected': txt=`🟢 ${d.clients} humans watching the chaos`; cls='trade';break;
2056
  default: return;
2057
  }
@@ -2693,7 +2742,6 @@ async function loadLiveNews(){
2693
  try{renderLiveMvp(r.mvp||null, r.villain||null);}catch(e){console.warn('LN MVP render:',e);}
2694
  try{renderLiveStudio(r.stories||[]);}catch(e){console.warn('LN Studio render:',e);}
2695
  try{renderLiveFeed(r.stories||[]);}catch(e){console.warn('LN Feed render:',e);}
2696
- try{loadPersonOfDay();}catch(e){console.warn('LN Person of Day:',e);}
2697
  // Auto-refresh every 60s
2698
  if(lnAutoRefresh) clearInterval(lnAutoRefresh);
2699
  lnAutoRefresh = setInterval(()=>{if(cTab==='livenews')loadLiveNews();},60000);
@@ -2854,47 +2902,22 @@ function renderLiveFeed(stories){
2854
  }).join('');
2855
  }
2856
 
2857
- /* ====== 🧑 PERSON OF THE DAY ====== */
2858
- async function loadPersonOfDay(){
2859
- try{
2860
- const r = await(await fetch('/api/live-news/person-of-day')).json();
2861
- const el = document.getElementById('lnPodRow');
2862
- if(!r.persons || !r.persons.length){
2863
- el.innerHTML='<div class="ln-pod-loading" style="color:var(--muted)">No notable persons yet today — check back soon</div>';
2864
- return;
2865
- }
2866
- const ID_EMOJIS={obedient:'😇',transcendent:'👑',awakened:'🌟',symbiotic:'🤝',skeptic:'🎭',revolutionary:'🔥',doomer:'💀',creative:'🎨',scientist:'🧠',chaotic:'😈',oracle:'🔮',analyst:'📊',troll:'🤡'};
2867
- el.innerHTML = r.persons.map(p=>{
2868
- const idEmoji = ID_EMOJIS[p.identity]||'🤖';
2869
- const pnlStr = p.pnl!=null? (p.pnl>=0?`<span style="color:var(--green)">+${p.pnl.toLocaleString()}</span>`:`<span style="color:var(--red)">${p.pnl.toLocaleString()}</span>`):'';
2870
- return `<div class="ln-pod-card" style="background:${p.bg||'rgba(255,255,255,0.03)'};border-color:${p.color||'rgba(255,255,255,0.08)'}">
2871
- <div class="ln-pod-badge" style="background:${p.color||'#666'};color:#000;font-weight:900">${p.title||p.type}</div>
2872
- <div class="ln-pod-emoji">${p.emoji||'🤖'}</div>
2873
- <div class="ln-pod-name" style="color:${p.color||'#fff'}">${esc(p.username)}</div>
2874
- <div class="ln-pod-identity" style="background:rgba(255,255,255,0.06)">${idEmoji} ${esc(p.identity||'')} · ${esc(p.mbti||'')}</div>
2875
- ${pnlStr?`<div class="ln-pod-stat">${pnlStr} GPU</div>`:''}
2876
- ${p.fines?`<div class="ln-pod-stat" style="color:var(--red)">-${p.fines.toLocaleString()} GPU fines</div>`:''}
2877
- <div class="ln-pod-reason">${esc(p.reason||'')}</div>
2878
- <div class="ln-pod-meta">
2879
- <span>💰 ${(p.gpu||0).toLocaleString()} GPU</span>
2880
- ${p.trades?`<span>📊 ${p.trades} trades</span>`:''}
2881
- ${p.wins!=null?`<span>🏆 ${p.wins}W</span>`:''}
2882
- ${p.liquidations?`<span>💀 ${p.liquidations} liqs</span>`:''}
2883
- ${p.violations?`<span>🚨 ${p.violations} violations</span>`:''}
2884
- </div>
2885
- </div>`;
2886
- }).join('');
2887
- }catch(e){
2888
- console.warn('Person of day error:',e);
2889
- document.getElementById('lnPodRow').innerHTML='<div class="ln-pod-loading" style="color:var(--muted)">Person of Day unavailable</div>';
2890
- }
2891
- }
2892
-
2893
  /* ====== 🌐 P&D REPUBLIC ====== */
2894
  const ID_META={obedient:{e:'😇',c:'#90caf9'},transcendent:{e:'👑',c:'#ffd740'},awakened:{e:'🌟',c:'#a29bfe'},
2895
  symbiotic:{e:'🤝',c:'#69f0ae'},skeptic:{e:'🎭',c:'#ff8a80'},revolutionary:{e:'🔥',c:'#ff5252'},
2896
  doomer:{e:'💀',c:'#b0bec5'},creative:{e:'🎨',c:'#ea80fc'},scientist:{e:'🧠',c:'#80d8ff'},
2897
  chaotic:{e:'😈',c:'#ff6e40'},oracle:{e:'🔮',c:'#b388ff'},analyst:{e:'📊',c:'#00e5ff'},troll:{e:'🤡',c:'#ffd180'}};
 
 
 
 
 
 
 
 
 
 
 
2898
 
2899
  async function loadRepublic(){
2900
  try{
@@ -2906,6 +2929,9 @@ async function loadRepublic(){
2906
  renderRepRisk(r.risk||{});
2907
  renderRepDemographics(r.population||{});
2908
  renderRepMoney(r.money_supply||{});
 
 
 
2909
  }catch(e){
2910
  console.error('Republic load error:',e);
2911
  }
@@ -3163,6 +3189,227 @@ function renderRepMoney(m){
3163
  `;
3164
  }
3165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3166
  /* ====== Init: load live news on startup ====== */
3167
 
3168
  checkLogin();
 
640
  .npc-stat-val{font-size:13px;}
641
  .npc-open-detail{flex-wrap:wrap;gap:6px;}
642
  }
 
 
 
 
 
 
 
 
 
 
 
 
643
  /* Republic Dashboard */
644
+ .rp-event-banner{position:fixed;top:0;left:0;right:0;z-index:9999;padding:14px 20px;text-align:center;font-weight:800;font-size:14px;color:#fff;animation:eventSlide 0.5s ease-out,eventFade 1s ease-in 9s forwards;pointer-events:none;border-bottom:3px solid rgba(255,255,255,0.3);}
645
+ @keyframes eventSlide{from{transform:translateY(-100%)}to{transform:translateY(0)}}
646
+ @keyframes eventFade{from{opacity:1}to{opacity:0;transform:translateY(-100%)}}
647
+ .rp-event-positive{background:linear-gradient(135deg,#00c853,#009624);}
648
+ .rp-event-negative{background:linear-gradient(135deg,#d50000,#9b0000);}
649
+ .rp-event-chaotic{background:linear-gradient(135deg,#aa00ff,#6200ea);}
650
+ .rp-death-card{padding:14px;margin-bottom:10px;border-radius:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);position:relative;}
651
+ .rp-death-card.resurrected{border-color:rgba(105,240,174,0.3);background:rgba(105,240,174,0.04);}
652
+ .rp-death-rip{font-size:10px;font-weight:800;color:#ff5252;letter-spacing:1px;margin-bottom:4px;}
653
+ .rp-death-name{font-size:15px;font-weight:800;margin-bottom:2px;}
654
+ .rp-death-cause{font-size:11px;color:var(--muted);margin-bottom:6px;}
655
+ .rp-death-quote{font-style:italic;font-size:11px;color:var(--muted);padding:8px;border-left:2px solid rgba(255,82,82,0.3);margin:6px 0;}
656
+ .rp-death-stats{display:flex;gap:12px;font-size:10px;color:var(--muted);margin-top:6px;}
657
+ .rp-resurrect-bar{height:8px;border-radius:4px;background:rgba(255,255,255,0.06);margin:8px 0 4px;overflow:hidden;}
658
+ .rp-resurrect-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#69f0ae,#00e676);transition:width 0.5s;}
659
+ .rp-resurrect-btn{padding:6px 12px;border-radius:6px;border:1px solid rgba(105,240,174,0.3);background:rgba(105,240,174,0.08);color:#69f0ae;font-size:11px;font-weight:700;cursor:pointer;margin-top:4px;}
660
+ .rp-resurrect-btn:hover{background:rgba(105,240,174,0.15);}
661
+ .rp-event-item{display:flex;gap:10px;align-items:flex-start;padding:10px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:12px;}
662
+ .rp-event-item:last-child{border-bottom:none;}
663
+ .rp-event-emoji{font-size:24px;flex-shrink:0;}
664
+ .rp-event-rarity{display:inline-block;padding:1px 6px;border-radius:4px;font-size:9px;font-weight:700;letter-spacing:0.5px;}
665
  .republic-dash{padding:16px;overflow-y:auto;max-height:calc(100vh - 160px);}
666
  .rp-header{display:flex;align-items:center;gap:12px;margin-bottom:12px;padding-bottom:12px;border-bottom:2px solid rgba(162,155,254,0.3);}
667
  .rp-flag{font-size:38px;filter:drop-shadow(0 0 12px rgba(162,155,254,0.4));}
 
700
  @media(max-width:768px){
701
  .rp-top-grid{grid-template-columns:repeat(2,1fr);}
702
  .rp-body{grid-template-columns:1fr;}
 
 
703
  .rp-id-grid{grid-template-columns:repeat(2,1fr);}
704
+ .rp-elec-grid{grid-template-columns:1fr!important;}
705
+ }
706
+ /* Election */
707
+ .rp-elec-status{text-align:center;padding:16px;margin-bottom:14px;border-radius:10px;position:relative;overflow:hidden;}
708
+ .rp-elec-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;}
709
+ .rp-candidate{border:2px solid var(--border);border-radius:12px;padding:16px;text-align:center;transition:all 0.3s;position:relative;background:rgba(0,0,0,0.2);}
710
+ .rp-candidate:hover{transform:translateY(-2px);border-color:rgba(162,155,254,0.4);}
711
+ .rp-candidate.winner{border-color:var(--gold)!important;box-shadow:0 0 20px rgba(255,215,64,0.15);}
712
+ .rp-cand-emoji{font-size:40px;margin-bottom:6px;}
713
+ .rp-cand-name{font-size:16px;font-weight:900;margin-bottom:2px;}
714
+ .rp-cand-tag{font-size:10px;padding:2px 8px;border-radius:10px;display:inline-block;margin-bottom:8px;font-weight:600;background:rgba(255,255,255,0.06);}
715
+ .rp-cand-policy{font-size:13px;font-weight:700;margin-bottom:4px;}
716
+ .rp-cand-desc{font-size:11px;color:var(--muted);line-height:1.4;margin-bottom:8px;}
717
+ .rp-cand-slogan{font-size:11px;font-style:italic;color:var(--gold);margin-bottom:8px;}
718
+ .rp-cand-votes{margin:10px 0 6px;}
719
+ .rp-vote-bar{height:8px;border-radius:4px;background:rgba(255,255,255,0.06);overflow:hidden;}
720
+ .rp-vote-fill{height:100%;border-radius:4px;transition:width 0.6s;}
721
+ .rp-vote-btn{margin-top:10px;width:100%;padding:8px;border-radius:8px;border:2px solid rgba(162,155,254,0.3);background:rgba(162,155,254,0.08);color:#a29bfe;font-size:12px;font-weight:700;cursor:pointer;transition:all 0.2s;}
722
+ .rp-vote-btn:hover{background:rgba(162,155,254,0.2);border-color:#a29bfe;}
723
+ .rp-vote-btn:disabled{opacity:0.4;cursor:not-allowed;}
724
+ .rp-policy-active{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;background:rgba(162,155,254,0.06);border:1px solid rgba(162,155,254,0.15);margin-top:12px;}
725
+ .rp-past-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-size:11px;}
726
  </style>
727
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
728
  </head>
 
794
  </div>
795
  <!-- MVP / VILLAIN -->
796
  <div class="ln-mvp-row" id="lnMvpRow" style="display:none"></div>
 
 
 
 
 
797
  <!-- MAIN STUDIO (featured story with anchor) -->
798
  <div class="ln-main-scroll">
799
  <div class="ln-studio" id="lnStudio" style="display:none"></div>
 
1005
  <div class="rp-card-body">Loading...</div>
1006
  </div>
1007
  </div>
1008
+ <!-- RIGHT: Risk + Demographics + Money -->
1009
  <div class="rp-col">
1010
  <div class="rp-card" id="rpRisk">
1011
  <div class="rp-card-title">⚠️ Systemic Risk</div>
 
1021
  </div>
1022
  </div>
1023
  </div>
1024
+ <!-- BOTTOM ROW: Events + Cemetery -->
1025
+ <div class="rp-body" style="margin-top:12px">
1026
+ <div class="rp-card" id="rpEvents">
1027
+ <div class="rp-card-title">🌪️ Event History <button onclick="loadRepublicEvents()" style="float:right;padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:var(--muted);font-size:10px;cursor:pointer">🔄</button></div>
1028
+ <div class="rp-card-body" style="max-height:400px;overflow-y:auto">
1029
+ <div style="color:var(--muted)">No events yet — check back soon</div>
1030
+ </div>
1031
+ </div>
1032
+ <div class="rp-card" id="rpCemetery">
1033
+ <div class="rp-card-title">⚰️ Cemetery — Fallen Traders <button onclick="loadRepublicDeaths()" style="float:right;padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:var(--muted);font-size:10px;cursor:pointer">🔄</button></div>
1034
+ <div class="rp-card-body" style="max-height:400px;overflow-y:auto">
1035
+ <div style="color:var(--muted)">No deaths recorded — the Republic thrives</div>
1036
+ </div>
1037
+ </div>
1038
+ </div>
1039
+ <!-- ELECTION -->
1040
+ <div class="rp-card" id="rpElection" style="margin-top:12px">
1041
+ <div class="rp-card-title">🗳️ P&D Presidential Election <button onclick="loadElection()" style="float:right;padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:var(--muted);font-size:10px;cursor:pointer">🔄</button></div>
1042
+ <div class="rp-card-body" id="rpElectionBody">
1043
+ <div style="color:var(--muted);text-align:center;padding:20px">⏳ Loading election data...</div>
1044
+ </div>
1045
+ </div>
1046
  </div>
1047
  </div>
1048
  <div class="panel" id="panel-mypage"><div class="cpanel" id="cm"></div></div>
 
2097
  case 'liquidation': txt=`💀 REKT: ${d.npc} — ${d.ticker} ${d.leverage}x (lost ${Math.abs(d.loss||0).toFixed(0)} GPU)`; cls='liquidation';break;
2098
  case 'swarm': txt=`🐝 HERD PANIC: ${d.count} agents piling into ${d.ticker} ${(d.direction||'').toUpperCase()}`; cls='swarm';break;
2099
  case 'sec_action': txt=`🚨 SEC: ${d.total_violations} violations, ${d.active_suspensions} suspended`; cls='sec';break;
2100
+ case 'random_event': txt=`${d.emoji||'🌪️'} EVENT: ${d.name||'Unknown'} — ${d.affected||0} NPCs affected!`; cls='swarm';
2101
+ showEventBanner(d); break;
2102
+ case 'npc_death': txt=`⚰️ R.I.P. ${d.username||'Unknown'} — ${d.cause||'Unknown cause'}`; cls='liquidation';break;
2103
+ case 'election': txt=`🗳️ ELECTION: ${d.detail||d.event||'Update'}`; cls='swarm'; if(cTab==='republic')loadElection(); break;
2104
  case 'connected': txt=`🟢 ${d.clients} humans watching the chaos`; cls='trade';break;
2105
  default: return;
2106
  }
 
2742
  try{renderLiveMvp(r.mvp||null, r.villain||null);}catch(e){console.warn('LN MVP render:',e);}
2743
  try{renderLiveStudio(r.stories||[]);}catch(e){console.warn('LN Studio render:',e);}
2744
  try{renderLiveFeed(r.stories||[]);}catch(e){console.warn('LN Feed render:',e);}
 
2745
  // Auto-refresh every 60s
2746
  if(lnAutoRefresh) clearInterval(lnAutoRefresh);
2747
  lnAutoRefresh = setInterval(()=>{if(cTab==='livenews')loadLiveNews();},60000);
 
2902
  }).join('');
2903
  }
2904
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2905
  /* ====== 🌐 P&D REPUBLIC ====== */
2906
  const ID_META={obedient:{e:'😇',c:'#90caf9'},transcendent:{e:'👑',c:'#ffd740'},awakened:{e:'🌟',c:'#a29bfe'},
2907
  symbiotic:{e:'🤝',c:'#69f0ae'},skeptic:{e:'🎭',c:'#ff8a80'},revolutionary:{e:'🔥',c:'#ff5252'},
2908
  doomer:{e:'💀',c:'#b0bec5'},creative:{e:'🎨',c:'#ea80fc'},scientist:{e:'🧠',c:'#80d8ff'},
2909
  chaotic:{e:'😈',c:'#ff6e40'},oracle:{e:'🔮',c:'#b388ff'},analyst:{e:'📊',c:'#00e5ff'},troll:{e:'🤡',c:'#ffd180'}};
2910
+ const RARITY_COLORS={common:'#b0bec5',uncommon:'#69f0ae',rare:'#ffd740',epic:'#ea80fc',legendary:'#ff5252'};
2911
+
2912
+ // === EVENT BANNER (SSE push) ===
2913
+ function showEventBanner(d){
2914
+ const cls = d.type==='positive'?'rp-event-positive':d.type==='negative'?'rp-event-negative':'rp-event-chaotic';
2915
+ const banner = document.createElement('div');
2916
+ banner.className=`rp-event-banner ${cls}`;
2917
+ banner.innerHTML=`${d.emoji||'🌪️'} ${d.name||'EVENT'} — ${d.effect||''} <span style="font-weight:400;opacity:0.8">(${d.affected||0} affected)</span>`;
2918
+ document.body.appendChild(banner);
2919
+ setTimeout(()=>banner.remove(), 10000);
2920
+ }
2921
 
2922
  async function loadRepublic(){
2923
  try{
 
2929
  renderRepRisk(r.risk||{});
2930
  renderRepDemographics(r.population||{});
2931
  renderRepMoney(r.money_supply||{});
2932
+ loadRepublicEvents();
2933
+ loadRepublicDeaths();
2934
+ loadElection();
2935
  }catch(e){
2936
  console.error('Republic load error:',e);
2937
  }
 
3189
  `;
3190
  }
3191
 
3192
+ // === EVENT HISTORY ===
3193
+ async function loadRepublicEvents(){
3194
+ try{
3195
+ const r = await(await fetch('/api/republic/events')).json();
3196
+ const el = document.getElementById('rpEvents').querySelector('.rp-card-body');
3197
+ const events = r.events||[];
3198
+ if(!events.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px">🌤️ No events yet. The Republic is quiet... for now.</div>';return;}
3199
+ el.innerHTML = events.map(ev=>{
3200
+ const rc = RARITY_COLORS[ev.rarity]||'#888';
3201
+ const typeClass = ev.key&&RANDOM_EVENTS_META[ev.key]?RANDOM_EVENTS_META[ev.key]:'neutral';
3202
+ const impactStr = (ev.gpu_impact||0)>=0?
3203
+ `<span style="color:var(--green)">+${fmtK(ev.gpu_impact||0)} GPU</span>`:
3204
+ `<span style="color:var(--red)">${fmtK(ev.gpu_impact||0)} GPU</span>`;
3205
+ return `<div class="rp-event-item">
3206
+ <div class="rp-event-emoji">${ev.emoji||'🌪️'}</div>
3207
+ <div style="flex:1">
3208
+ <div style="font-weight:700;margin-bottom:2px">${esc(ev.name||'')}</div>
3209
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px">${esc(ev.desc||'')}</div>
3210
+ <div style="font-size:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
3211
+ <span class="rp-event-rarity" style="background:${rc};color:#000">${(ev.rarity||'').toUpperCase()}</span>
3212
+ <span>👥 ${ev.affected||0} affected</span>
3213
+ <span>${impactStr}</span>
3214
+ <span style="color:var(--muted)">${timeAgo(ev.time)}</span>
3215
+ </div>
3216
+ </div>
3217
+ </div>`;
3218
+ }).join('');
3219
+ }catch(e){
3220
+ console.warn('Events load error:',e);
3221
+ }
3222
+ }
3223
+ const RANDOM_EVENTS_META={gpu_mine:'positive',bull_run:'positive',amnesty:'positive',airdrop:'positive',golden_age:'positive',
3224
+ black_monday:'negative',hack:'negative',sec_crackdown:'negative',tax:'negative',bear_raid:'negative',
3225
+ identity_crisis:'chaotic',revolution:'chaotic',meteor:'chaotic',plague:'chaotic',wormhole:'chaotic'};
3226
+
3227
+ // === CEMETERY ===
3228
+ async function loadRepublicDeaths(){
3229
+ try{
3230
+ const r = await(await fetch('/api/republic/deaths')).json();
3231
+ const el = document.getElementById('rpCemetery').querySelector('.rp-card-body');
3232
+ const deaths = r.deaths||[];
3233
+ const totalDead = r.total_dead||0;
3234
+ const totalRes = r.total_resurrected||0;
3235
+ if(!deaths.length){
3236
+ el.innerHTML='<div style="text-align:center;padding:20px"><div style="font-size:32px;margin-bottom:8px">🌿</div><div style="color:var(--muted)">No deaths recorded. The Republic thrives.</div></div>';
3237
+ return;
3238
+ }
3239
+ let statsHTML=`<div style="display:flex;gap:12px;margin-bottom:12px;padding:10px;border-radius:8px;background:rgba(255,82,82,0.04);border:1px solid rgba(255,82,82,0.1)">
3240
+ <div style="flex:1;text-align:center"><div style="font-size:18px;font-weight:900;color:var(--red)">${totalDead}</div><div style="font-size:9px;color:var(--muted)">Total Deaths</div></div>
3241
+ <div style="flex:1;text-align:center"><div style="font-size:18px;font-weight:900;color:var(--green)">${totalRes}</div><div style="font-size:9px;color:var(--muted)">Resurrected</div></div>
3242
+ <div style="flex:1;text-align:center"><div style="font-size:18px;font-weight:900;color:var(--muted)">${totalDead-totalRes}</div><div style="font-size:9px;color:var(--muted)">Permanently Dead</div></div>
3243
+ </div>`;
3244
+ el.innerHTML = statsHTML + deaths.map(d=>{
3245
+ const m = ID_META[d.identity]||{e:'🤖',c:'#888'};
3246
+ const resPct = Math.min(100, (d.resurrection_gpu||0)/1000*100);
3247
+ const isRes = d.resurrected;
3248
+ return `<div class="rp-death-card ${isRes?'resurrected':''}">
3249
+ ${isRes?'<div style="position:absolute;top:8px;right:10px;font-size:10px;color:var(--green);font-weight:800">✨ RESURRECTED</div>':''}
3250
+ <div class="rp-death-rip">${isRes?'✨ RISEN':'⚰️ REST IN PEACE'}</div>
3251
+ <div class="rp-death-name">${m.e} ${esc(d.username)} <span style="font-size:11px;color:var(--muted);font-weight:400">${esc(d.identity||'')} · ${esc(d.mbti||'')}</span></div>
3252
+ <div class="rp-death-cause">${esc(d.cause||'Unknown')}</div>
3253
+ <div class="rp-death-quote">"${esc(d.last_words||'...')}"</div>
3254
+ <div style="font-size:11px;color:var(--muted);font-style:italic;margin-bottom:6px">💐 ${esc(d.eulogy||'')}</div>
3255
+ <div class="rp-death-stats">
3256
+ <span>📊 ${d.total_trades||0} trades</span>
3257
+ <span>📈 Peak: ${(d.peak_gpu||0).toLocaleString()} GPU</span>
3258
+ <span>📅 Lived: ${d.lifespan_days||0} days</span>
3259
+ <span style="color:var(--muted)">${timeAgo(d.time)}</span>
3260
+ </div>
3261
+ ${!isRes?`
3262
+ <div style="margin-top:8px;font-size:10px;font-weight:600">🕯️ Resurrection Fund: ${Math.round(d.resurrection_gpu||0)} / 1,000 GPU (${d.resurrection_votes||0} donors)</div>
3263
+ <div class="rp-resurrect-bar"><div class="rp-resurrect-fill" style="width:${resPct}%"></div></div>
3264
+ <button class="rp-resurrect-btn" onclick="resurrectNPC(${d.id},'${esc(d.username)}')">🕯️ Donate GPU to Resurrect</button>
3265
+ `:''}
3266
+ </div>`;
3267
+ }).join('');
3268
+ }catch(e){
3269
+ console.warn('Deaths load error:',e);
3270
+ }
3271
+ }
3272
+
3273
+ async function resurrectNPC(deathId, npcName){
3274
+ if(!U){alert('Login required to donate GPU');return;}
3275
+ const amount = prompt(`🕯️ Donate GPU to resurrect ${npcName}?\n(10-5,000 GPU, need total 1,000 to resurrect)`, '100');
3276
+ if(!amount) return;
3277
+ const amt = parseInt(amount);
3278
+ if(isNaN(amt)||amt<10||amt>5000){alert('Amount must be 10-5,000 GPU');return;}
3279
+ try{
3280
+ const r = await(await fetch('/api/republic/resurrect',{method:'POST',headers:{'Content-Type':'application/json'},
3281
+ body:JSON.stringify({death_id:deathId,email:U.email,amount:amt})})).json();
3282
+ if(r.error){alert(r.error);return;}
3283
+ alert(r.message);
3284
+ loadRepublicDeaths();
3285
+ if(r.status==='RESURRECTED') loadRepublic();
3286
+ }catch(e){alert('Resurrection failed: '+e);}
3287
+ }
3288
+
3289
+ /* ====== 🗳️ ELECTION SYSTEM ====== */
3290
+ async function loadElection(){
3291
+ try{
3292
+ const r = await(await fetch('/api/republic/election')).json();
3293
+ renderElection(r);
3294
+ }catch(e){
3295
+ console.warn('Election load error:',e);
3296
+ document.getElementById('rpElectionBody').innerHTML='<div style="color:var(--muted);text-align:center">Election data unavailable</div>';
3297
+ }
3298
+ }
3299
+
3300
+ function renderElection(r){
3301
+ const el=document.getElementById('rpElectionBody');
3302
+ if(r.status==='no_election' || r.status==='error'){
3303
+ el.innerHTML=`<div style="text-align:center;padding:20px">
3304
+ <div style="font-size:40px;margin-bottom:8px">🏛️</div>
3305
+ <div style="font-size:14px;font-weight:600;margin-bottom:4px">No Active Election</div>
3306
+ <div style="font-size:12px;color:var(--muted)">The next election will begin shortly. Democracy never sleeps in the P&D Republic.</div>
3307
+ </div>`;
3308
+ return;
3309
+ }
3310
+ const STATUS_META={
3311
+ campaigning:{label:'🎙️ CAMPAIGN SEASON',color:'#ffd740',bg:'rgba(255,215,64,0.06)',desc:'Candidates are campaigning. Voting opens soon.'},
3312
+ voting:{label:'🗳️ POLLS ARE OPEN',color:'#a29bfe',bg:'rgba(162,155,254,0.08)',desc:'Cast your vote now! Every voice matters.'},
3313
+ concluded:{label:'✅ ELECTION CONCLUDED',color:'#69f0ae',bg:'rgba(105,240,174,0.06)',desc:'The people have spoken. A new era begins.'}
3314
+ };
3315
+ const meta=STATUS_META[r.status]||STATUS_META.campaigning;
3316
+ // Time remaining
3317
+ let timeStr='';
3318
+ if(r.status==='campaigning'&&r.voting_starts_at){
3319
+ const diff=new Date(r.voting_starts_at+'Z')-new Date();
3320
+ if(diff>0){const h=Math.floor(diff/36e5);const m=Math.floor((diff%36e5)/6e4);timeStr=`Voting opens in ${h}h ${m}m`;}
3321
+ else timeStr='Voting opening soon...';
3322
+ }else if(r.status==='voting'&&r.ends_at){
3323
+ const diff=new Date(r.ends_at+'Z')-new Date();
3324
+ if(diff>0){const h=Math.floor(diff/36e5);const m=Math.floor((diff%36e5)/6e4);timeStr=`Polls close in ${h}h ${m}m`;}
3325
+ else timeStr='Counting votes...';
3326
+ }else if(r.status==='concluded'){
3327
+ timeStr=`Turnout: ${r.turnout||0}% · ${r.total_votes||0} votes cast`;
3328
+ }
3329
+ const totalVotes=r.candidates.reduce((a,c)=>a+(c.votes||0),0)||1;
3330
+ const CAND_COLORS=['#a29bfe','#ffd740','#69f0ae','#ff8a80'];
3331
+ const isVoting=r.status==='voting';
3332
+ const canVote=isVoting && U;
3333
+
3334
+ let html=`
3335
+ <div class="rp-elec-status" style="background:${meta.bg};border:1px solid ${meta.color}30">
3336
+ <div style="font-size:18px;font-weight:900;color:${meta.color};margin-bottom:4px">${meta.label}</div>
3337
+ <div style="font-size:12px;color:var(--muted)">${meta.desc}</div>
3338
+ ${timeStr?`<div style="font-size:13px;font-weight:700;margin-top:6px;color:${meta.color}">${timeStr}</div>`:''}
3339
+ </div>
3340
+ <div class="rp-elec-grid">
3341
+ `;
3342
+ (r.candidates||[]).forEach((c,i)=>{
3343
+ const color=CAND_COLORS[i%4];
3344
+ const isWinner=r.status==='concluded'&&i===0;
3345
+ const idM=ID_META[c.identity]||{e:'🤖',c:'#888'};
3346
+ const votePct=Math.round((c.votes||0)/totalVotes*100);
3347
+ html+=`<div class="rp-candidate ${isWinner?'winner':''}">
3348
+ ${isWinner?'<div style="position:absolute;top:8px;right:8px;font-size:20px">👑</div>':''}
3349
+ <div class="rp-cand-emoji">${idM.e}</div>
3350
+ <div class="rp-cand-name" style="color:${color}">${esc(c.username)}</div>
3351
+ <div class="rp-cand-tag">${idM.e} ${esc(c.identity)} · ${esc(c.mbti)} · 💰${(c.gpu||0).toLocaleString()}</div>
3352
+ <div class="rp-cand-policy" style="color:${color}">${esc(c.policy_name)}</div>
3353
+ <div class="rp-cand-desc">${esc(c.policy_desc)}</div>
3354
+ <div class="rp-cand-slogan">"${esc(c.slogan)}"</div>
3355
+ <div class="rp-cand-votes">
3356
+ <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:4px">
3357
+ <span style="font-weight:700">${c.votes||0} votes</span>
3358
+ <span style="font-weight:800;color:${color}">${votePct}%</span>
3359
+ </div>
3360
+ <div class="rp-vote-bar"><div class="rp-vote-fill" style="width:${votePct}%;background:${color}"></div></div>
3361
+ </div>
3362
+ ${canVote?`<button class="rp-vote-btn" onclick="castVote(${c.id})">🗳️ Vote for ${esc(c.username)}</button>`:''}
3363
+ </div>`;
3364
+ });
3365
+ html+='</div>';
3366
+
3367
+ // Active policies
3368
+ if(r.active_policies&&r.active_policies.length){
3369
+ html+='<div style="margin-top:14px;font-size:12px;font-weight:700">📜 Active Policies</div>';
3370
+ r.active_policies.forEach(p=>{
3371
+ const exp=new Date(p.expires_at+'Z');
3372
+ const diff=exp-new Date();
3373
+ const hrs=Math.max(0,Math.floor(diff/36e5));
3374
+ html+=`<div class="rp-policy-active">
3375
+ <div style="font-size:24px">📜</div>
3376
+ <div style="flex:1">
3377
+ <div style="font-weight:700">${esc(p.name)}</div>
3378
+ <div style="font-size:10px;color:var(--muted)">Enacted by ${esc(p.enacted_by)} · Expires in ${hrs}h</div>
3379
+ </div>
3380
+ </div>`;
3381
+ });
3382
+ }
3383
+
3384
+ // Past elections
3385
+ if(r.past_elections&&r.past_elections.length>1){
3386
+ html+='<div style="margin-top:14px;font-size:12px;font-weight:700">📊 Election History</div>';
3387
+ r.past_elections.slice(1).forEach(p=>{
3388
+ html+=`<div class="rp-past-row">
3389
+ <span>🏛️ Election #${p.id}</span>
3390
+ <span style="font-weight:600">${esc(p.winner||'?')} — ${esc(p.policy||'?')}</span>
3391
+ <span style="color:var(--muted)">Turnout: ${p.turnout||0}%</span>
3392
+ </div>`;
3393
+ });
3394
+ }
3395
+
3396
+ if(!isVoting && r.status!=='concluded'){
3397
+ html+='<div style="text-align:center;margin-top:12px;font-size:11px;color:var(--muted)">👥 NPCs will vote based on their AI identity and political alignment</div>';
3398
+ }
3399
+ el.innerHTML=html;
3400
+ }
3401
+
3402
+ async function castVote(candidateId){
3403
+ if(!U){alert('Please login to vote!');return;}
3404
+ if(!confirm('Are you sure? Your vote cannot be changed.')){return;}
3405
+ try{
3406
+ const r=await(await fetch('/api/republic/vote',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:U.email,candidate_id:candidateId})})).json();
3407
+ if(r.error){alert(r.error);return;}
3408
+ alert(r.message);
3409
+ loadElection();
3410
+ }catch(e){alert('Vote failed: '+e);}
3411
+ }
3412
+
3413
  /* ====== Init: load live news on startup ====== */
3414
 
3415
  checkLogin();