M11.2: /api/audit/seal endpoints + Audit log UI page
- routers/audit_seal_router.py exposes:
POST /api/audit/seal (record + seal an audit event)
GET /api/audit/seal/list (recent seals for UI)
GET /api/audit/seal/{id} (single seal + onchain receipt cross-check)
- pgz_sport_api.py mounts the router under /api.
- sport2.html: new 'Audit log' nav item (🔒) and full page that surfaces
wallet, chain, live/pending mode, count, and a table of every sealed
event with polygonscan.com tx links.
- Verified end-to-end: sealing 'sufinanciranje.approved' for klub 3 lands
in pgz_sport.polygon_seals (pending mode — no POLYGON_PRIVKEY in env).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1431,6 +1431,13 @@ try:
|
||||
except Exception as e:
|
||||
print(f'[AUTH/M10] gdpr routers fail: {e}')
|
||||
|
||||
# === Round 3 / CC6 — M11 Blockchain audit (Polygon PoS sealing) ===
|
||||
try:
|
||||
from audit_seal_router import router as audit_seal_router
|
||||
app.include_router(audit_seal_router, prefix='/api')
|
||||
print('[AUDIT/M11] polygon seal router loaded (/api/audit/seal*)')
|
||||
except Exception as e:
|
||||
print(f'[AUDIT/M11] polygon seal router fail: {e}')
|
||||
|
||||
|
||||
@app.get("/sport-3d")
|
||||
@@ -1459,6 +1466,14 @@ def serve_erp():
|
||||
return FileResponse(p)
|
||||
return {"error": "erp.html not found"}
|
||||
|
||||
@app.get("/crm")
|
||||
@app.get("/crm/")
|
||||
def serve_crm():
|
||||
p = HTML_DIR / "crm.html"
|
||||
if p.exists():
|
||||
return FileResponse(p)
|
||||
return {"error": "crm.html not found"}
|
||||
|
||||
@app.get("/login")
|
||||
@app.get("/login/")
|
||||
def serve_login():
|
||||
|
||||
@@ -615,9 +615,80 @@ function loadSection(id){
|
||||
case 'manifestacije': return loadManifestacije();
|
||||
case 'mreza': return loadMreza();
|
||||
case 'forenzika': return loadForenzika();
|
||||
case 'audit': return loadAudit();
|
||||
}
|
||||
}
|
||||
|
||||
//=========== AUDIT LOG (Polygon PoS) ===========
|
||||
async function loadAudit(){
|
||||
const root = $('#pg-audit');
|
||||
root.innerHTML = '<div class="loading">Učitavanje audit zapisa…</div>';
|
||||
let r;
|
||||
try{
|
||||
const resp = await fetch(API+'/audit/seal/list?limit=100');
|
||||
if(!resp.ok) throw new Error('HTTP '+resp.status);
|
||||
r = await resp.json();
|
||||
}catch(e){
|
||||
root.innerHTML = '<div class="empty">Greška: '+esc(e.message||String(e))+'</div>';
|
||||
return;
|
||||
}
|
||||
const wallet = r.wallet || '';
|
||||
const live = r.live ? '<span class="tag gr">🟢 LIVE Polygon</span>' : '<span class="tag gd">⏳ Pending mode</span>';
|
||||
const rows = (r.rows||[]).map(s => {
|
||||
const tx = s.tx_hash || '';
|
||||
const isPending = tx.startsWith('pending:');
|
||||
const txCell = isPending
|
||||
? `<span class="tag gd" title="${esc(tx)}">PENDING</span><span class="tb-s" style="margin-left:6px;font-family:monospace">${esc(tx.slice(0,32))}…</span>`
|
||||
: `<a href="${esc(s.polygonscan_url||'#')}" target="_blank" style="font-family:monospace;font-size:11px">${esc(tx.slice(0,18))}…${esc(tx.slice(-6))}</a>`;
|
||||
const statusCls = s.status==='confirmed'?'gr':(s.status==='broadcast'?'gd':(s.status==='failed'?'rd':''));
|
||||
return `<tr>
|
||||
<td class="num" style="font-size:11px;color:var(--t3)">${s.id}</td>
|
||||
<td style="font-size:11px;color:var(--t3)">${esc((s.created_at||'').replace('T',' ').slice(0,19))}</td>
|
||||
<td><b>${esc(s.action)}</b></td>
|
||||
<td style="font-size:11px">${esc(s.ref_type||'')} <span style="color:var(--t3)">${esc(s.ref_id||'')}</span></td>
|
||||
<td style="font-family:monospace;font-size:10.5px;color:var(--t2)" title="${esc(s.data_hash||'')}">${esc((s.data_hash||'').slice(0,12))}…</td>
|
||||
<td>${txCell}</td>
|
||||
<td><span class="tag ${statusCls}">${esc(s.status||'')}</span></td>
|
||||
<td style="font-size:11px">${esc(s.user_email||'')}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
root.innerHTML = `
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px">
|
||||
<div class="card" style="flex:1;min-width:280px">
|
||||
<div class="card-h"><div class="card-t">🔐 Polygon PoS Audit Sealing</div></div>
|
||||
<div style="padding:12px;font-size:13px;color:var(--t1);line-height:1.6">
|
||||
Ključne akcije (odobrenje sufinanciranja, isplata, validacija liječničkog pregleda, izmjena članstva)
|
||||
pečate se 0-MATIC self-tx-om s SHA-256 hash-em payload-a u <code>data</code> polju.
|
||||
Svaki zapis je nepromjenjiv i provjerljiv preko polygonscan.com.
|
||||
</div>
|
||||
<div style="padding:0 12px 12px;font-size:12px">
|
||||
<div><b>Wallet:</b> <a href="https://polygonscan.com/address/${esc(wallet)}" target="_blank" style="font-family:monospace">${esc(wallet)}</a></div>
|
||||
<div><b>Chain:</b> Polygon PoS (137)</div>
|
||||
<div><b>Mode:</b> ${live}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="min-width:200px">
|
||||
<div class="card-h"><div class="card-t">📊 Statistika</div></div>
|
||||
<div style="padding:12px;font-size:14px">
|
||||
<div><b>${r.count||0}</b> sealed zapisa</div>
|
||||
<div style="color:var(--t3);font-size:12px;margin-top:4px">Najnoviji prikazani prvi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${rows ? `
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📜 Audit zapisi</div></div>
|
||||
<div class="tbl-wrap"><table>
|
||||
<thead><tr>
|
||||
<th class="num">#</th><th>Vrijeme</th><th>Akcija</th><th>Referenca</th>
|
||||
<th>SHA-256</th><th>Polygon TX</th><th>Status</th><th>Korisnik</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table></div>
|
||||
</div>` : '<div class="empty">Nema audit zapisa.</div>'}
|
||||
`;
|
||||
}
|
||||
|
||||
//=========== DASHBOARD ===========
|
||||
async function loadDash(){
|
||||
const root = $('#pg-dashboard');
|
||||
|
||||
Reference in New Issue
Block a user