from pathlib import Path
p = Path('/opt/struktur/agent-solutions-cockpit/app.py')
s = p.read_text()
old = ''' {% endfor %}\n</div>\n<footer>Agent Solutions · VPS IONOS · 31.70.68.228</footer>'''
new = ''' {% endfor %}\n <a class="card" href="/routing" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit;">\n <div class="card-header"><div class="card-icon">🧭</div><span class="badge planned">read-only</span></div>\n <div class="card-title">Routing</div>\n <div class="card-desc">Hermes Routing Control Plane: Entscheidungen, Modelle, Feedback und Hygiene.</div>\n <div class="card-actions"><span class="btn btn-open">Dashboard öffnen ↗</span></div>\n </a>\n</div>\n<footer>Agent Solutions · VPS IONOS · 31.70.68.228</footer>'''
if old not in s:
raise SystemExit('main page insertion anchor not found')
s = s.replace(old, new, 1)
anchor = "@app.route('/favicon.ico')\ndef favicon():\n"
insert = r'''ROUTING_DB = '/home/hermes/.hermes/routing-control/routing.db'
ROUTING_HTML = ''' + "'''" + r'''<!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Agent Solutions · Routing</title>
<style>
body{margin:0;background:#0f1117;color:#e2e8f0;font:14px system-ui,sans-serif}header{padding:1.5rem 3rem;border-bottom:1px solid #1e2330;display:flex;align-items:center;justify-content:space-between}h1,h2{color:#fff;margin:0 0 .7rem}h1{font-size:1.5rem}.sub,.muted{color:#94a3b8}.back{color:#93c5fd;text-decoration:none;border:1px solid #1e3a5f;padding:.4rem .8rem;border-radius:7px}.wrap{padding:1.8rem 3rem;max-width:1500px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}.card,section{background:#161b27;border:1px solid #26324a;border-radius:10px;padding:1rem}.metric{font-size:1.8rem;font-weight:700;color:#fff}.label{color:#94a3b8;font-size:.82rem}.two{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:1rem;margin-bottom:1.5rem}table{width:100%;border-collapse:collapse;font-size:.82rem}th,td{text-align:left;padding:.55rem .45rem;border-bottom:1px solid #26324a;vertical-align:top}th{color:#93c5fd;font-weight:600;white-space:nowrap}td{color:#dbe4f0}.scroll{overflow:auto;max-height:650px}.pill{display:inline-block;padding:.15rem .45rem;border-radius:5px;background:#26324a;color:#cbd5e1}.sev-critical{color:#fca5a5}.sev-error{color:#fb923c}.sev-warning{color:#fbbf24}.sev-info{color:#93c5fd}.empty{color:#94a3b8;padding:.5rem 0}
</style></head><body><header><div><h1>🧭 Hermes Routing Control Plane</h1><div class="sub">Read-only Auswertung · Datenquelle: routing.db</div></div><a class="back" href="/" target="_blank" rel="noopener noreferrer">← Agent Solutions</a></header>
<main class="wrap"><div class="grid">{% for label,value in metrics %}<div class="card"><div class="label">{{label}}</div><div class="metric">{{value}}</div></div>{% endfor %}</div>
<div class="two"><section><h2>Modellverteilung</h2><table><tr><th>Modellgruppe</th><th>Anzahl</th></tr>{% for row in model_distribution %}<tr><td>{{row.label}}</td><td>{{row.count}}</td></tr>{% endfor %}</table></section>
<section><h2>Router vs Legacy</h2><table><tr><th>Quelle</th><th>Anzahl</th></tr>{% for row in source_distribution %}<tr><td>{{row.label}}</td><td>{{row.count}}</td></tr>{% endfor %}</table></section></div>
<section><h2>Letzte 20 Routingentscheidungen</h2><div class="scroll"><table><tr><th>Zeit</th><th>Board</th><th>Task</th><th>Assignee</th><th>Modell</th><th>Reasoning</th><th>Risk</th><th>Confidence</th><th>Begründung</th></tr>{% for row in decisions %}<tr><td>{{row.time}}</td><td>{{row.board}}</td><td>{{row.task}}</td><td>{{row.assignee}}</td><td>{{row.model}}</td><td>{{row.reasoning}}</td><td>{{row.risk}}</td><td>{{row.confidence}}</td><td>{{row.reasons}}</td></tr>{% endfor %}</table></div></section>
<div class="two"><section><h2>Feedback-Auswertung</h2><h3>Nach Modell</h3><table><tr><th>Modell</th><th>Erfolge</th><th>Gesamt</th><th>Quote</th></tr>{% for row in feedback_model %}<tr><td>{{row.label}}</td><td>{{row.success}}</td><td>{{row.total}}</td><td>{{row.rate}}</td></tr>{% endfor %}</table><h3>Nach Familie</h3><table><tr><th>Familie</th><th>Erfolge</th><th>Gesamt</th><th>Quote</th></tr>{% for row in feedback_family %}<tr><td>{{row.label}}</td><td>{{row.success}}</td><td>{{row.total}}</td><td>{{row.rate}}</td></tr>{% endfor %}</table></section>
<section><h2>Hygiene-Befunde nach Severity</h2><table><tr><th>Severity</th><th>Anzahl</th></tr>{% for row in hygiene_summary %}<tr><td class="sev-{{row.label|lower}}">{{row.label}}</td><td>{{row.count}}</td></tr>{% endfor %}</table><h3>Befunde</h3><table><tr><th>Severity</th><th>Kategorie</th><th>Item</th><th>Detail</th></tr>{% for row in hygiene %}<tr><td class="sev-{{row.severity|lower}}">{{row.severity}}</td><td>{{row.category}}</td><td>{{row.item}}</td><td>{{row.detail}}</td></tr>{% endfor %}</table></section></div></main></body></html>''' + "'''" + '''
def _routing_rows():
import sqlite3
from datetime import datetime, timezone
db = sqlite3.connect('file:' + ROUTING_DB + '?mode=ro', uri=True)
db.row_factory = sqlite3.Row
try:
decisions = db.execute('SELECT board, task_id, assignee, model, reasoning, risk_score, risk_class, confidence, reasons_json, source, created_at FROM decisions ORDER BY created_at DESC LIMIT 20').fetchall()
all_decisions = db.execute('SELECT model, source FROM decisions').fetchall()
feedback = db.execute('SELECT model, family, success FROM feedback').fetchall()
hygiene = db.execute('SELECT severity, category, item, detail FROM hygiene ORDER BY observed_at DESC').fetchall()
finally:
db.close()
def count_by(rows, key, labels=None):
counts = {}
for row in rows:
value = row[key] or 'sonstige'
counts[value] = counts.get(value, 0) + 1
if labels is None:
labels = sorted(counts)
return [{'label': label, 'count': counts.get(label, 0)} for label in labels]
def model_group(model):
value = (model or '').lower()
for label in ('Luna', 'Sol', 'Astra'):
if label.lower() in value:
return label
return 'sonstige'
model_counts = {label: 0 for label in ('Luna', 'Sol', 'Astra', 'sonstige')}
source_counts = {'Router': 0, 'Legacy': 0, 'sonstige': 0}
for row in all_decisions:
model_counts[model_group(row['model'])] += 1
source = (row['source'] or '').lower()
source_counts['Router' if source == 'router' else 'Legacy' if source == 'legacy' else 'sonstige'] += 1
def feedback_table(key):
groups = {}
for row in feedback:
label = row[key] or 'unbekannt'
item = groups.setdefault(label, [0, 0])
item[0] += int(row['success'] or 0); item[1] += 1
return [{'label': label, 'success': values[0], 'total': values[1], 'rate': f"{(100 * values[0] / values[1]):.1f}%"} for label, values in sorted(groups.items())]
def timestamp(value):
try: return datetime.fromtimestamp(int(value), timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
except (TypeError, ValueError, OverflowError): return str(value or '')
formatted = []
for row in decisions:
try: reasons = ', '.join(json.loads(row['reasons_json'] or '[]'))
except (TypeError, ValueError): reasons = str(row['reasons_json'] or '')
confidence = '' if row['confidence'] is None else f"{float(row['confidence']):.2f}"
formatted.append({'time': timestamp(row['created_at']), 'board': row['board'], 'task': row['task_id'], 'assignee': row['assignee'] or '—', 'model': row['model'] or '—', 'reasoning': row['reasoning'] or '—', 'risk': f"{row['risk_class']} ({row['risk_score']})", 'confidence': confidence, 'reasons': reasons or '—'})
hygiene_summary = count_by(hygiene, 'severity', ['critical', 'error', 'warning', 'info'])
return {'metrics': [('Gesamtentscheidungen', len(all_decisions)), ('Router', source_counts['Router']), ('Legacy', source_counts['Legacy']), ('Feedback-Einträge', len(feedback)), ('Hygiene-Befunde', len(hygiene))], 'model_distribution': [{'label': label, 'count': model_counts[label]} for label in ('Luna', 'Sol', 'Astra', 'sonstige')], 'source_distribution': [{'label': label, 'count': source_counts[label]} for label in ('Router', 'Legacy', 'sonstige')], 'decisions': formatted, 'feedback_model': feedback_table('model'), 'feedback_family': feedback_table('family'), 'hygiene_summary': hygiene_summary, 'hygiene': [dict(row) for row in hygiene]}
@app.route('/routing')
def routing():
try:
return render_template_string(ROUTING_HTML, **_routing_rows())
except (OSError, sqlite3.Error):
return Response('Routing-Daten nicht verfügbar', status=503, mimetype='text/plain')
'''
if anchor not in s:
raise SystemExit('route anchor not found')
s = s.replace(anchor, insert + anchor, 1)
p.write_text(s)