CtrlK
BlogDocsLog inGet started
Tessl Logo

spec-driven-development/agent-registry

Coordina multipli agenti AI CLI (Kimi, Claude, Gemini, OpenAI, ecc.) che lavorano contemporaneamente sullo stesso progetto e mantiene una memoria collettiva (wiki) di tutte le sessioni passate. Usa questa skill SEMPRE quando lavori in parallelo con altre AI CLI, quando devi salvare lo stato di una sessione condivisa, quando vuoi evitare sovrascritture su file toccati da altri agenti, quando devi chiedere "questo lavoro è già stato fatto? questo bug si è già visto?", o quando l'utente parla di "multi-tap", "registry agenti", "coordination", "lock file", "handoff condiviso", "wiki delle sessioni", "chi sta lavorando su cosa" o "evitare che le AI si pestino i piedi".

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

index.htmlscripts/webapp/static/

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Agent Registry Dashboard</title>
    <style>
        :root {
            --bg: #0f1115;
            --panel: #16191f;
            --text: #e6e6e6;
            --muted: #9aa0a6;
            --border: #2c313a;
            --accent: #4f8cff;
            --ok: #2ecc71;
            --warn: #f1c40f;
            --danger: #e74c3c;
        }
        body {
            margin: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background: var(--bg);
            color: var(--text);
            line-height: 1.5;
        }
        header {
            padding: 1.5rem 2rem;
            border-bottom: 1px solid var(--border);
            background: var(--panel);
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 1rem;
            flex-wrap: wrap;
        }
        h1 { margin: 0; font-size: 1.4rem; }
        .status {
            font-size: 0.85rem;
            color: var(--muted);
        }
        .status.connected { color: var(--ok); }
        .status.disconnected { color: var(--danger); }
        .header-right {
            display: flex;
            align-items: center;
            gap: 1rem;
            flex-wrap: wrap;
        }
        main { padding: 2rem; }
        .meta {
            margin-bottom: 1rem;
            color: var(--muted);
            font-size: 0.9rem;
        }
        .tabs {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 1rem;
        }
        .tab-btn {
            padding: 0.4rem 1rem;
            border: 1px solid var(--border);
            border-radius: 6px;
            background: var(--panel);
            color: var(--muted);
            cursor: pointer;
            font-size: 0.9rem;
        }
        .tab-btn.active {
            color: var(--accent);
            border-color: var(--accent);
        }
        .filters {
            display: flex;
            gap: 0.75rem;
            align-items: center;
            flex-wrap: wrap;
            margin-bottom: 1rem;
            padding: 0.75rem 1rem;
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 8px;
        }
        .filters label {
            font-size: 0.8rem;
            color: var(--muted);
            display: flex;
            flex-direction: column;
            gap: 0.2rem;
        }
        .filters input, .filters select {
            background: var(--bg);
            color: var(--text);
            border: 1px solid var(--border);
            border-radius: 4px;
            padding: 0.3rem 0.5rem;
            font-size: 0.85rem;
        }
        button.action {
            background: var(--accent);
            color: #fff;
            border: none;
            border-radius: 4px;
            padding: 0.35rem 0.8rem;
            cursor: pointer;
            font-size: 0.8rem;
        }
        button.action:hover { opacity: 0.85; }
        button.danger { background: var(--danger); }
        button.warn { background: var(--warn); color: #111; }
        table {
            width: 100%;
            border-collapse: collapse;
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 8px;
            overflow: hidden;
        }
        th, td {
            padding: 0.75rem 1rem;
            text-align: left;
            border-bottom: 1px solid var(--border);
            font-size: 0.9rem;
            vertical-align: top;
        }
        th {
            background: #1f232c;
            color: var(--accent);
            font-weight: 600;
        }
        tr:last-child td { border-bottom: none; }
        tr.wiki-row { cursor: pointer; }
        tr.wiki-row:hover td { background: rgba(79, 140, 255, 0.06); }
        .empty {
            padding: 3rem;
            text-align: center;
            color: var(--muted);
        }
        .badge {
            display: inline-block;
            padding: 0.15rem 0.5rem;
            border-radius: 4px;
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
        }
        .badge-onworking { background: rgba(79, 140, 255, 0.15); color: var(--accent); }
        .badge-finished { background: rgba(46, 204, 113, 0.15); color: var(--ok); }
        .badge-stop { background: rgba(241, 196, 15, 0.15); color: var(--warn); }
        .badge-killed { background: rgba(231, 76, 60, 0.15); color: var(--danger); }
        .badge-stale { background: rgba(241, 196, 15, 0.15); color: var(--warn); }
        .badge-live { background: rgba(46, 204, 113, 0.15); color: var(--ok); }
        .cell-list {
            max-width: 220px;
            white-space: pre-wrap;
            word-break: break-word;
        }
        .issues { color: var(--danger); }
        .section-title {
            margin: 1.5rem 0 0.75rem;
            font-size: 1rem;
            color: var(--muted);
        }
        .notice {
            margin: 0.75rem 0;
            padding: 0.6rem 1rem;
            border-radius: 6px;
            border: 1px solid var(--border);
            background: var(--panel);
            font-size: 0.85rem;
            display: none;
        }
        .notice.ok { border-color: var(--ok); color: var(--ok); }
        .notice.err { border-color: var(--danger); color: var(--danger); }
        .detail-row td {
            background: #10131a;
            white-space: pre-wrap;
            word-break: break-word;
        }
        .detail-grid {
            display: grid;
            grid-template-columns: 180px 1fr;
            gap: 0.3rem 1rem;
            font-size: 0.85rem;
        }
        .detail-grid dt { color: var(--muted); }
        .detail-grid dd { margin: 0; }
        .sync-ok { color: var(--ok); }
        .sync-off { color: var(--muted); }
        .sync-err { color: var(--warn); }
        .setup-card {
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 8px;
            padding: 1.25rem;
            margin-bottom: 1rem;
        }
        .setup-card h2 { margin: 0 0 0.5rem; font-size: 1.1rem; color: var(--accent); }
        .setup-card p { margin: 0 0 0.75rem; color: var(--muted); font-size: 0.9rem; }
        .setup-card input[type="text"] {
            background: var(--bg);
            color: var(--text);
            border: 1px solid var(--border);
            border-radius: 4px;
            padding: 0.4rem 0.6rem;
            width: min(100%, 480px);
            font-size: 0.9rem;
        }
        .setup-card label.confirm {
            display: block;
            margin: 0.75rem 0;
            font-size: 0.85rem;
            color: var(--warn);
        }
        .setup-card .setup-actions {
            display: flex;
            gap: 0.5rem;
            align-items: center;
            flex-wrap: wrap;
            margin-top: 0.5rem;
        }
        .setup-card .setup-msg {
            margin-top: 0.75rem;
            font-size: 0.85rem;
        }
        .setup-card .setup-msg.ok { color: var(--ok); }
        .setup-card .setup-msg.err { color: var(--danger); }
        .setup-card.hidden { display: none; }
    </style>
</head>
<body>
    <header>
        <h1>🧭 Agent Registry Dashboard</h1>
        <div class="header-right">
            <div id="sync-status" class="status sync-off">git-sync: —</div>
            <button class="action warn" id="cleanup-btn">Cleanup zombie</button>
            <div id="connection-status" class="status">Connessione in corso...</div>
        </div>
    </header>
    <main>
        <div class="meta" id="registry-path">Registry: —</div>
        <div class="notice" id="notice"></div>

        <div class="setup-card" id="setup-card">
            <h2>Configura il multi-macchina</h2>
            <p>Il git-sync non è configurato. Inserisci l'URL di un repository git privato per sincronizzare questa home su più macchine.</p>
            <input type="text" id="setup-url" placeholder="git@github.com:tuo-user/agent-registry.git">
            <label class="confirm hidden" id="setup-confirm-public">
                <input type="checkbox" id="setup-confirm-public-check"> Confermo: il repository è pubblico e accetto il rischio di esporre contesto di lavoro
            </label>
            <label class="confirm hidden" id="setup-confirm-merge">
                <input type="checkbox" id="setup-confirm-merge-check"> Confermo: integrare i dati locali con quelli del remote
            </label>
            <div class="setup-actions">
                <button class="action" id="setup-btn">Configura multi-macchina</button>
            </div>
            <div class="setup-msg" id="setup-msg"></div>
        </div>

        <div class="tabs">
            <button class="tab-btn active" id="tab-sessions">Sessioni</button>
            <button class="tab-btn" id="tab-wiki">Wiki</button>
        </div>

        <section id="view-sessions">
            <div class="filters">
                <label>Data
                    <input type="date" id="filter-date">
                </label>
                <label>Provider
                    <select id="filter-provider"><option value="">Tutti</option></select>
                </label>
                <label>Status
                    <select id="filter-status">
                        <option value="OnWorking" selected>OnWorking</option>
                        <option value="Stop">Stop</option>
                        <option value="Finished">Finished</option>
                        <option value="Killed">Killed</option>
                        <option value="">Tutti</option>
                    </select>
                </label>
            </div>
            <div id="table-container">
                <div class="empty">Caricamento in corso...</div>
            </div>

            <h2 class="section-title">Lock attivi</h2>
            <div id="locks-container">
                <div class="empty">Nessun lock attivo.</div>
            </div>
        </section>

        <section id="view-wiki" style="display:none">
            <div class="filters">
                <label>Ricerca (router / cosa fatto / bug)
                    <input type="text" id="wiki-q" placeholder="lascia vuoto per le ultime entry" size="40">
                </label>
                <button class="action" id="wiki-search-btn">Cerca</button>
            </div>
            <div id="wiki-container">
                <div class="empty">Caricamento in corso...</div>
            </div>
        </section>
    </main>

    <script>
        const statusEl = document.getElementById('connection-status');
        const syncEl = document.getElementById('sync-status');
        const pathEl = document.getElementById('registry-path');
        const containerEl = document.getElementById('table-container');
        const locksEl = document.getElementById('locks-container');
        const noticeEl = document.getElementById('notice');
        const dateEl = document.getElementById('filter-date');
        const providerEl = document.getElementById('filter-provider');
        const statusFilterEl = document.getElementById('filter-status');

        let lastAgents = [];
        let lastLocks = [];

        // Default: data odierna (locale del browser, coerente con Roma per l'utente)
        dateEl.value = new Date().toLocaleDateString('sv-SE');

        function escapeHtml(text) {
            if (text === null || text === undefined) return '';
            return String(text)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
        }

        function showNotice(text, ok) {
            noticeEl.textContent = text;
            noticeEl.className = 'notice ' + (ok ? 'ok' : 'err');
            noticeEl.style.display = 'block';
            setTimeout(() => { noticeEl.style.display = 'none'; }, 8000);
        }

        function badge(status) {
            const cls = {
                'OnWorking': 'badge-onworking',
                'Finished': 'badge-finished',
                'Stop': 'badge-stop',
                'Killed': 'badge-killed'
            }[status] || 'badge-stop';
            return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
        }

        function currentFilters() {
            return {
                date: dateEl.value || '',
                provider: providerEl.value || '',
                status: statusFilterEl.value
            };
        }

        function applyFilters(agents) {
            const f = currentFilters();
            return (agents || []).filter(a => {
                if (f.date && !String(a.started_at || '').startsWith(f.date)) return false;
                if (f.provider && a.provider !== f.provider) return false;
                if (f.status && a.status !== f.status) return false;
                return true;
            });
        }

        function updateProviderOptions(agents) {
            const current = providerEl.value;
            const providers = [...new Set((agents || []).map(a => a.provider).filter(Boolean))].sort();
            providerEl.innerHTML = '<option value="">Tutti</option>' +
                providers.map(p => `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`).join('');
            if (providers.includes(current)) providerEl.value = current;
        }

        function renderAgent(a) {
            const todo = a.todo || {};
            const fmtList = (arr) => Array.isArray(arr) && arr.length ? arr.join('\n') : '—';
            const killBtn = a.status === 'OnWorking'
                ? `<button class="action danger" onclick="killSession('${escapeHtml(a.session_id)}')">Kill</button>`
                : '';
            return `
                <tr>
                    <td><strong>${escapeHtml(a.session_id)}</strong><br><span style="color:var(--muted);font-size:0.8rem">${escapeHtml(a.project || '')} ${escapeHtml(a.git_branch || '')}</span></td>
                    <td>${escapeHtml(a.provider)}<br><span style="color:var(--muted);font-size:0.8rem">${escapeHtml(a.ai_version)}</span></td>
                    <td>${escapeHtml(a.started_at)}</td>
                    <td>${escapeHtml(a.working_on)}</td>
                    <td class="cell-list">${escapeHtml(fmtList(todo.past))}</td>
                    <td class="cell-list">${escapeHtml(fmtList(todo.present))}</td>
                    <td class="cell-list">${escapeHtml(fmtList(todo.future))}</td>
                    <td class="cell-list">${escapeHtml(fmtList(a.space))}</td>
                    <td class="cell-list">${escapeHtml(fmtList(a.do_not_touch))}</td>
                    <td>${badge(a.status)}</td>
                    <td class="issues">${escapeHtml(a.issues || '—')}</td>
                    <td class="cell-list">${escapeHtml(a.handoff || '—')}</td>
                    <td>${killBtn}</td>
                </tr>
            `;
        }

        function renderSessions() {
            const filtered = applyFilters(lastAgents);
            if (filtered.length === 0) {
                containerEl.innerHTML = '<div class="empty">Nessuna sessione corrisponde ai filtri.</div>';
                return;
            }
            const rows = filtered.map(renderAgent).join('');
            containerEl.innerHTML = `
                <table>
                    <thead>
                        <tr>
                            <th>Session ID</th>
                            <th>Provider / Version</th>
                            <th>Started At</th>
                            <th>Working On</th>
                            <th>To Do Past</th>
                            <th>To Do Present</th>
                            <th>To Do Future</th>
                            <th>Space</th>
                            <th>Do Not Touch</th>
                            <th>Status</th>
                            <th>Issues</th>
                            <th>Handoff</th>
                            <th>Azioni</th>
                        </tr>
                    </thead>
                    <tbody>${rows}</tbody>
                </table>
            `;
        }

        function renderLocks() {
            if (!lastLocks.length) {
                locksEl.innerHTML = '<div class="empty">Nessun lock attivo.</div>';
                return;
            }
            const rows = lastLocks.map(l => `
                <tr>
                    <td class="cell-list">${escapeHtml(l.path)}</td>
                    <td>${escapeHtml(l.session_id)}</td>
                    <td>${Math.round(l.age)}s</td>
                    <td>${l.stale ? '<span class="badge badge-stale">stale</span>' : '<span class="badge badge-live">attivo</span>'}</td>
                    <td><button class="action warn" onclick="forceRelease('${escapeHtml(l.path)}', '${escapeHtml(l.session_id)}')">Force release</button></td>
                </tr>
            `).join('');
            locksEl.innerHTML = `
                <table>
                    <thead><tr><th>Path</th><th>Owner</th><th>Age</th><th>Stato</th><th>Azioni</th></tr></thead>
                    <tbody>${rows}</tbody>
                </table>
            `;
        }

        const setupCard = document.getElementById('setup-card');
        const setupUrl = document.getElementById('setup-url');
        const setupBtn = document.getElementById('setup-btn');
        const setupMsg = document.getElementById('setup-msg');
        const setupConfirmPublic = document.getElementById('setup-confirm-public');
        const setupConfirmPublicCheck = document.getElementById('setup-confirm-public-check');
        const setupConfirmMerge = document.getElementById('setup-confirm-merge');
        const setupConfirmMergeCheck = document.getElementById('setup-confirm-merge-check');

        let lastSyncEnabled = false;
        let pendingConfirm = null; // {reason, url}

        function setSetupMsg(text, ok) {
            setupMsg.textContent = text;
            setupMsg.className = 'setup-msg ' + (ok ? 'ok' : 'err');
        }

        function resetSetupConfirm() {
            pendingConfirm = null;
            setupConfirmPublic.classList.add('hidden');
            setupConfirmPublicCheck.checked = false;
            setupConfirmMerge.classList.add('hidden');
            setupConfirmMergeCheck.checked = false;
            setupBtn.textContent = 'Configura multi-macchina';
        }

        function hideSetupCard() {
            setupCard.classList.add('hidden');
        }

        function updateSetupVisibility(sync) {
            const enabled = sync && sync.enabled;
            if (lastSyncEnabled !== enabled) {
                lastSyncEnabled = enabled;
                if (enabled) {
                    hideSetupCard();
                } else {
                    setupCard.classList.remove('hidden');
                }
            }
        }

        async function submitSetup() {
            const url = setupUrl.value.trim();
            if (!url) {
                setSetupMsg('Inserisci un URL remote.', false);
                return;
            }
            const body = { url };
            if (pendingConfirm) {
                if (pendingConfirm.reason === 'public_repo') {
                    if (!setupConfirmPublicCheck.checked) {
                        setSetupMsg('Conferma il repository pubblico per procedere.', false);
                        return;
                    }
                    body.confirm_public = true;
                } else if (pendingConfirm.reason === 'merge_with_local_data') {
                    if (!setupConfirmMergeCheck.checked) {
                        setSetupMsg('Conferma l\'integrazione dei dati locali per procedere.', false);
                        return;
                    }
                    body.confirm_merge = true;
                }
            }
            try {
                const res = await fetch('/api/sync/init', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body)
                });
                const data = await res.json();
                if (data.status === 'needs_confirm') {
                    pendingConfirm = { reason: data.reason, url };
                    if (data.reason === 'public_repo') {
                        setupConfirmPublic.classList.remove('hidden');
                        setupBtn.textContent = 'Conferma e configura';
                        setSetupMsg(data.message + ' Spunta la conferma e riprova.', false);
                    } else if (data.reason === 'merge_with_local_data') {
                        setupConfirmMerge.classList.remove('hidden');
                        setupBtn.textContent = 'Conferma e configura';
                        setSetupMsg(data.message + ' Spunta la conferma e riprova.', false);
                    } else {
                        setSetupMsg('Conferma richiesta: ' + data.message, false);
                    }
                    return;
                }
                if (data.status === 'ok') {
                    resetSetupConfirm();
                    setSetupMsg('Setup completato: ' + data.message, true);
                    hideSetupCard();
                    return;
                }
                // error
                setSetupMsg('Setup fallito: ' + (data.message || res.status), false);
            } catch (err) {
                setSetupMsg('Errore di rete: ' + err, false);
            }
        }

        setupBtn.addEventListener('click', submitSetup);
        setupUrl.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitSetup(); });

        function renderSync(sync) {
            updateSetupVisibility(sync);
            if (!sync || !sync.enabled) {
                syncEl.textContent = 'git-sync: disabilitato';
                syncEl.className = 'status sync-off';
                return;
            }
            let text = 'git-sync: attivo';
            if (sync.last_sync_at) text += ` • ultimo: ${sync.last_sync_at}`;
            if (sync.pending) text += ' • in sospeso';
            if (sync.last_error) text += ` • errore: ${sync.last_error}`;
            syncEl.textContent = text;
            syncEl.className = 'status ' + (sync.last_error ? 'sync-err' : 'sync-ok');
        }

        function render(data) {
            pathEl.textContent = `Registry: ${data.registry_path}`;
            lastAgents = data.agents || [];
            lastLocks = data.locks || [];
            updateProviderOptions(lastAgents);
            renderSessions();
            renderLocks();
            renderSync(data.sync);
        }

        async function killSession(sessionId) {
            const msg = `Kill della sessione "${sessionId}".\n\n` +
                `Se il processo è su questa macchina (PID verificabile) verrà TERMINATO realmente (SIGTERM → SIGKILL).\n` +
                `Altrimenti la sessione sarà solo marcata Killed (stop logico) e i suoi lock rilasciati.\n\nProcedere?`;
            if (!confirm(msg)) return;
            try {
                const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/kill`, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({})
                });
                const data = await res.json();
                if (res.ok) {
                    const azione = data.action === 'process_terminated'
                        ? 'processo terminato realmente'
                        : 'stop logico (processo non su questa macchina o non verificabile)';
                    showNotice(`Sessione ${sessionId}: Killed — ${azione}. ${data.note || ''}`, true);
                } else {
                    showNotice(`Kill fallito: ${data.error || res.status}`, false);
                }
            } catch (err) {
                showNotice(`Kill fallito: ${err}`, false);
            }
        }

        async function forceRelease(path, sessionId) {
            await forceReleaseAttempt(path, sessionId, false);
        }

        async function forceReleaseAttempt(path, sessionId, confirmed) {
            try {
                const res = await fetch('/api/locks/force-release', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({path, session_id: sessionId, confirm: confirmed})
                });
                const data = await res.json();
                if (res.status === 409 && data.warning) {
                    if (confirm(`⚠️ ${data.warning}\n\nForzare il rilascio?`)) {
                        await forceReleaseAttempt(path, sessionId, true);
                    }
                    return;
                }
                if (res.ok && data.released !== false) {
                    showNotice(`Lock rilasciato: ${path} (owner: ${data.owner || sessionId})`, true);
                } else {
                    showNotice(`Force release fallito: ${data.error || res.status}`, false);
                }
            } catch (err) {
                showNotice(`Force release fallito: ${err}`, false);
            }
        }

        document.getElementById('cleanup-btn').addEventListener('click', async () => {
            if (!confirm('Eseguire il cleanup delle sessioni zombie?\n\nLe sessioni OnWorking con PID morto o lock tutti stale saranno marcate Stop e i loro lock rilasciati.')) return;
            try {
                const res = await fetch('/api/cleanup', {method: 'POST'});
                const data = await res.json();
                if (res.ok) {
                    showNotice(`Cleanup completato: ${data.count} sessioni marcate Stop${data.stopped && data.stopped.length ? ' (' + data.stopped.join(', ') + ')' : ''}.`, true);
                } else {
                    showNotice(`Cleanup fallito: ${res.status}`, false);
                }
            } catch (err) {
                showNotice(`Cleanup fallito: ${err}`, false);
            }
        });

        // --- Tab ---
        const tabSessions = document.getElementById('tab-sessions');
        const tabWiki = document.getElementById('tab-wiki');
        const viewSessions = document.getElementById('view-sessions');
        const viewWiki = document.getElementById('view-wiki');

        tabSessions.addEventListener('click', () => {
            tabSessions.classList.add('active');
            tabWiki.classList.remove('active');
            viewSessions.style.display = '';
            viewWiki.style.display = 'none';
        });
        tabWiki.addEventListener('click', () => {
            tabWiki.classList.add('active');
            tabSessions.classList.remove('active');
            viewSessions.style.display = 'none';
            viewWiki.style.display = '';
            loadWiki();
        });

        // --- Filtri (ri-render immediato sul payload già ricevuto) ---
        dateEl.addEventListener('change', renderSessions);
        providerEl.addEventListener('change', renderSessions);
        statusFilterEl.addEventListener('change', renderSessions);

        // --- Wiki ---
        const wikiContainer = document.getElementById('wiki-container');
        const wikiQ = document.getElementById('wiki-q');

        function fmtBugs(bugs) {
            if (!Array.isArray(bugs) || !bugs.length) return '—';
            return bugs.join('; ');
        }

        async function loadWiki() {
            wikiContainer.innerHTML = '<div class="empty">Caricamento in corso...</div>';
            try {
                const q = wikiQ.value.trim();
                const res = await fetch(`/api/wiki?q=${encodeURIComponent(q)}&limit=100`);
                const data = await res.json();
                renderWiki(data.entries || []);
            } catch (err) {
                wikiContainer.innerHTML = `<div class="empty">Errore: ${escapeHtml(String(err))}</div>`;
            }
        }

        function renderWiki(entries) {
            if (!entries.length) {
                wikiContainer.innerHTML = '<div class="empty">Nessuna entry wiki trovata.</div>';
                return;
            }
            const rows = entries.map(e => `
                <tr class="wiki-row" onclick="toggleWikiDetail(${e.id}, this)">
                    <td>${e.id}</td>
                    <td>${escapeHtml(e.provider)}</td>
                    <td>${escapeHtml(e.modello)}</td>
                    <td>${escapeHtml(e.data)}</td>
                    <td class="cell-list">${escapeHtml(e.router)}</td>
                    <td class="cell-list">${escapeHtml(fmtBugs(e.bug_trovati))}</td>
                    <td>${badge(e.status === 'ok' ? 'Finished' : 'Stop')}</td>
                </tr>
            `).join('');
            wikiContainer.innerHTML = `
                <table>
                    <thead>
                        <tr><th>ID</th><th>Provider</th><th>Modello</th><th>Data</th><th>Router</th><th>Bug trovati</th><th>Status</th></tr>
                    </thead>
                    <tbody>${rows}</tbody>
                </table>
            `;
        }

        async function toggleWikiDetail(entryId, rowEl) {
            const next = rowEl.nextElementSibling;
            if (next && next.classList.contains('detail-row')) {
                next.remove();
                return;
            }
            const detailRow = document.createElement('tr');
            detailRow.className = 'detail-row';
            detailRow.innerHTML = '<td colspan="7">Caricamento dettaglio...</td>';
            rowEl.after(detailRow);
            try {
                const res = await fetch(`/api/wiki/${entryId}`);
                const e = await res.json();
                if (!res.ok) {
                    detailRow.innerHTML = `<td colspan="7">Errore: ${escapeHtml(e.error || res.status)}</td>`;
                    return;
                }
                const fmt = (v) => Array.isArray(v) ? (v.length ? v.join('\n') : '—') : (v || '—');
                detailRow.innerHTML = `
                    <td colspan="7">
                        <dl class="detail-grid">
                            <dt>Session ID</dt><dd>${escapeHtml(e.session_id)}</dd>
                            <dt>Provider / Modello</dt><dd>${escapeHtml(e.provider)} / ${escapeHtml(e.modello)}</dd>
                            <dt>Data</dt><dd>${escapeHtml(e.data)}</dd>
                            <dt>Router</dt><dd>${escapeHtml(e.router)}</dd>
                            <dt>Cosa fatto</dt><dd>${escapeHtml(e.cosa_fatto)}</dd>
                            <dt>Come fatto</dt><dd>${escapeHtml(e.come_fatto)}</dd>
                            <dt>Problema risolto</dt><dd>${escapeHtml(e.problema_risolto)}</dd>
                            <dt>File toccati</dt><dd>${escapeHtml(fmt(e.file_toccati))}</dd>
                            <dt>Git push</dt><dd>${escapeHtml(fmt(e.git_push))}</dd>
                            <dt>Handoff</dt><dd>${escapeHtml(fmt(e.handoff))}</dd>
                            <dt>Bug trovati</dt><dd>${escapeHtml(fmt(e.bug_trovati))}</dd>
                            <dt>Skill/Tool/MCP</dt><dd>${escapeHtml(fmt(e.skill_tool_mcp))}</dd>
                            <dt>Status</dt><dd>${escapeHtml(e.status)}</dd>
                            <dt>Context file</dt><dd>${escapeHtml(e.context_md || '—')}</dd>
                        </dl>
                    </td>
                `;
            } catch (err) {
                detailRow.innerHTML = `<td colspan="7">Errore: ${escapeHtml(String(err))}</td>`;
            }
        }

        document.getElementById('wiki-search-btn').addEventListener('click', loadWiki);
        wikiQ.addEventListener('keydown', (e) => { if (e.key === 'Enter') loadWiki(); });

        // --- SSE live ---
        function connect() {
            const es = new EventSource('/api/registry/stream');

            es.onopen = () => {
                statusEl.textContent = 'Connesso • aggiornamento live';
                statusEl.className = 'status connected';
            };

            es.onmessage = (event) => {
                try {
                    const data = JSON.parse(event.data);
                    if (data.error) {
                        console.error('Registry error:', data.error);
                        return;
                    }
                    render(data);
                } catch (err) {
                    console.error('Parse error:', err);
                }
            };

            es.onerror = () => {
                statusEl.textContent = 'Disconnesso • riconnessione...';
                statusEl.className = 'status disconnected';
                es.close();
                setTimeout(connect, 3000);
            };
        }

        connect();
    </script>
</body>
</html>

README.md

requirements-dev.txt

SKILL.md

SPEC_SETUP.md

tile.json