Spaces:
Running
Running
Update index.html
Browse files- 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();
|