289 lines
12 KiB
JavaScript
289 lines
12 KiB
JavaScript
const KeysPage = (() => {
|
|
let _keys = [];
|
|
|
|
async function render(container) {
|
|
container.innerHTML = `<div class="page"><div class="loading">Loading…</div></div>`;
|
|
await load(container);
|
|
}
|
|
|
|
async function load(container) {
|
|
try {
|
|
_keys = await Api.get('/api/keys');
|
|
container.innerHTML = `
|
|
<div class="page">
|
|
<div class="section-header">
|
|
<h2>API Keys</h2>
|
|
<button class="btn btn-primary btn-sm" id="btn-new-key">+ New Key</button>
|
|
</div>
|
|
|
|
<div class="table-card" style="margin-bottom:24px">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Label</th>
|
|
<th>Prefix</th>
|
|
<th>Last Used</th>
|
|
<th>Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${_keys.length ? _keys.map(k => `
|
|
<tr>
|
|
<td><strong>${k.label}</strong></td>
|
|
<td><code style="color:var(--accent);font-size:12px">${k.key_prefix}…</code></td>
|
|
<td style="color:var(--text-muted);font-size:12px">${k.last_used_at ? new Date(k.last_used_at).toLocaleString() : 'Never'}</td>
|
|
<td>${k.is_active
|
|
? '<span class="badge badge-success">Active</span>'
|
|
: '<span class="badge badge-muted">Revoked</span>'
|
|
}</td>
|
|
<td style="display:flex;gap:6px;justify-content:flex-end">
|
|
${k.is_active
|
|
? `<button class="btn btn-ghost btn-sm btn-revoke" data-id="${k.id}">Revoke</button>`
|
|
: ''
|
|
}
|
|
<button class="btn btn-danger btn-sm btn-delete" data-id="${k.id}">Delete</button>
|
|
</td>
|
|
</tr>
|
|
`).join('') : '<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:20px">No API keys yet</td></tr>'}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New key modal -->
|
|
<div class="modal-overlay" id="new-key-modal">
|
|
<div class="modal">
|
|
<h2>Create API Key</h2>
|
|
|
|
<!-- Step 1: label input -->
|
|
<div id="step-create">
|
|
<div class="form-group">
|
|
<label>Label</label>
|
|
<input type="text" id="key-label" placeholder="Mac mini, Work Laptop…" value="My Machine">
|
|
</div>
|
|
<div id="key-error" class="error-msg"></div>
|
|
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
|
|
<button class="btn btn-ghost" id="btn-cancel-key">Cancel</button>
|
|
<button class="btn btn-primary" id="btn-create-key">Create Key</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: download setup script -->
|
|
<div id="step-done" style="display:none">
|
|
<div style="
|
|
background:rgba(34,197,94,.08);
|
|
border:1px solid rgba(34,197,94,.25);
|
|
border-radius:var(--radius-sm);
|
|
padding:14px 16px;
|
|
margin-bottom:20px;
|
|
font-size:13px;
|
|
">
|
|
<strong style="color:var(--success)">Key created successfully.</strong><br>
|
|
<span style="color:var(--text-muted);font-size:12px">
|
|
Download the setup script below — it contains your API key and will configure everything automatically.
|
|
</span>
|
|
</div>
|
|
|
|
<div style="margin-bottom:20px">
|
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text-muted);margin-bottom:10px">
|
|
What the script does:
|
|
</div>
|
|
<ol style="font-size:12px;color:var(--text-muted);line-height:1.8;padding-left:18px">
|
|
<li>Asks you for your <strong style="color:var(--text)">projects root folder</strong></li>
|
|
<li>Downloads <code style="color:var(--accent)">cc-collector.py</code> to <code style="color:var(--accent)">~/.claude/</code></li>
|
|
<li>Adds the Claude Code Stop hook to your settings</li>
|
|
<li>Tests the connection to confirm everything works</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 14px;font-size:11px;color:var(--text-muted);margin-bottom:20px;font-family:'Courier New',monospace;">
|
|
curl -s <span id="script-url-preview" style="color:var(--accent)"></span> | bash
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
<button class="btn btn-ghost" id="btn-done-key">Close</button>
|
|
<button class="btn btn-primary" id="btn-download-script">
|
|
⬇ Download Setup Script
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// ── New key button ──
|
|
document.getElementById('btn-new-key').onclick = () => {
|
|
document.getElementById('step-create').style.display = '';
|
|
document.getElementById('step-done').style.display = 'none';
|
|
document.getElementById('key-error').style.display = 'none';
|
|
document.getElementById('key-label').value = 'My Machine';
|
|
document.getElementById('new-key-modal').classList.add('open');
|
|
};
|
|
|
|
document.getElementById('btn-cancel-key').onclick = () => {
|
|
document.getElementById('new-key-modal').classList.remove('open');
|
|
};
|
|
|
|
// ── Create key ──
|
|
document.getElementById('btn-create-key').onclick = async () => {
|
|
const label = document.getElementById('key-label').value.trim() || 'My Machine';
|
|
const errEl = document.getElementById('key-error');
|
|
errEl.style.display = 'none';
|
|
try {
|
|
const key = await Api.post('/api/keys', { label });
|
|
document.getElementById('step-create').style.display = 'none';
|
|
document.getElementById('step-done').style.display = '';
|
|
_setupDownloadStep(key.raw_key);
|
|
} catch (e) {
|
|
errEl.textContent = e.message;
|
|
errEl.style.display = 'block';
|
|
}
|
|
};
|
|
|
|
document.getElementById('btn-done-key').onclick = async () => {
|
|
document.getElementById('new-key-modal').classList.remove('open');
|
|
await load(container); // refresh table after modal closes
|
|
};
|
|
|
|
// ── Revoke buttons ──
|
|
container.querySelectorAll('.btn-revoke').forEach(btn => {
|
|
btn.onclick = async () => {
|
|
if (!confirm('Revoke this key? It will stop working immediately.')) return;
|
|
try {
|
|
await Api.patch(`/api/keys/${btn.dataset.id}/revoke`);
|
|
await load(container);
|
|
} catch (e) { alert(e.message); }
|
|
};
|
|
});
|
|
|
|
// ── Delete buttons ──
|
|
container.querySelectorAll('.btn-delete').forEach(btn => {
|
|
btn.onclick = async () => {
|
|
if (!confirm('Permanently delete this key? This cannot be undone.')) return;
|
|
try {
|
|
await Api.del(`/api/keys/${btn.dataset.id}`);
|
|
await load(container);
|
|
} catch (e) { alert(e.message); }
|
|
};
|
|
});
|
|
|
|
} catch (e) {
|
|
container.innerHTML = `<div class="page"><div class="empty"><div class="big">⚠️</div>${e.message}</div></div>`;
|
|
}
|
|
}
|
|
|
|
// Build setup script content with embedded API key
|
|
function _buildSetupScript(rawKey) {
|
|
const server = window.location.origin + '/cc-dashboard';
|
|
const collectorUrl = server + '/static/collector/cc-collector.py';
|
|
|
|
// Use array join to avoid JS template literal conflicts with bash ${VAR} syntax
|
|
const L = [
|
|
'#!/usr/bin/env bash',
|
|
'# ============================================================',
|
|
'# CC Dashboard — Setup Script',
|
|
'# Auto-generated — contains your API key — run once',
|
|
'# ============================================================',
|
|
'set -euo pipefail',
|
|
'',
|
|
'CC_API_KEY="' + rawKey + '"',
|
|
'CC_SERVER="' + server + '"',
|
|
'COLLECTOR_URL="' + collectorUrl + '"',
|
|
'CLAUDE_DIR="$HOME/.claude"',
|
|
'COLLECTOR_PATH="$CLAUDE_DIR/cc-collector.py"',
|
|
'SETTINGS_PATH="$CLAUDE_DIR/settings.json"',
|
|
'',
|
|
'echo ""',
|
|
'echo " CC Dashboard Setup"',
|
|
'echo " ─────────────────────────────────────────────"',
|
|
'echo ""',
|
|
'',
|
|
'# Ask for projects root folder',
|
|
'read -rp " Enter your projects root folder (e.g. ~/Projects): " PROJECTS_ROOT',
|
|
'# Expand ~ manually',
|
|
'PROJECTS_ROOT="${PROJECTS_ROOT/#\\~/$HOME}"',
|
|
'',
|
|
'if [ ! -d "$PROJECTS_ROOT" ]; then',
|
|
' echo " Warning: directory not found, creating it..."',
|
|
' mkdir -p "$PROJECTS_ROOT"',
|
|
'fi',
|
|
'',
|
|
'echo ""',
|
|
'echo " Using: $PROJECTS_ROOT"',
|
|
'echo ""',
|
|
'',
|
|
'# Download cc-collector.py',
|
|
'echo " Downloading cc-collector.py..."',
|
|
'mkdir -p "$CLAUDE_DIR"',
|
|
'curl -fsSL "$COLLECTOR_URL" -o "$COLLECTOR_PATH"',
|
|
'chmod +x "$COLLECTOR_PATH"',
|
|
'echo " OK: saved to $COLLECTOR_PATH"',
|
|
'',
|
|
'# Build hook command',
|
|
'HOOK_CMD="CC_API_KEY=$CC_API_KEY CC_SERVER=$CC_SERVER CC_ROOT_PATH=$PROJECTS_ROOT python3 $COLLECTOR_PATH 2>/dev/null || true"',
|
|
'',
|
|
'# Update Claude Code settings.json',
|
|
'if [ -f "$SETTINGS_PATH" ]; then',
|
|
' cp "$SETTINGS_PATH" "$SETTINGS_PATH.bak"',
|
|
' python3 -c "',
|
|
'import json, sys',
|
|
'path, cmd = sys.argv[1], sys.argv[2]',
|
|
'with open(path) as f: cfg = json.load(f)',
|
|
'entry = {\"hooks\": [{\"type\": \"command\", \"command\": cmd, \"async\": True, \"statusMessage\": \"Syncing to CC Dashboard...\"}]}',
|
|
'cfg.setdefault(\"hooks\", {}).setdefault(\"Stop\", [])',
|
|
'cfg[\"hooks\"][\"Stop\"] = [h for h in cfg[\"hooks\"][\"Stop\"] if \"cc-collector\" not in str(h)]',
|
|
'cfg[\"hooks\"][\"Stop\"].append(entry)',
|
|
'open(path, \"w\").write(json.dumps(cfg, indent=2))',
|
|
'" -- "$SETTINGS_PATH" "$HOOK_CMD"',
|
|
' echo " OK: merged hook into $SETTINGS_PATH (backup saved)"',
|
|
'else',
|
|
' python3 -c "',
|
|
'import json, sys',
|
|
'cmd = sys.argv[1]',
|
|
'cfg = {\"hooks\": {\"Stop\": [{\"hooks\": [{\"type\": \"command\", \"command\": cmd, \"async\": True, \"statusMessage\": \"Syncing to CC Dashboard...\"}]}]}}',
|
|
'open(sys.argv[2], \"w\").write(json.dumps(cfg, indent=2))',
|
|
'" -- "$HOOK_CMD" "$SETTINGS_PATH"',
|
|
' echo " OK: created $SETTINGS_PATH"',
|
|
'fi',
|
|
'',
|
|
'# Test connection',
|
|
'echo ""',
|
|
'echo " Testing connection..."',
|
|
'HTTP=$(curl -s -o /dev/null -w "%{http_code}" \\',
|
|
' -H "X-API-Key: $CC_API_KEY" \\',
|
|
' "$CC_SERVER/api/ingest" \\',
|
|
' -X POST -H "Content-Type: application/json" -d "[]" 2>/dev/null || echo 000)',
|
|
'if [ "$HTTP" = "204" ] || [ "$HTTP" = "200" ]; then',
|
|
' echo " OK: connection successful"',
|
|
'else',
|
|
' echo " Note: could not reach server (HTTP $HTTP) — hook will retry on next session"',
|
|
'fi',
|
|
'',
|
|
'echo ""',
|
|
'echo " ─────────────────────────────────────────────"',
|
|
'echo " Setup complete! Open Claude Code to start tracking."',
|
|
'echo ""',
|
|
];
|
|
return L.join('\n');
|
|
}
|
|
|
|
|
|
function _setupDownloadStep(rawKey) {
|
|
const server = window.location.origin + '/cc-dashboard';
|
|
const previewEl = document.getElementById('script-url-preview');
|
|
if (previewEl) previewEl.textContent = server + '/setup';
|
|
|
|
document.getElementById('btn-download-script').onclick = () => {
|
|
const script = _buildSetupScript(rawKey);
|
|
const blob = new Blob([script], { type: 'text/x-shellscript' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'cc-dashboard-setup.sh';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
};
|
|
}
|
|
|
|
return { render };
|
|
})();
|