88 lines
3.8 KiB
YAML
88 lines
3.8 KiB
YAML
apiVersion: v1
|
||
kind: ConfigMap
|
||
metadata:
|
||
name: argos-panel-config
|
||
namespace: argos-core
|
||
data:
|
||
app.py: |
|
||
import os, sqlite3, time
|
||
from fastapi import FastAPI, HTTPException
|
||
from fastapi.responses import HTMLResponse
|
||
from minio import Minio
|
||
from urllib.parse import urlparse
|
||
|
||
DB="/data/argos.db"
|
||
mc=Minio(os.getenv("MINIO_ENDPOINT","s3.argos.interna"),
|
||
access_key=os.getenv("MINIO_ACCESS_KEY"),
|
||
secret_key=os.getenv("MINIO_SECRET_KEY"),
|
||
secure=os.getenv("MINIO_SECURE","false").lower()=="true")
|
||
app=FastAPI()
|
||
|
||
def rows(limit=100, camera=None, since=None):
|
||
q="SELECT id, ts, edge, camera, label, s3url, thumb_s3 FROM events"
|
||
cond=[]; args=[]
|
||
if camera: cond.append("camera=?"); args.append(camera)
|
||
if since: cond.append("ts>=?"); args.append(int(since))
|
||
if cond: q+=" WHERE "+ " AND ".join(cond)
|
||
q+=" ORDER BY ts DESC LIMIT ?"; args.append(limit)
|
||
con=sqlite3.connect(DB); cur=con.cursor()
|
||
cur.execute(q, tuple(args)); r=cur.fetchall(); con.close()
|
||
return r
|
||
|
||
@app.get("/api/events")
|
||
def api_events(limit:int=100, camera:str=None, since:int=None):
|
||
return [dict(id=i, ts=t, edge=e, camera=c, label=l or "", s3url=s, thumb=th or "")
|
||
for (i,t,e,c,l,s,th) in rows(limit,camera,since)]
|
||
|
||
@app.get("/api/url/{event_id}")
|
||
def presign(event_id: str, expires: int = 600):
|
||
con=sqlite3.connect(DB); cur=con.cursor()
|
||
cur.execute("SELECT s3url FROM events WHERE id=?", (event_id,))
|
||
row=cur.fetchone(); con.close()
|
||
if not row: raise HTTPException(404, "Not found")
|
||
s3url=row[0]; p=urlparse(s3url); b=p.netloc; k=p.path.lstrip("/")
|
||
return {"url": mc.presigned_get_object(b, k, expires=expires)}
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
def index():
|
||
return """
|
||
<!doctype html><meta charset="utf-8"><title>ARGOS Panel</title>
|
||
<style>body{font-family:system-ui;margin:1.5rem} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
|
||
.card{border:1px solid #ddd;border-radius:10px;padding:10px} img{width:100%;height:160px;object-fit:cover;border-radius:8px}
|
||
button{padding:.4rem .6rem;margin-right:.3rem}</style>
|
||
<h1>ARGOS – Alarmas</h1>
|
||
<div class="grid" id="grid"></div>
|
||
<div style="margin-top:1rem"><video id="player" width="960" controls></video></div>
|
||
<script>
|
||
const fmt=t=>new Date(t*1000).toLocaleString();
|
||
async function load(){
|
||
const r=await fetch('/api/events?limit=100'); const data=await r.json();
|
||
const g=document.getElementById('grid'); g.innerHTML='';
|
||
for(const ev of data){
|
||
const d=document.createElement('div'); d.className='card';
|
||
const img = ev.thumb ? `<img src="${ev.thumb.replace('s3://','/api/url/THUMB?key=')}" alt="thumb">` : '';
|
||
d.innerHTML = `${img}<div><b>${ev.camera}</b> — ${fmt(ev.ts)}<br>${ev.label||''}</div>
|
||
<div style="margin-top:.4rem">
|
||
<button data-id="${ev.id}" data-action="clip">Ver clip</button>
|
||
<button data-path="${ev.camera}" data-action="live">En directo</button>
|
||
</div>`;
|
||
g.appendChild(d);
|
||
}
|
||
g.onclick=async (e)=>{
|
||
if(e.target.tagName!=='BUTTON') return;
|
||
const v=document.getElementById('player');
|
||
if(e.target.dataset.action==='clip'){
|
||
const id=e.target.dataset.id;
|
||
const j=await (await fetch('/api/url/'+id)).json();
|
||
v.src=j.url; v.play();
|
||
}else if(e.target.dataset.action==='live'){
|
||
const path=e.target.dataset.path;
|
||
// usa MediaMTX web player
|
||
window.open('http://mediamtx.argos.interna/?path='+encodeURIComponent(path),'_blank');
|
||
}
|
||
}
|
||
}
|
||
load(); setInterval(load,10000);
|
||
</script>
|
||
"""
|