añadido Argos Core

This commit is contained in:
2025-08-20 01:16:53 +02:00
parent 550d5fbe52
commit d973225012
17 changed files with 550 additions and 151 deletions

View File

@@ -11,5 +11,4 @@ data:
hls: no
webrtc: yes
paths:
all:
sourceOnDemand: yes
all: {}

View File

@@ -1,9 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mosquitto-conf
name: mosquitto-config
namespace: argos-core
data:
mosquitto.conf: |
persistence true
persistence_location /mosquitto/data/
listener 1883 0.0.0.0
allow_anonymous true

View File

@@ -6,11 +6,14 @@ metadata:
data:
settings.yaml: |
mqtt:
host: mqtt.argos.interna
port: 1883
host: mosquitto.argos-core.svc.cluster.local
topic: frigate/events
topic_base: argos/alerts
port: 1883
client_id: argos-core
minio:
endpoint: minio.argos-core.svc.cluster.local:9000
secure: false
bucket: argos
region: us-east-1
edges:

View File

@@ -5,83 +5,136 @@ metadata:
namespace: argos-core
data:
app.py: |
import os, sqlite3, time
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
import os
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
from minio import Minio
from urllib.parse import urlparse
from urllib.parse import quote
from datetime import timezone
import io
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()
BUCKET = os.getenv("MINIO_BUCKET", "argos")
endpoint = os.getenv("MINIO_ENDPOINT", "minio.argos-core.svc.cluster.local:9000")
access_key = os.getenv("MINIO_ACCESS_KEY")
secret_key = os.getenv("MINIO_SECRET_KEY")
secure = os.getenv("MINIO_SECURE", "false").lower() == "true"
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
mc = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure)
app = FastAPI()
@app.get("/health")
def health():
# simple check contra MinIO
try:
mc.list_buckets()
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
def _iter_events(prefix: str | None, limit: int):
# list_objects devuelve en orden “no garantizado”; recolectamos y ordenamos por last_modified desc
objs = []
for obj in mc.list_objects(BUCKET, prefix=prefix, recursive=True):
# omitimos directorios “lógicos” y thumbs por defecto en el listado principal
if obj.is_dir:
continue
if "/thumbs/" in obj.object_name:
continue
objs.append(obj)
objs.sort(key=lambda o: o.last_modified or 0, reverse=True)
return objs[:limit]
@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)]
def api_events(prefix: str | None = Query(default=None), limit: int = 50):
data = []
for o in _iter_events(prefix, limit):
ts = None
if o.last_modified:
ts = o.last_modified.astimezone(timezone.utc).isoformat()
# si existe thumb, la inferimos (cam/... → cam/thumbs/...)
thumb = None
parts = o.object_name.split("/")
if len(parts) >= 2:
maybe = f"{parts[0]}/thumbs/" + parts[-1].rsplit(".",1)[0] + ".jpg"
try:
mc.stat_object(BUCKET, maybe)
thumb = maybe
except:
pass
data.append({
"key": o.object_name,
"size": o.size,
"last_modified": ts,
"thumb": thumb
})
return JSONResponse(data)
@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("/file")
def file(key: str):
try:
resp = mc.get_object(BUCKET, key)
except Exception as e:
raise HTTPException(status_code=404, detail=f"not found: {e}")
# stream al cliente sin exponer MinIO
def gen():
for d in resp.stream(32*1024):
yield d
# tipo de contenido básico por extensión
ctype = "application/octet-stream"
k = key.lower()
if k.endswith(".mp4"): ctype = "video/mp4"
elif k.endswith(".jpg") or k.endswith(".jpeg"): ctype = "image/jpeg"
return StreamingResponse(gen(), media_type=ctype)
@app.get("/view", response_class=HTMLResponse)
def view(key: str):
keyq = quote(key)
return f"""
<!doctype html>
<html><head><meta charset="utf-8"><title>Ver {key}</title>
<style>body{{font-family:sans-serif;padding:16px}} video{{max-width:100%;height:auto}}</style>
</head><body>
<h2>{key}</h2>
<video controls src="/file?key={keyq}"></video>
<p><a href="/file?key={keyq}" download>Descargar</a></p>
<p><a href="/">Volver</a></p>
</body></html>
"""
@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>
def home(prefix: str | None = None, limit: int = 20):
# HTML mínimo que consume /api/events y pinta thumbs/links
p = f"&prefix={quote(prefix)}" if prefix else ""
items = []
for e in _iter_events(prefix, limit):
thumb_html = ""
# intentar thumb
parts = e.object_name.split("/")
if len(parts) >= 2:
maybe = f"{parts[0]}/thumbs/" + parts[-1].rsplit(".",1)[0] + ".jpg"
try:
mc.stat_object(BUCKET, maybe)
thumb_html = f'<img src="/file?key={quote(maybe)}" style="height:80px;border-radius:6px;margin-right:8px" />'
except:
pass
items.append(f"""
<li style="margin:8px 0;list-style:none;display:flex;align-items:center">
{thumb_html}
<a href="/view?key={quote(e.object_name)}">{e.object_name}</a>
</li>
""")
items_html = "\n".join(items) or "<p><i>Sin eventos aún.</i></p>"
return f"""
<!doctype html><html><head><meta charset="utf-8"><title>Argos Panel</title>
<style>body{{font-family:sans-serif;max-width:1000px;margin:24px auto;padding:0 16px}}</style>
</head><body>
<h1>Argos Panel</h1>
<form>
<label>Prefijo (cámara): <input type="text" name="prefix" value="{prefix or ""}"></label>
<label style="margin-left:12px">Límite: <input type="number" name="limit" value="{limit}" min="1" max="200"></label>
<button type="submit">Filtrar</button>
</form>
<ul style="padding:0">{items_html}</ul>
</body></html>
"""

View File

@@ -0,0 +1,87 @@
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>
"""