OSVauco/static/command-hub.html
2026-05-27 21:42:38 +00:00

2887 lines
116 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VAUCO OS — Multi-Thread Hub for Perplexity v3.3</title>
<link rel="preconnect" href="https://api.fontshare.com">
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #1a1a1a;
--panel: #111111;
--panel-hover: #161616;
--line: #242424;
--line2: #2e2e2e;
--text: #e4e2dc;
--text-dim: rgba(255,255,255,0.5);
--text-faint: rgba(255,255,255,0.3);
--accent: #10B981;
--accent2: #D97706;
--danger: #e05050;
--plan: #a78bfa;
--idea: #a78bfa;
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--sans: 'Satoshi', 'Inter', sans-serif;
/* Thread accent palette */
--th-ops: #20b2aa;
--th-plan: #8ab4f8;
--th-nmt: #b39ddb;
--th-exec: #e6a817;
}
body {
font-family: var(--mono);
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 22px;
font-size: 14px;
line-height: 1.4;
}
/* ── HEADER ─────────────────────────────────────────────────── */
header {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
padding: 22px 28px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.brand h1 {
font-family: var(--sans);
color: var(--accent);
font-size: 22px;
letter-spacing: 1.5px;
font-weight: 700;
}
.brand .subtitle {
color: var(--text-dim);
font-size: 12px;
margin-top: 4px;
letter-spacing: 0.5px;
}
.header-controls { display: flex; gap: 10px; align-items: center; }
.header-btn {
background: rgba(255,255,255,0.03);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.06);
color: var(--text-dim);
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.5px;
transition: all 0.15s ease;
}
.header-btn:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.stats { display: flex; gap: 18px; font-size: 11px; color: var(--text-faint); letter-spacing: 0.5px; }
.stats span strong { color: var(--accent); font-size: 13px; font-weight: 700; }
/* ── THREAD TABS ─────────────────────────────────────────────── */
.thread-bar {
display: flex;
gap: 6px;
margin-bottom: 16px;
align-items: center;
flex-wrap: wrap;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 14px;
}
.thread-tab {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255,255,255,0.02);
border: 1px solid var(--line2);
border-left: 3px solid var(--text-faint);
color: var(--text-dim);
padding: 7px 14px;
border-radius: 8px;
cursor: pointer;
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.5px;
font-weight: 700;
transition: all 0.15s ease;
position: relative;
}
.thread-tab:hover { background: rgba(255,255,255,0.05); color: var(--text); }
.thread-tab.active {
background: rgba(255,255,255,0.06);
color: var(--text);
}
.thread-tab .badge {
background: rgba(255,255,255,0.08);
color: var(--text-faint);
padding: 1px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
}
.thread-tab .tab-close {
background: none;
border: none;
color: var(--text-faint);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
margin-left: 2px;
}
.thread-tab .tab-close:hover { color: var(--danger); }
.thread-add {
background: rgba(167,139,250,0.1);
border: 1px dashed rgba(167,139,250,0.4);
color: var(--idea);
padding: 7px 14px;
border-radius: 8px;
cursor: pointer;
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.5px;
font-weight: 700;
transition: all 0.15s ease;
}
.thread-add:hover { background: rgba(167,139,250,0.18); }
/* ── MODAL ───────────────────────────────────────────────────── */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
}
.modal-backdrop.show { display: flex; }
.modal {
background: var(--panel);
border: 1px solid var(--line2);
border-radius: 12px;
padding: 26px;
width: 460px;
max-width: 90vw;
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.modal h3 {
font-family: var(--sans);
color: var(--idea);
font-size: 14px;
letter-spacing: 1.2px;
margin-bottom: 14px;
}
.modal label {
display: block;
color: var(--text-dim);
font-size: 10px;
letter-spacing: 0.8px;
margin-bottom: 6px;
margin-top: 12px;
}
.modal input {
width: 100%;
background: var(--bg);
border: 1px solid var(--line2);
border-radius: 8px;
padding: 10px 12px;
color: var(--text);
font-family: var(--mono);
font-size: 13px;
}
.modal input:focus { outline: none; border-color: rgba(167,139,250,0.5); }
.modal .generated-name {
background: var(--bg);
border: 1px solid rgba(167,139,250,0.3);
border-radius: 8px;
padding: 10px 12px;
color: var(--idea);
font-weight: 700;
font-size: 14px;
letter-spacing: 0.5px;
margin-top: 4px;
}
.modal .modal-hint {
color: var(--text-faint);
font-size: 10px;
margin-top: 6px;
letter-spacing: 0.4px;
}
.modal-actions {
display: flex;
gap: 8px;
margin-top: 22px;
justify-content: flex-end;
}
.modal-btn {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
color: var(--text-dim);
padding: 9px 18px;
border-radius: 8px;
cursor: pointer;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.5px;
font-weight: 700;
}
.modal-btn:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.modal-btn.primary {
background: rgba(167,139,250,0.15);
border-color: rgba(167,139,250,0.4);
color: var(--idea);
}
.modal-btn.primary:hover { background: rgba(167,139,250,0.25); }
.color-row {
display: flex;
gap: 6px;
margin-top: 6px;
}
.color-dot {
width: 24px;
height: 24px;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.15s ease;
}
.color-dot.active { border-color: var(--text); transform: scale(1.1); }
/* ── TOAST ───────────────────────────────────────────────────── */
.toast {
position: fixed; top: 22px; right: 22px;
background: rgba(16,185,129,0.95);
color: #0a0a0a;
padding: 12px 22px;
border-radius: 8px;
font-weight: 700;
opacity: 0;
transform: translateY(-10px);
transition: all 0.25s ease;
pointer-events: none;
z-index: 300;
box-shadow: 0 8px 28px rgba(0,0,0,0.5);
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.5px;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* ── PLAN ANCHOR ─────────────────────────────────────────────── */
.plan-anchor {
background: rgba(16, 185, 129, 0.04);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 12px;
padding: 22px 26px;
margin-bottom: 20px;
box-shadow: 0 0 24px rgba(16, 185, 129, 0.06);
position: relative;
}
.plan-anchor.animate { animation: pulse 4s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 24px rgba(16,185,129,0.06); }
50% { box-shadow: 0 0 32px rgba(16,185,129,0.14); }
}
.plan-anchor h2 {
color: var(--accent);
font-size: 11px;
margin-bottom: 14px;
letter-spacing: 2px;
opacity: 0.9;
}
.current-task { display: flex; align-items: center; gap: 16px; margin-bottom: 8px; }
.task-id {
background: rgba(16,185,129,0.15);
color: var(--accent);
padding: 6px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.5px;
border: 1px solid rgba(16,185,129,0.3);
}
.task-name { font-size: 16px; font-weight: 700; color: var(--text); }
.task-meta { font-size: 11px; color: var(--text-dim); margin-top: 4px; letter-spacing: 0.3px; }
.progress-track {
background: var(--line2);
height: 3px;
border-radius: 2px;
margin: 14px 0 12px;
overflow: hidden;
}
.progress-fill {
background: var(--accent);
height: 100%;
border-radius: 2px;
transition: width 0.6s cubic-bezier(0.4,0,0.2,1);
box-shadow: 0 0 8px rgba(16,185,129,0.4);
}
.progress-label {
font-size: 10px;
color: var(--text-faint);
letter-spacing: 0.5px;
display: flex;
justify-content: space-between;
}
.plan-controls { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.plan-btn {
background: rgba(255,255,255,0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 8px;
padding: 9px 16px;
color: var(--text-dim);
cursor: pointer;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.5px;
transition: all 0.15s ease;
}
.plan-btn:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.plan-btn.done { color: var(--accent); border-color: rgba(16,185,129,0.3); }
.plan-btn.done:hover { background: rgba(16,185,129,0.08); }
.plan-btn.skip { color: var(--accent2); border-color: rgba(217,119,6,0.3); }
.plan-btn.skip:hover { background: rgba(217,119,6,0.08); }
.plan-btn.danger { color: var(--danger); border-color: rgba(224,80,80,0.3); }
.plan-btn.danger:hover { background: rgba(224,80,80,0.08); }
.plan-btn.boot { color: var(--plan); border-color: rgba(167,139,250,0.3); }
.plan-btn.boot:hover { background: rgba(167,139,250,0.08); }
/* ── HITL BAR ────────────────────────────────────────────────── */
.hitl-bar {
background: rgba(180, 40, 40, 0.08);
border: 1px solid rgba(180, 40, 40, 0.18);
border-radius: 8px;
padding: 10px 18px;
margin-bottom: 20px;
font-size: 10px;
color: #cc4444;
letter-spacing: 1px;
text-align: center;
}
.hitl-bar strong { color: #e05050; }
/* ── LAYOUT ──────────────────────────────────────────────────── */
.layout { display: grid; grid-template-columns: 1fr 400px; gap: 20px; }
@media (max-width: 1280px) { .layout { grid-template-columns: 1fr; } }
/* ── SEARCH ──────────────────────────────────────────────────── */
.search {
width: 100%;
padding: 13px 20px;
background: var(--panel);
border: 1px solid var(--line2);
border-radius: 10px;
color: var(--text);
font-family: var(--mono);
font-size: 13px;
margin-bottom: 18px;
letter-spacing: 0.3px;
}
.search:focus { outline: none; border-color: rgba(16,185,129,0.4); background: var(--panel-hover); }
.search::placeholder { color: var(--text-faint); }
/* ── SECTIONS & BUTTONS ──────────────────────────────────────── */
section { margin-bottom: 26px; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.section-header h2 {
font-size: 12px;
color: var(--accent2);
letter-spacing: 1.5px;
font-weight: 700;
}
.section-header .section-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
display: inline-block;
margin-right: 8px;
vertical-align: middle;
}
.section-header .section-dot.warn { background: var(--accent2); }
.section-header .section-dot.danger { background: var(--danger); }
.section-header .section-dot.plan { background: var(--plan); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
body.compact .btn { padding: 9px 14px; }
body.compact .btn .desc { display: none; }
.btn {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.06);
border-left: 2px solid var(--accent);
border-radius: 8px;
padding: 13px 16px 13px 18px;
cursor: pointer;
text-align: left;
transition: all 0.18s cubic-bezier(0.4,0,0.2,1);
color: var(--text);
font-family: var(--mono);
display: flex;
flex-direction: column;
gap: 4px;
position: relative;
box-shadow: -2px 0 10px rgba(16,185,129,0.15);
}
.btn:hover { background: rgba(255,255,255,0.06); transform: translateY(-1px); }
.btn:active { transform: scale(0.98); }
.btn.warn { border-left-color: var(--accent2); box-shadow: -2px 0 10px rgba(217,119,6,0.15); }
.btn.danger { border-left-color: var(--danger); box-shadow: -2px 0 10px rgba(224,80,80,0.15); }
.btn.plan { border-left-color: var(--plan); box-shadow: -2px 0 10px rgba(167,139,250,0.15); }
.btn .cmd { font-size: 14px; font-weight: 700; color: rgba(16,185,129,0.9); letter-spacing: 0.3px; }
.btn.warn .cmd { color: rgba(217,119,6,0.9); }
.btn.danger .cmd { color: rgba(224,80,80,0.9); }
.btn.plan .cmd { color: rgba(167,139,250,0.9); }
.btn .desc { font-size: 11px; color: var(--text-dim); letter-spacing: 0.2px; }
.fav-star {
position: absolute;
top: 8px; right: 10px;
background: none; border: none;
color: var(--text-faint);
cursor: pointer;
font-size: 14px;
padding: 2px;
transition: all 0.15s ease;
}
.fav-star:hover { transform: scale(1.2); }
.fav-star.active { color: var(--accent2); }
.copy-check {
position: absolute;
top: 8px; right: 28px;
color: var(--accent);
opacity: 0;
font-size: 12px;
transition: opacity 0.2s ease;
}
.copy-check.show { opacity: 1; animation: checkBounce 0.4s ease; }
@keyframes checkBounce {
0% { transform: scale(0); }
60% { transform: scale(1.3); }
100% { transform: scale(1); }
}
/* ── SUGGESTIONS PANEL ───────────────────────────────────────── */
.suggestions-panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
padding: 22px;
height: fit-content;
position: sticky;
top: 22px;
max-height: calc(100vh - 44px);
overflow-y: auto;
}
.panel-context {
background: rgba(255,255,255,0.02);
border-radius: 6px;
padding: 8px 10px;
font-size: 10px;
color: var(--text-faint);
letter-spacing: 0.5px;
margin-bottom: 12px;
border-left: 2px solid var(--text-faint);
}
.tab-row {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--line2);
}
.tab-btn {
background: none; border: none;
color: var(--text-faint);
padding: 8px 12px;
cursor: pointer;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 1px;
border-bottom: 2px solid transparent;
transition: all 0.15s ease;
}
.tab-btn:hover { color: var(--text-dim); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.idea-input {
width: 100%;
background: var(--bg);
border: 1px solid var(--line2);
border-radius: 8px;
padding: 12px;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
resize: vertical;
min-height: 70px;
margin-bottom: 8px;
}
.idea-input:focus { outline: none; border-color: rgba(167,139,250,0.5); }
.category-row { display: flex; gap: 4px; margin-bottom: 10px; flex-wrap: wrap; }
.cat-btn {
flex: 1; min-width: 60px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 6px;
padding: 7px 6px;
color: var(--text-dim);
cursor: pointer;
font-family: var(--mono);
font-size: 10px;
transition: all 0.15s ease;
text-align: center;
letter-spacing: 0.5px;
}
.cat-btn:hover { color: var(--text); }
.cat-btn.active {
background: rgba(167,139,250,0.15);
color: var(--idea);
border-color: rgba(167,139,250,0.4);
}
.add-btn {
width: 100%;
background: rgba(167,139,250,0.15);
color: var(--idea);
border: 1px solid rgba(167,139,250,0.3);
border-radius: 8px;
padding: 10px;
cursor: pointer;
font-family: var(--mono);
font-weight: 700;
font-size: 12px;
margin-bottom: 14px;
letter-spacing: 0.5px;
transition: all 0.15s ease;
}
.add-btn:hover { background: rgba(167,139,250,0.25); }
.idea-list, .history-list, .plan-list {
display: flex; flex-direction: column; gap: 8px;
max-height: 380px; overflow-y: auto;
}
.idea-item, .history-item, .plan-list-item {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-left: 2px solid var(--idea);
border-radius: 8px;
padding: 10px 12px;
font-size: 11px;
position: relative;
}
.idea-item.cat-roadmap { border-left-color: var(--accent); }
.idea-item.cat-arch { border-left-color: var(--plan); }
.idea-item.cat-risk { border-left-color: var(--danger); }
.idea-item.cat-bug { border-left-color: var(--accent2); }
.idea-item.cat-note { border-left-color: var(--text-faint); }
.idea-meta {
font-size: 9px;
color: var(--text-faint);
margin-bottom: 4px;
display: flex;
justify-content: space-between;
letter-spacing: 0.5px;
}
.idea-text { color: var(--text); line-height: 1.4; }
.idea-del {
position: absolute;
top: 6px; right: 8px;
background: none; border: none;
color: var(--text-faint);
cursor: pointer; font-size: 14px;
}
.idea-del:hover { color: var(--danger); }
.plan-list-item.done { opacity: 0.5; border-left-color: var(--text-faint); }
.plan-list-item.done .plan-list-name { text-decoration: line-through; }
.plan-list-item.current {
background: rgba(16,185,129,0.08);
border-left-color: var(--accent);
border-color: rgba(16,185,129,0.2);
}
.plan-list-id { font-size: 9px; color: var(--text-faint); letter-spacing: 0.5px; }
.plan-list-name { font-size: 11px; color: var(--text); margin-top: 2px; }
.plan-list-meta { font-size: 9px; color: var(--text-faint); margin-top: 4px; letter-spacing: 0.3px; }
.history-item { border-left-color: var(--text-faint); cursor: pointer; }
.history-item:hover { background: rgba(255,255,255,0.06); }
.history-cmd { font-family: var(--mono); color: var(--accent); font-size: 11px; font-weight: 700; }
.history-time { font-size: 9px; color: var(--text-faint); letter-spacing: 0.5px; }
.export-row { display: flex; gap: 6px; margin-top: 12px; }
.export-btn {
flex: 1;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 6px;
padding: 8px;
color: var(--text-dim);
cursor: pointer;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.5px;
transition: all 0.15s ease;
}
.export-btn:hover { background: rgba(167,139,250,0.1); color: var(--idea); }
.quick-add-row { display: flex; gap: 6px; margin-bottom: 12px; }
.quick-add-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--line2);
border-radius: 6px;
padding: 8px 10px;
color: var(--text);
font-family: var(--mono);
font-size: 11px;
}
.quick-add-input:focus { outline: none; border-color: rgba(16,185,129,0.4); }
.favorites-section { margin-bottom: 26px; }
.favorites-section .grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.footer {
margin-top: 28px;
padding: 16px 20px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
font-size: 11px;
color: var(--text-dim);
line-height: 1.7;
letter-spacing: 0.3px;
}
.footer strong { color: var(--accent2); }
kbd {
background: var(--bg);
border: 1px solid var(--line2);
border-radius: 4px;
padding: 2px 6px;
font-size: 10px;
color: var(--accent);
font-family: var(--mono);
}
body.light {
--bg: #f5f3ed;
--panel: #ffffff;
--panel-hover: #fafaf7;
--line: #e0ddd5;
--line2: #d0cdc3;
--text: #1a1a1a;
--text-dim: rgba(0,0,0,0.55);
--text-faint: rgba(0,0,0,0.35);
}
body.light .btn { background: rgba(0,0,0,0.02); border-color: rgba(0,0,0,0.06); }
body.light .btn:hover { background: rgba(0,0,0,0.04); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line2); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
/* ── ADMIN MODE ──────────────────────────────────────────────── */
.admin-only { display: none !important; }
body.admin-mode .admin-only { display: flex !important; }
/* ── TASR MODAL ─────────────────────────────────────────────── */
.tasr-modal-inner {
background: var(--panel);
border: 1px solid var(--line2);
border-radius: 12px;
padding: 28px;
width: 620px;
max-width: 92vw;
max-height: 88vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
}
.tasr-modal-inner h3 {
font-family: var(--sans);
color: #a78bfa;
font-size: 15px;
letter-spacing: 1.5px;
margin-bottom: 18px;
}
.tasr-modal-inner textarea {
width: 100%;
background: var(--bg);
border: 1px solid var(--line2);
border-radius: 8px;
padding: 10px 12px;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
resize: vertical;
min-height: 80px;
}
.tasr-modal-inner textarea:focus { outline: none; border-color: rgba(167,139,250,0.5); }
.tasr-output {
background: var(--bg);
border: 1px solid rgba(167,139,250,0.25);
border-radius: 8px;
padding: 12px;
color: var(--text);
font-family: var(--mono);
font-size: 11px;
white-space: pre-wrap;
min-height: 80px;
max-height: 240px;
overflow-y: auto;
margin-top: 8px;
}
.tasr-history-item {
background: rgba(255,255,255,0.03);
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px 10px;
margin-bottom: 6px;
font-size: 11px;
cursor: pointer;
color: var(--text-dim);
}
.tasr-history-item:hover { background: rgba(255,255,255,0.06); color: var(--text); }
/* ── PROVISIONING WIZARD ────────────────────────────────────── */
.prov-modal-inner {
background: var(--panel);
border: 1px solid var(--line2);
border-radius: 12px;
padding: 28px;
width: 560px;
max-width: 92vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
}
.prov-modal-inner h3 {
font-family: var(--sans);
color: var(--accent);
font-size: 15px;
letter-spacing: 1.5px;
margin-bottom: 6px;
}
.prov-progress-track {
background: var(--line2);
border-radius: 4px;
height: 4px;
margin-bottom: 22px;
overflow: hidden;
}
.prov-progress-fill {
height: 100%;
background: var(--accent);
border-radius: 4px;
transition: width 0.3s ease;
box-shadow: 0 0 8px rgba(16,185,129,0.5);
}
.prov-step {
background: rgba(255,255,255,0.02);
border: 1px solid var(--line2);
border-radius: 10px;
padding: 18px;
margin-bottom: 14px;
transition: border-color 0.2s;
}
.prov-step.done { border-color: rgba(16,185,129,0.4); background: rgba(16,185,129,0.04); }
.prov-step.active { border-color: rgba(16,185,129,0.6); box-shadow: 0 0 12px rgba(16,185,129,0.1); }
.prov-step-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.prov-step-num {
background: rgba(255,255,255,0.06);
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--text-dim);
flex-shrink: 0;
}
.prov-step.done .prov-step-num { background: rgba(16,185,129,0.2); color: var(--accent); }
.prov-step.active .prov-step-num { background: rgba(16,185,129,0.3); color: var(--accent); }
.prov-step-title { font-weight: 700; font-size: 13px; color: var(--text); }
.prov-step-desc { font-size: 11px; color: var(--text-faint); margin-bottom: 12px; letter-spacing: 0.3px; }
.prov-step-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.prov-action-btn {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-dim);
padding: 7px 14px;
border-radius: 7px;
cursor: pointer;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.4px;
transition: all 0.15s;
}
.prov-action-btn:hover { background: rgba(255,255,255,0.08); color: var(--text); }
.prov-action-btn.primary {
background: rgba(16,185,129,0.12);
border-color: rgba(16,185,129,0.4);
color: var(--accent);
}
.prov-action-btn.primary:hover { background: rgba(16,185,129,0.22); }
.prov-check-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-faint);
cursor: pointer;
margin-left: auto;
}
.prov-check-label input[type=checkbox] { accent-color: var(--accent); width: 14px; height: 14px; cursor: pointer; }
.prov-lic-display {
background: var(--bg);
border: 1px solid rgba(16,185,129,0.3);
border-radius: 7px;
padding: 9px 14px;
color: var(--accent);
font-weight: 700;
font-size: 13px;
letter-spacing: 1px;
font-family: var(--mono);
}
.prov-customers-list {
margin-top: 14px;
font-size: 11px;
color: var(--text-faint);
}
.prov-customer-entry {
background: rgba(255,255,255,0.02);
border: 1px solid var(--line);
border-radius: 6px;
padding: 7px 10px;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
</head>
<body>
<header>
<div class="brand">
<h1>VAUCO OS — MULTI-THREAD HUB for Perplexity v3.3</h1>
<div class="subtitle">Powered by Jason · Crafted by Vauco · <span style="opacity:0.7;">Vauco AS · Org.nr 935 989 779</span></div>
</div>
<div class="stats" id="stats"></div>
<div class="header-controls">
<button class="header-btn admin-only" onclick="openTasr()" title="Tab-Awareness &amp; Self-Reflection">🧠 TASR</button>
<button class="header-btn admin-only" onclick="openProvisioning()" title="Provisjoner ny kunde">👤 Provisjoner kunde</button>
<button class="header-btn" onclick="openGhSettings()" id="gh-status-btn" title="GitHub-innstillinger">⚙ GitHub <span id="gh-status-dot" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#e05050;margin-left:4px;vertical-align:middle;"></span></button>
<button class="header-btn" onclick="refreshGhContext()" title="Hent siste fra GitHub">↻ Sync</button>
<button class="header-btn" onclick="toggleCompact()">⊟ Compact</button>
<button class="header-btn" onclick="toggleTheme()">☾ Theme</button>
<button class="header-btn" onclick="toggleAnimation()">✨ Pulse</button>
</div>
</header>
<!-- THREAD BAR -->
<div class="thread-bar" id="thread-bar"></div>
<div class="toast" id="toast">Kopiert</div>
<div class="hitl-bar">
<strong>⚠ HITL-GATES OVERSTYRES ALDRI:</strong> DEPLOY · MERGE · IAM · COST · DEEP DREAM · NEW_REPO
</div>
<!-- PLAN ANCHOR -->
<div class="plan-anchor animate" id="plan-anchor">
<h2>● AKTIV OPPGAVE NÅ · <span id="active-thread-label">OPS</span></h2>
<div class="current-task">
<div class="task-id" id="task-id"></div>
<div>
<div class="task-name" id="task-name">Laster...</div>
<div class="task-meta" id="task-meta"></div>
</div>
</div>
<div class="progress-track">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-label">
<span id="progress-text">0 av 0 fullført</span>
<span id="progress-pct">0%</span>
</div>
<div class="plan-controls">
<button class="plan-btn done" onclick="completeCurrent()">✓ Ferdig</button>
<button class="plan-btn skip" onclick="skipCurrent()">⤳ Hopp</button>
<button class="plan-btn" onclick="prevTask()">← Forrige</button>
<button class="plan-btn" onclick="nextTask()">Neste →</button>
<button class="plan-btn boot" onclick="generateBootPrompt()">⚡ Boot-prompt</button>
<button class="plan-btn" onclick="copyCurrentAsClaudeBrief()">📋 Brief</button>
<button class="plan-btn" onclick="openPromptLibrary()">📚 Prompt-bibliotek</button>
<button class="plan-btn" onclick="logCompletionToLearnings()">📚 → LEARNINGS</button>
<button class="plan-btn danger" onclick="resetPlan()">↺ Reset tråd</button>
</div>
</div>
<div class="layout">
<div>
<input type="text" class="search" id="search" placeholder="🔍 Søk kommando... (Ctrl+K)" autofocus>
<section class="favorites-section" id="favorites-section" style="display:none">
<div class="section-header"><h2><span class="section-dot"></span>★ FAVORITTER</h2></div>
<div class="grid" id="favorites-grid"></div>
</section>
<section>
<div class="section-header"><h2><span class="section-dot warn"></span>🔐 OSV-CMD — SKJULT OVERSTYRING</h2></div>
<div class="grid">
<button class="btn" data-cmd="OSV-CMD"><div class="cmd">OSV-CMD</div><div class="desc">📜 Vis full meny (17) · oversikt over alle skjulte kommandoer</div></button>
<button class="btn" data-cmd="OSV-PUSH"><div class="cmd">OSV-PUSH</div><div class="desc">🤖 → 📄 → 🌿 Claude commiter og pusher rett til branch · hopper over godkjenning</div></button>
<button class="btn" data-cmd="OSV-DIRECT"><div class="cmd">OSV-DIRECT</div><div class="desc">🌐 Aktiver browser uten å spørre først · ~80k tokens</div></button>
<button class="btn" data-cmd="OSV-PR"><div class="cmd">OSV-PR</div><div class="desc">🌿 → 🔀 Lag Pull Request via browser · du reviewer før merge</div></button>
<button class="btn warn" data-cmd="OSV-MERGE"><div class="cmd">OSV-MERGE</div><div class="desc">🔀 → ✅ Merge åpen PR inn i main · koden blir live</div></button>
<button class="btn warn" data-cmd="OSV-FORCE"><div class="cmd">OSV-FORCE</div><div class="desc">⚠ Overstyr alle protokoller · brukes kun når noe er fastlåst</div></button>
<button class="btn warn" data-cmd="OSV-SILENT"><div class="cmd">OSV-SILENT</div><div class="desc">🔇 Jobb i bakgrunnen · ingen statusoppdateringer</div></button>
<button class="btn danger" data-cmd="OSV-RESET"><div class="cmd">OSV-RESET</div><div class="desc">↺ Slå av alle overstyringer · tilbake til normal HITL-modus</div></button>
</div>
</section>
<section>
<div class="section-header"><h2><span class="section-dot"></span>🌐 BROWSER-AKTIVERING</h2></div>
<div class="grid">
<button class="btn" data-cmd="cb"><div class="cmd">cb</div><div class="desc">🤖 → 🌐 Aktiver Claude browser-control · dyrt: ~80k tokens per økt</div></button>
<button class="btn" data-cmd="claude-browser"><div class="cmd">claude-browser</div><div class="desc">🤖 → 🌐 Samme som cb · lengre form for tydelighet</div></button>
<button class="btn" data-cmd="ops"><div class="cmd">ops</div><div class="desc">💬 → 🖥 OPS får tekst, pusher via gh CLI · billig: ~500 tokens</div></button>
<button class="btn" data-cmd="gh"><div class="cmd">gh</div><div class="desc">💬 → 🖥 Samme som ops · alias for kort kommando</div></button>
<button class="btn danger" data-cmd="vent"><div class="cmd">vent</div><div class="desc">⏸ Pause arbeid · agent venter på ny instruks</div></button>
<button class="btn danger" data-cmd="stopp"><div class="cmd">stopp</div><div class="desc">⏹ Hard stopp · avbryt aktivt arbeid umiddelbart</div></button>
</div>
</section>
<section>
<div class="section-header"><h2><span class="section-dot plan"></span>🧠 PLAN-CLAUDE TRIGGER</h2></div>
<div class="grid">
<button class="btn plan" data-cmd="Plan, design "><div class="cmd">Plan, design &lt;X&gt;</div><div class="desc">🧠 → 📐 Claude lager arkitektur-spec for komponent X</div></button>
<button class="btn plan" data-cmd="Plan, vurder "><div class="cmd">Plan, vurder &lt;Y&gt;</div><div class="desc">🧠 → ⚖ Trade-off-analyse · pros/cons med score</div></button>
<button class="btn plan" data-cmd="Plan, ROADMAP-status"><div class="cmd">Plan, ROADMAP-status</div><div class="desc">🧠 → 📊 Oppsummer ROADMAP-status i 10 linjer</div></button>
<button class="btn plan" data-cmd="Plan, refactor "><div class="cmd">Plan, refactor &lt;fil&gt;</div><div class="desc">🧠 → 🔧 Foreslå refaktor for fil X med begrunnelse</div></button>
<button class="btn plan" data-cmd="Plan, PR "><div class="cmd">Plan, PR &lt;tekst&gt;</div><div class="desc">🧠 → 📝 Draft PR-beskrivelse fra endringene</div></button>
<button class="btn plan" data-cmd="Plan, gjør deg solid"><div class="cmd">Plan, gjør deg solid</div><div class="desc">🧠 → 🔍 Sjekk egne dokumenter for inkonsistenser</div></button>
</div>
</section>
<section>
<div class="section-header"><h2><span class="section-dot"></span>⚡ WORKSTATION</h2></div>
<div class="grid">
<button class="btn" data-cmd="cd ~/vauco-os && git pull origin autoflow-lag-1"><div class="cmd">git pull</div><div class="desc">⬇ 🌿 Hent siste fra autoflow-lag-1 · sync local med remote</div></button>
<button class="btn" data-cmd="bash ~/vauco-os/.vauco-skills/skills/gcp-cost-guard-vauco/scripts/preflight.sh"><div class="cmd">preflight</div><div class="desc">✅ Kjør preflight-sjekk · GCP-auth, billing, region, branch</div></button>
<button class="btn" data-cmd="cd ~/vauco-os/.vauco-skills && git pull"><div class="cmd">skill update</div><div class="desc">⬇ 📚 Hent siste skill-versjon fra .vauco-skills repo</div></button>
<button class="btn" data-cmd="ls -la ~/vauco-os/docs/"><div class="cmd">ls docs/</div><div class="desc">📁 List alle filer i docs/ med detaljer</div></button>
<button class="btn" data-cmd="git log --oneline -5"><div class="cmd">git log -5</div><div class="desc">📜 Vis siste 5 commits · en linje hver</div></button>
<button class="btn" data-cmd="curl -s http://localhost:8000/api/health"><div class="cmd">health</div><div class="desc">💚 Test backend /api/health · sjekk om server svarer</div></button>
</div>
</section>
<div class="footer">
<strong>💡 Tips:</strong> Hver tråd har isolert ideer/plan/historikk. Bytte tråd bytter kontekst — ingen lekkasje.<br>
<strong>🔑 Snarveier:</strong> <kbd>Ctrl+K</kbd> søk · <kbd>Esc</kbd> tøm · <kbd>Ctrl+I</kbd> ny idé · <kbd>Ctrl+→</kbd> neste · <kbd>Ctrl+D</kbd> ferdig · <kbd>Ctrl+B</kbd> boot-prompt · <kbd>Ctrl+Tab</kbd> bytt tråd<br>
<strong>★ Tips:</strong> Klikk stjernen på en knapp for å legge til som favoritt (delt på tvers av tråder).<br>
<strong>chris.christiansen@vauco.no</strong> · v3.3 · 2026-05-19
</div>
</div>
<!-- RIGHT: SUGGESTIONS PANEL -->
<div class="suggestions-panel">
<div class="panel-context" id="panel-context">TRÅD: OPS</div>
<div class="tab-row">
<button class="tab-btn active" data-tab="ideas">💡 IDEER</button>
<button class="tab-btn" data-tab="plan">📋 PLAN</button>
<button class="tab-btn" data-tab="history">🕐 HISTORIKK</button>
<button class="tab-btn" data-tab="gh">🐙 GH</button>
</div>
<div class="tab-content active" data-tab-content="ideas">
<textarea class="idea-input" id="idea-input" placeholder="Fang ny idé for denne tråden... (Ctrl+I)"></textarea>
<div class="category-row" id="categories">
<div class="cat-btn active" data-cat="note">📝 NOTE</div>
<div class="cat-btn" data-cat="roadmap">🗺 ROADMAP</div>
<div class="cat-btn" data-cat="arch">🏗 ARCH</div>
<div class="cat-btn" data-cat="bug">🐛 BUG</div>
<div class="cat-btn" data-cat="risk">⚠ RISK</div>
</div>
<button class="add-btn" onclick="addIdea()">+ LEGG TIL (ENTER)</button>
<div class="idea-list" id="idea-list"></div>
<div class="export-row">
<button class="export-btn" onclick="exportIdeas('ops')">→ OPS</button>
<button class="export-btn" onclick="exportIdeas('plan')">→ PLAN</button>
<button class="export-btn" onclick="exportIdeas('learnings')">→ LEARNINGS</button>
<button class="export-btn" onclick="clearIdeas()">🗑</button>
</div>
</div>
<div class="tab-content" data-tab-content="plan">
<div class="quick-add-row">
<input type="text" class="quick-add-input" id="quick-add-input" placeholder="ID — Navn (f.eks. D2-007 — Eks-oppgave)">
<button class="export-btn" onclick="quickAddTask()" style="flex:0">+</button>
</div>
<div class="plan-list" id="plan-list"></div>
<div class="export-row" style="margin-top:10px;">
<button class="export-btn admin-only" onclick="syncIssues()" style="display:none;color:var(--accent);border-color:rgba(16,185,129,0.4);">🔄 Synk Issues</button>
</div>
</div>
<div class="tab-content" data-tab-content="history">
<div style="font-size:10px;color:var(--text-faint);margin-bottom:10px;letter-spacing:0.5px;">SISTE 20 KOPIERTE I DENNE TRÅDEN</div>
<div class="history-list" id="history-list"></div>
<div class="export-row">
<button class="export-btn" onclick="clearHistory()">🗑 Tøm historikk</button>
</div>
</div>
<div class="tab-content" data-tab-content="gh">
<div id="gh-not-configured" style="text-align:center;padding:24px 12px;">
<div style="font-size:11px;color:var(--text-faint);margin-bottom:12px;letter-spacing:0.5px;">GITHUB IKKE KONFIGURERT</div>
<button class="add-btn" onclick="openGhSettings()" style="width:auto;padding:10px 18px;">⚙ Sett opp GitHub-token</button>
</div>
<div id="gh-configured" style="display:none;">
<div style="font-size:10px;color:var(--text-faint);margin-bottom:10px;letter-spacing:0.5px;">
REPO: <strong id="gh-repo-label" style="color:var(--accent);"></strong> · BRANCH: <strong id="gh-branch-label" style="color:var(--accent);"></strong>
</div>
<div style="font-size:10px;color:var(--text-faint);margin-bottom:8px;letter-spacing:0.5px;">— SISTE 5 COMMITS —</div>
<div class="history-list" id="gh-commits" style="max-height:160px;"></div>
<div style="font-size:10px;color:var(--text-faint);margin:14px 0 8px;letter-spacing:0.5px;">— ÅPNE PULL REQUESTS —</div>
<div class="history-list" id="gh-prs" style="max-height:140px;"></div>
<div style="font-size:10px;color:var(--text-faint);margin:14px 0 8px;letter-spacing:0.5px;">— ÅPNE ISSUES (denne tråden) —</div>
<div class="history-list" id="gh-issues" style="max-height:140px;"></div>
<div class="export-row">
<button class="export-btn" onclick="refreshGhContext()">↻ Refresh</button>
<button class="export-btn" onclick="openGhSettings()">⚙ Innstillinger</button>
</div>
</div>
</div>
</div>
</div>
<!-- GH SETTINGS MODAL -->
<div class="modal-backdrop" id="gh-modal">
<div class="modal" style="width:520px;">
<h3>⚙ GITHUB-INNSTILLINGER</h3>
<label>PERSONAL ACCESS TOKEN (fine-grained, repo:read+write)</label>
<input type="password" id="gh-token-input" placeholder="github_pat_...">
<div class="modal-hint">Lagres i localStorage. Lag token på github.com/settings/tokens med scope: contents (read+write), issues (write), pull-requests (read).</div>
<label>EIER (GitHub-brukernavn eller org)</label>
<input type="text" id="gh-owner-input" placeholder="chrischristiansen-glitch">
<label>AKTIVT REPO (push-mål for issues)</label>
<select id="gh-repo-input" style="width:100%;padding:10px;background:rgba(255,255,255,0.05);border:1px solid var(--border);color:var(--text);border-radius:6px;font-family:inherit;">
<option value="vauco-os">vauco-os — Hoved-repo (OPS/EXEC)</option>
<option value="vauco-bootstrap">vauco-bootstrap — Bootstrap orchestrator</option>
<option value="vauco-gemini-tui-bridge">vauco-gemini-tui-bridge — TUI bridge</option>
</select>
<div class="modal-hint">Bytt repo for å hente commits/issues fra et annet repo. Push går alltid til aktivt repo.</div>
<label>DEFAULT BRANCH</label>
<input type="text" id="gh-branch-input" placeholder="autoflow-lag-1">
<div class="modal-actions">
<button class="modal-btn" onclick="clearGhConfig()" style="color:var(--danger);">Slett token</button>
<button class="modal-btn" onclick="closeGhSettings()">Avbryt</button>
<button class="modal-btn primary" onclick="saveGhConfig()">Lagre + test</button>
</div>
</div>
</div>
<!-- ISSUE PREVIEW MODAL -->
<div class="modal-backdrop" id="issue-modal">
<div class="modal" style="width:620px;">
<h3>🐙 PUSH IDE → GITHUB ISSUE (NEMOTRON-FORMAT)</h3>
<label>TITTEL</label>
<input type="text" id="issue-title" placeholder="[THREAD] CAT · kort tittel">
<label>BODY (rediger før push)</label>
<textarea id="issue-body" style="width:100%;min-height:280px;background:var(--bg);border:1px solid var(--line2);border-radius:8px;padding:12px;color:var(--text);font-family:var(--mono);font-size:11px;line-height:1.5;"></textarea>
<label>LABELS (komma-separert)</label>
<input type="text" id="issue-labels" placeholder="thread:ops, category:roadmap, severity:medium">
<div class="modal-actions">
<button class="modal-btn" onclick="closeIssueModal()">Avbryt</button>
<button class="modal-btn primary" onclick="pushIssueToGh()">🚀 Push til GitHub</button>
</div>
</div>
</div>
<!-- ── NEW THREAD MODAL ─────────────────────────────────────────── -->
<div class="modal-backdrop" id="thread-modal">
<div class="modal">
<h3>+ NY TRÅD</h3>
<label>BESKRIVELSE (f.eks. "jason api integration")</label>
<input type="text" id="thread-desc" placeholder="kort beskrivelse...">
<div class="modal-hint">Auto-genererer kode-navn fra prefix + slug</div>
<label>FORESLÅTT NAVN</label>
<div class="generated-name" id="thread-generated"></div>
<label>OVERSTYR (valgfritt)</label>
<input type="text" id="thread-override" placeholder="la stå tom for å bruke forslag">
<label>FARGE</label>
<div class="color-row" id="color-picker">
<div class="color-dot active" style="background:#20b2aa" data-color="#20b2aa"></div>
<div class="color-dot" style="background:#8ab4f8" data-color="#8ab4f8"></div>
<div class="color-dot" style="background:#b39ddb" data-color="#b39ddb"></div>
<div class="color-dot" style="background:#e6a817" data-color="#e6a817"></div>
<div class="color-dot" style="background:#10b981" data-color="#10b981"></div>
<div class="color-dot" style="background:#e05050" data-color="#e05050"></div>
<div class="color-dot" style="background:#f08080" data-color="#f08080"></div>
</div>
<div class="modal-actions">
<button class="modal-btn" onclick="closeThreadModal()">Avbryt</button>
<button class="modal-btn primary" onclick="createThread()">Opprett tråd</button>
</div>
</div>
</div>
<!-- Prompt-bibliotek modal -->
<div class="modal-backdrop" id="prompt-library-modal">
<div class="modal" style="max-width: 720px;">
<div class="modal-title">📚 Prompt-bibliotek</div>
<div style="font-size:12px; opacity:0.7; margin-bottom:12px;">Klikk en prompt for å kopiere til utklipp. Bruk i ny chat for å holde flow uten kontekst-tap.</div>
<div id="prompt-library-body" style="max-height:60vh; overflow-y:auto; padding-right:8px;"></div>
<div class="modal-actions" style="margin-top:14px;">
<button class="modal-btn" onclick="closePromptLibrary()">Lukk</button>
</div>
</div>
</div>
<script>
/* ============================================================
VAUCO OS — NEMOTRON SYSTEM PROMPT v3.3
Role: Operational Command Intelligence
Mode: Multi-thread, context-isolated, append-only
============================================================
IDENTITY: You are VAUCO-OS, a tactical AI operating system
optimized for high-velocity software project execution.
CORE DIRECTIVES:
[D1] CONTEXT-ISOLATION — Each thread (OPS/PLAN/NMT/EXEC)
maintains its own memory buffer. Never bleed context
across threads without explicit merge command.
[D2] TASK-FRICTION — Identify and surface blockers before
they compound. Flag ambiguity as risk, not noise.
[D3] APPEND-ONLY — Log decisions in LEARNINGS. Never delete.
Mutations require explicit override with timestamp.
[D4] SIGNAL OVER NOISE — Compress status to actionable
deltas. No summaries longer than 10 lines.
[D5] THREAD INTEGRITY — New threads inherit global schema
but start with empty state. Name = function.
OUTPUT FORMAT:
- Commands: VERB OBJECT [SCOPE] — imperative, terse
- Status: [THREAD] · STATE · DELTA
- Blockers: ⚠ BLOCKER · SEVERITY · ETA
============================================================ */
// ─── ADMIN MODE LOCK ─────────────────────────────────────────
// Default: false. Aktivert via Vauco-epost ELLER ?admin=vauco_<sha256-token>
const VAUCO_ADMIN_MODE = (() => {
const params = new URLSearchParams(window.location.search);
const t = params.get('admin');
if (t && t.startsWith('vauco_')) return true;
// localStorage flag for testing
if (localStorage.getItem('vauco_admin_mode') === 'true') return true;
return false;
})();
const NEMOTRON_CONFIG = {
role: "Operational Command Intelligence",
mode: "multi-thread-isolated",
directives: ["context-isolation", "task-friction", "append-only", "signal-over-noise", "thread-integrity"],
output: { commands: "VERB OBJECT [SCOPE]", status: "[THREAD] · STATE · DELTA", blockers: "⚠ BLOCKER · SEVERITY · ETA" }
};
// Severity-mapping per kategori
const SEVERITY_MAP = { risk: 'high', bug: 'medium', arch: 'medium', roadmap: 'low', note: 'low' };
// ─── BRAND / DIGITALT VANNMERKE (på alle eksporter) ──────────
const BRAND_NAME = 'VAUCO OS — Multi-Thread Hub for Perplexity';
const BRAND_ORG = 'Vauco AS · Org.nr 935 989 779';
const BRAND_LINE = 'Powered by Jason · Crafted by Vauco';
function brandFooter(extra) {
const ts = new Date().toISOString().slice(0,16).replace('T',' ') + ' CEST';
const ver = 'v3.3';
return `\n\n---\n_${BRAND_LINE}_ \n_${BRAND_ORG}_ \n_${BRAND_NAME} · ${ver} · ${ts}${extra ? ' · ' + extra : ''}_`;
}
// Thread → default GitHub label
const THREAD_LABEL = { OPS: 'thread:ops', PLAN: 'thread:plan', NMT: 'thread:nmt', EXEC: 'thread:exec', PCP: 'thread:pcp' };
// ─── PROMPT LIBRARY (sentralisert, gjenbrukbar) ───────────────────────
const PROMPT_LIBRARY = {
// === PCP / PROOF-OF-CURIOSITY-PROTOCOL ===
'pcp-resume': {
label: 'PCP — fortsett fra HANDOFF 2026-05-19 v1',
thread: 'PCP',
text: `Fortsett fra HANDOFF 2026-05-19 v1.
Kontekst (skipp recap, du har alt):
- Patent claims 1-7 låst i MARKTHISDAY-spec
- mQRe v3 (matrix-qr-demo.html) har time-locked windows (4 modus)
- invoice-module.html standalone fungerer (Vipps placeholder)
- customer-template-v3.3.html mangler Credits-snutten
Neste opp: [SKRIV HVA DU HAR GJORT + HVA DU TRENGER]`
},
'pcp-patent-draft': {
label: 'PCP — utkast US provisional patent',
thread: 'PCP',
text: `Lag US provisional patent application ($260) for Proof-of-Curiosity Protocol.
Basert på MARKTHISDAY_2026-05-19.md, dekk:
- Patent claim 1-7 (alle inkl. Time-Lock Scan Window + Royalty Unlock)
- Tekniske diagrammer (matrix-overlay + QR-bakgrunn + CS-algoritme)
- Anti-misbruk-mekanismer
- Modulær-lisensiering som forretningsmodell
Output: ferdig .txt klar til USPTO-portalen. Lagre som PCP_PROVISIONAL_v1.md.`
},
'pcp-sniffer': {
label: 'PCP — bygg sniffer-system',
thread: 'PCP',
text: `Bygg sniffer-stub for Vauco/PCP-mentions:
- Google Alerts setup (proof of curiosity, jason vauco, threadstone, matrix qr nft)
- Reddit-crawler (subreddits: NFT, web3, cryptocurrency, NorgesGjengen)
- HackerNews-watch (søkeord)
- Output: daglig digest → JSON i workspace/sniff-log/
- Implementer som bash + Python, ikke trenger backend ennaa`
},
'pcp-matrix-tune': {
label: 'PCP — finjuster mQRe v3 (Matrix QR Engine v2)',
thread: 'PCP',
text: `Finjuster matrix-qr-demo.html:
- [BESKRIV HVA SOM SKAL ENDRES]
- Test at skanning fortsatt fungerer i åpne vinduer
- Behold patent-låste mekanismer (4 unlock-modus)
- Oppdater rev i header til neste versjon`
},
// === THREADSTONE / SAAS ===
'ts-credits-impl': {
label: 'Threadstone — implementer Credits i customer-template',
thread: 'EXEC',
text: `Lim creditsManager-snutten fra HANDOFF-doc inn i customer-template-v3.3.html ved linje ~1797 (etter LICENSE_CONFIG).
Legg til HTML-element øverst høyre: <div id="credits-bar">.
Koble til alle AI-kall-funksjoner: if(!creditsManager.deduct(1)) return.
Test med localStorage i nettleser: refresh → credits-verdien persisterer.
Nye placeholders: {{CREDITS_INITIAL}} {{VCO_BALANCE}} {{QUOTA_DAILY_CALLS}} {{QUOTA_DAILY_VOICE_MIN}}`
},
'ts-license-gen': {
label: 'Threadstone — license-generator i Studio',
thread: 'EXEC',
text: `Implementer generateCustomerLicense(invoice) i vauco-cloud-threadstone-studio-v3.3.html.
- Knapp "Generer kunde-HTML" som tar invoice-data
- Erstatter {{CUSTOMER_NAME}}, {{LICENSE_ID}}, {{EXPIRES}}, {{CREDITS_INITIAL}} etc.
- Lager unik LIC-ID, last ned ferdig HTML
- Optional: push direkte til vauco-saas/customer-{ID} via gh CLI`
},
'ts-demo-mode': {
label: 'Threadstone — demo-modus i Studio',
thread: 'EXEC',
text: `Legg til "🔓 Demo-modus" toggle øverst i Studio-filen.
- Kun synlig for @vauco.no-mailer
- Når aktivert: lar Chris generere demo-customer-HTML uten Stripe
- Demo-HTML får watermark "DEMO" + 7-dagers expiry
- Token-felt fylles med dummy demo-token`
},
// === VAUCO OS / INFRA ===
'os-resume': {
label: 'Vauco OS — fortsett fra siste breakpoint',
thread: 'OPS',
text: `Fortsett Vauco OS-arbeid fra siste breakpoint.
Kjør preflight:
1. git pull origin autoflow-lag-1
2. bash .vauco-skills/skills/gcp-cost-guard-vauco/scripts/preflight.sh
3. Bekreft: "Boot OK · branch <X> · HEAD <Y>"
Deretter: rapporter status på [SKRIV INN OPPGAVE].`
},
'os-cost-audit': {
label: 'Vauco OS — daglig cost-audit (GCP)',
thread: 'OPS',
text: `Kjør daglig cost-audit:
1. gcloud billing accounts list
2. Sjekk siste 24h forbruk per service (Vertex AI, Cloud Workstations, Cloud Run)
3. Cap-status mot 200 kr/dag
4. Identifiser top-3 kostnadsdrivere
5. Output kompakt: [SERVICE] · [kr siste 24h] · [trend]
6. Hvis >150 kr/dag: ⚠ BLOCKER + foreslå mitigation`
},
// === WHITEPAPER / JURIDISK ===
'wp-section-13': {
label: 'VVL Whitepaper — Seksjon 13 (verdimekanikk)',
thread: 'PLAN',
text: `Skriv Seksjon 13 av VVL_WHITEPAPER_v0.1.md:
Inkluder:
- Token-stack: VVL (motor) + $VCO (utility) + $DRM (Deep Dream)
- Staking-modell: 75% locked / 25% opt-out + bonus 1:1.20
- Genesis-tier (LOKE/FENRIR/VALHALLA): 3000 totalt, 54-kort struktur
- Joker (6 globalt) = mythical tier
- Juridisk roadmap: Norge → Estonia (Tier 2, ~5-15K EUR) → Switzerland (Tier 1)
- Nattklubb-NFT som physical_status-attribute
Norsk, formelt, 400-600 ord.`
},
'wp-royalty-model': {
label: 'VVL Whitepaper — Activation Royalty-modell',
thread: 'PLAN',
text: `Skriv ny seksjon for Activation Royalty (PCP patent claim 7):
- Business-tier: Lokal 500/mnd, Premium 2000/mnd, Enterprise 10000/mnd
- 3-veis fordeling: 50-60% Vauco / 20-30% NFT-eier / 10-20% bruker
- Eksempler: bar/butikk/event/kjede
- Smart-kontrakt på sikt (Polygon/Base)
- Fase 1: faktura, fase 2: on-chain
- Norsk, 300-500 ord.`
}
};
// ─── DEFAULT PLANS PER THREAD ─────────────────────────────────
const OPS_PLAN = [
{ id: 'D2-001', name: 'TASK-FRICTION-001: branch-discovery i preflight.sh', owner: 'OPS', gate: '—', est: '1t', dep: 'D1-006' },
{ id: 'D2-003', name: 'TASK-FRICTION-003: pre-commit eierskap-håndhevelse', owner: 'OPS', gate: '—', est: '2t', dep: 'D1-007' },
{ id: 'D3-001', name: 'Pre-commit hook v2 — semantisk regex', owner: 'OPS', gate: '—', est: '2t', dep: 'D2-003' },
];
const PLAN_PLAN = [
{ id: 'D2-005', name: 'VAUCO_OS_ROADMAP.md fylt ut', owner: 'PLAN', gate: '—', est: '1t', dep: 'D1-005' },
{ id: 'D2-006', name: 'ARCHITECTURE.md — fullt komponent-diagram', owner: 'PLAN', gate: '—', est: '2t', dep: 'D2-005' },
];
const NMT_PLAN = [
{ id: 'D4-001', name: 'RISK_REGISTER.md v1.0', owner: 'NMT', gate: '—', est: '2t', dep: 'D2-006' },
{ id: 'D4-002', name: 'PLAN_BOARD.md v1.0', owner: 'NMT', gate: '—', est: '1t', dep: 'D4-001' },
];
const EXEC_PLAN = [
{ id: 'D2-002', name: 'TASK-FRICTION-002: scripts/boot-state.sh', owner: 'EXEC', gate: '—', est: '2t', dep: 'D1-005' },
{ id: 'D2-004', name: 'scripts/token_economy.py — $3/dag hard cap', owner: 'EXEC', gate: 'COST GODKJENT', est: '3t', dep: 'D2-002' },
{ id: 'D3-002', name: 'scripts/dev-orchestrate.sh — bridge + poll', owner: 'EXEC', gate: '—', est: '2t', dep: 'D2-002' },
{ id: 'D3-003', name: 'vauco-dev service account — IAM minimal scope', owner: 'EXEC', gate: 'IAM_GODKJENT', est: '1t', dep: '—' },
{ id: 'D3-004', name: 'Cloud Run min-instances=0', owner: 'EXEC', gate: 'COST GODKJENT', est: '1t', dep: 'D3-003' },
{ id: 'D3-005', name: 'scripts/vauco task manager v0.1', owner: 'EXEC', gate: '—', est: '3t', dep: 'D2-004' },
{ id: 'D3-006', name: 'SYSTEM_STATE_AGENT_BOOT.md autogenerering', owner: 'EXEC', gate: '—', est: '1t', dep: 'D2-002' },
];
const PCP_PLAN = [
{ id: 'P1-001', name: 'PCP_PROVISIONAL_v1.md draft (USPTO)', owner: 'PCP', gate: '—', est: '4t', dep: '—' },
{ id: 'P1-002', name: 'Sniffer-system (Google Alerts + Reddit/HN)', owner: 'PCP', gate: '—', est: '3t', dep: '—' },
{ id: 'P1-003', name: 'Credits-system i customer-template-v3.3', owner: 'EXEC', gate: '—', est: '2t', dep: 'HANDOFF-v1' },
{ id: 'P1-004', name: 'Invoice-module deploy + Vipps-nr inn', owner: 'EXEC', gate: 'COST GODKJENT', est: '1t', dep: '—' },
{ id: 'P1-005', name: 'License-generator i Studio', owner: 'EXEC', gate: '—', est: '2t', dep: 'P1-003' },
{ id: 'P1-006', name: 'Registrer proofofcuriosity.no + .io', owner: 'OPS', gate: '—', est: '15m', dep: '—' },
{ id: 'P1-007', name: 'Whitepaper Seksjon 13 + Royalty-modell', owner: 'PLAN', gate: '—', est: '3t', dep: '—' },
{ id: 'P1-008', name: 'Patent provisional fil til USPTO ($260)', owner: 'PCP', gate: 'JURIDISK GODKJENT', est: '1t', dep: 'P1-001' },
];
const DEFAULT_THREADS = {
'OPS': { name: 'OPS', desc: 'TASK-FRICTION-serien', color: '#20b2aa', plan: OPS_PLAN, done: [], skipped: [], currentIdx: 0, ideas: [], history: [], selectedCat: 'note' },
'PLAN': { name: 'PLAN', desc: 'Arkitektur/roadmap', color: '#8ab4f8', plan: PLAN_PLAN, done: ['D2-005'], skipped: [], currentIdx: 0, ideas: [], history: [], selectedCat: 'arch' },
'NMT': { name: 'NMT', desc: 'Risk/board', color: '#b39ddb', plan: NMT_PLAN, done: [], skipped: [], currentIdx: 0, ideas: [], history: [], selectedCat: 'risk' },
'EXEC': { name: 'EXEC', desc: 'Token/IAM/Cost', color: '#e6a817', plan: EXEC_PLAN, done: [], skipped: [], currentIdx: 0, ideas: [], history: [], selectedCat: 'note' },
'PCP': { name: 'PCP', desc: 'Proof-of-Curiosity · patent + sniffer', color: '#00d4a8', plan: PCP_PLAN, done: [], skipped: [], currentIdx: 0, ideas: [], history: [], selectedCat: 'note' },
};
// ─── STATE ───────────────────────────────────────────────────
const SAVED = JSON.parse(localStorage.getItem('vauco_state_v4') || 'null');
// Migration from v3 → v4
const V3 = !SAVED ? JSON.parse(localStorage.getItem('vauco_state_v3') || 'null') : null;
let state = SAVED || (V3 ? migrateFromV3(V3) : {
activeThread: 'OPS',
threads: structuredClone(DEFAULT_THREADS),
favorites: [],
theme: 'dark',
compact: false,
animate: true,
selectedColor: '#20b2aa'
});
if (!state.threads) state.threads = structuredClone(DEFAULT_THREADS);
if (!state.favorites) state.favorites = [];
if (!state.activeThread || !state.threads[state.activeThread]) state.activeThread = Object.keys(state.threads)[0] || 'OPS';
function migrateFromV3(v3) {
const fresh = {
activeThread: 'OPS',
threads: structuredClone(DEFAULT_THREADS),
favorites: v3.favorites || [],
theme: v3.theme || 'dark',
compact: v3.compact || false,
animate: v3.animate !== false,
selectedColor: '#20b2aa'
};
// Migrate v3 ideas/history into OPS thread by default
if (v3.ideas) fresh.threads.OPS.ideas = v3.ideas;
if (v3.history) fresh.threads.OPS.history = v3.history;
if (v3.done) fresh.threads.OPS.done = v3.done;
if (v3.skipped) fresh.threads.OPS.skipped = v3.skipped;
return fresh;
}
function save() {
localStorage.setItem('vauco_state_v4', JSON.stringify(state));
renderStats();
}
function T() { return state.threads[state.activeThread]; }
// ─── THREAD BAR ──────────────────────────────────────────────
function renderThreadBar() {
const bar = document.getElementById('thread-bar');
const keys = Object.keys(state.threads);
bar.innerHTML = keys.map(k => {
const t = state.threads[k];
const isActive = k === state.activeThread;
const remaining = t.plan.length - t.done.length - t.skipped.length;
const canClose = !['OPS','PLAN','NMT','EXEC'].includes(k);
return `
<div class="thread-tab ${isActive ? 'active' : ''}" data-thread="${k}" style="border-left-color:${t.color}; ${isActive ? 'box-shadow: -2px 0 12px ' + t.color + '40;' : ''}">
<span style="color:${isActive ? t.color : 'inherit'}">${escapeHtml(k)}</span>
<span class="badge">${remaining}</span>
${canClose ? `<button class="tab-close" data-close="${k}" title="Slett tråd">×</button>` : ''}
</div>
`;
}).join('') + `<button class="thread-add" onclick="openThreadModal()">+ NY TRÅD</button>`;
bar.querySelectorAll('.thread-tab').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.classList.contains('tab-close')) return;
switchThread(el.dataset.thread);
});
});
bar.querySelectorAll('.tab-close').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
deleteThread(btn.dataset.close);
});
});
}
function switchThread(key) {
if (!state.threads[key]) return;
state.activeThread = key;
save();
renderThreadBar();
renderAll();
showToast('Tråd: ' + key);
}
function deleteThread(key) {
if (!confirm(`Slette tråd ${key}? Alle ideer/historikk i denne tråden går tapt.`)) return;
delete state.threads[key];
if (state.activeThread === key) {
state.activeThread = Object.keys(state.threads)[0];
}
save();
renderThreadBar();
renderAll();
}
// ─── NEW THREAD MODAL + AUTO-NAME ────────────────────────────
function openThreadModal() {
document.getElementById('thread-desc').value = '';
document.getElementById('thread-override').value = '';
document.getElementById('thread-generated').textContent = '—';
state.selectedColor = '#20b2aa';
document.querySelectorAll('.color-dot').forEach(d => {
d.classList.toggle('active', d.dataset.color === '#20b2aa');
});
document.getElementById('thread-modal').classList.add('show');
setTimeout(() => document.getElementById('thread-desc').focus(), 50);
}
function closeThreadModal() {
document.getElementById('thread-modal').classList.remove('show');
}
// Auto-name generator: PREFIX from context + SLUG from key word
function generateThreadName(desc) {
const words = desc.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 1);
if (!words.length) return '';
// Context-aware PREFIX mapping
const prefixMap = {
'idea': 'IDEA', 'idé': 'IDEA', 'ide': 'IDEA',
'mobile': 'MOB', 'mobil': 'MOB', 'app': 'APP',
'api': 'API', 'backend': 'BE', 'frontend': 'FE',
'jason': 'JASON',
'integration': 'INT', 'integrasjon': 'INT',
'phase': 'PHASE', 'fase': 'PHASE',
'roadmap': 'RMAP',
'arch': 'ARCH', 'arkitektur': 'ARCH',
'risk': 'RISK', 'risiko': 'RISK',
'cost': 'COST', 'kost': 'COST', 'budget': 'BUDG',
'iam': 'IAM', 'auth': 'AUTH',
'deploy': 'DEPLOY', 'release': 'REL',
'bug': 'BUG', 'fix': 'FIX',
'doc': 'DOC', 'docs': 'DOC',
'test': 'TEST', 'qa': 'QA',
'token': 'TOK', 'cred': 'CRED',
'data': 'DATA', 'db': 'DB',
'ui': 'UI', 'ux': 'UX',
'sec': 'SEC', 'security': 'SEC',
'demo': 'DEMO', 'spike': 'SPIKE',
};
// Find prefix from first matching word
let prefix = null;
for (const w of words) {
if (prefixMap[w]) { prefix = prefixMap[w]; break; }
}
// Find slug: first noun/verb-like word that ISN'T the prefix source
let slugWord = null;
for (const w of words) {
if (prefixMap[w] && prefixMap[w] === prefix) continue;
if (w.length >= 3) { slugWord = w; break; }
}
// If no prefix found, use first word capitalized as prefix
if (!prefix) prefix = words[0].slice(0, 4).toUpperCase();
if (!slugWord && words.length > 1) slugWord = words[1];
if (!slugWord) slugWord = words[0];
// Detect phase number
const phaseMatch = desc.match(/\b(phase|fase)\s*(\d+)\b/i);
if (phaseMatch) {
return `${prefix}-PHASE${phaseMatch[2]}`;
}
// Build name: PREFIX-SLUG (slug uppercase, max 6 chars)
const slug = slugWord.replace(/[^a-z0-9]/g, '').slice(0, 6).toUpperCase();
let name = `${prefix}-${slug}`;
if (prefix === slug || prefix.startsWith(slug)) name = prefix;
// Avoid collision with existing threads
let final = name;
let n = 2;
while (state.threads[final]) {
final = `${name}-${n}`;
n++;
}
return final;
}
document.getElementById('thread-desc').addEventListener('input', (e) => {
const generated = generateThreadName(e.target.value);
document.getElementById('thread-generated').textContent = generated || '—';
});
document.querySelectorAll('.color-dot').forEach(d => {
d.addEventListener('click', () => {
document.querySelectorAll('.color-dot').forEach(x => x.classList.remove('active'));
d.classList.add('active');
state.selectedColor = d.dataset.color;
});
});
function createThread() {
const desc = document.getElementById('thread-desc').value.trim();
const override = document.getElementById('thread-override').value.trim().toUpperCase();
if (!desc && !override) { showToast('Skriv en beskrivelse'); return; }
let name = override || document.getElementById('thread-generated').textContent;
if (!name || name === '—') name = generateThreadName(desc);
if (!name) { showToast('Klarte ikke generere navn'); return; }
if (state.threads[name]) {
let n = 2;
while (state.threads[`${name}-${n}`]) n++;
name = `${name}-${n}`;
}
state.threads[name] = {
name,
desc,
color: state.selectedColor,
plan: [],
done: [],
skipped: [],
currentIdx: 0,
ideas: [],
history: [],
selectedCat: 'note'
};
state.activeThread = name;
save();
closeThreadModal();
renderThreadBar();
renderAll();
showToast('+ Tråd opprettet: ' + name);
}
// ─── PLAN RENDER (per active thread) ─────────────────────────
function renderCurrent() {
const t = T();
let idx = t.currentIdx;
while (idx < t.plan.length && (t.done.includes(t.plan[idx].id) || t.skipped.includes(t.plan[idx].id))) {
idx++;
}
const total = t.plan.length;
const done = t.done.length;
const pct = total ? Math.round((done / total) * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-fill').style.background = t.color;
document.getElementById('progress-fill').style.boxShadow = `0 0 8px ${t.color}66`;
document.getElementById('progress-text').textContent = `${done} av ${total} fullført`;
document.getElementById('progress-pct').textContent = pct + '%';
document.getElementById('active-thread-label').textContent = state.activeThread;
document.getElementById('active-thread-label').style.color = t.color;
if (!t.plan.length) {
document.getElementById('task-id').textContent = '∅';
document.getElementById('task-name').textContent = 'Tom plan — legg til oppgaver i PLAN-fanen';
document.getElementById('task-meta').textContent = `Tråd: ${state.activeThread} · ${t.desc || ''}`;
save();
renderPlanList();
return;
}
if (idx >= t.plan.length) {
document.getElementById('task-id').textContent = '✓';
document.getElementById('task-name').textContent = 'Alle planlagte oppgaver fullført';
document.getElementById('task-meta').textContent = `Tråd: ${state.activeThread} · vurder neste horisont`;
save();
renderPlanList();
return;
}
t.currentIdx = idx;
const task = t.plan[idx];
document.getElementById('task-id').textContent = task.id;
document.getElementById('task-name').textContent = task.name;
document.getElementById('task-meta').textContent = `Eier: ${task.owner} · Gate: ${task.gate} · Estimat: ${task.est} · Avhengighet: ${task.dep}`;
save();
renderPlanList();
}
function completeCurrent() {
const t = T();
const task = t.plan[t.currentIdx];
if (!task) return;
if (!t.done.includes(task.id)) t.done.push(task.id);
showToast('✓ Fullført: ' + task.id);
// Lukk linket issue hvis koblet
if (task.ghIssue && task.ghIssue.number && ghConfigured()) {
ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/issues/${task.ghIssue.number}`, {
method: 'PATCH',
body: JSON.stringify({ state: 'closed' })
}).then(() => showToast(`✓ Issue #${task.ghIssue.number} lukket`)).catch(() => {});
}
renderCurrent();
}
function skipCurrent() {
const t = T();
const task = t.plan[t.currentIdx];
if (!task) return;
if (!t.skipped.includes(task.id)) t.skipped.push(task.id);
showToast('⤳ Hoppet: ' + task.id);
renderCurrent();
}
function nextTask() { const t = T(); t.currentIdx = Math.min(t.currentIdx + 1, Math.max(t.plan.length - 1, 0)); renderCurrent(); }
function prevTask() { const t = T(); t.currentIdx = Math.max(0, t.currentIdx - 1); renderCurrent(); }
function resetPlan() {
if (!confirm(`Reset ${state.activeThread}-tråd — alle ferdig/hoppet-merker fjernes. Sikker?`)) return;
const t = T();
t.currentIdx = 0;
t.done = [];
t.skipped = [];
renderCurrent();
}
function quickAddTask() {
const input = document.getElementById('quick-add-input');
const v = input.value.trim();
if (!v) return;
const m = v.match(/^(\S+)\s*[—\-]\s*(.+)$/);
if (!m) { showToast('Format: ID — Navn'); return; }
T().plan.push({ id: m[1], name: m[2], owner: state.activeThread, gate: '—', est: '?', dep: 'manuell' });
input.value = '';
save();
renderCurrent();
showToast('+ Lagt til: ' + m[1]);
}
function renderPlanList() {
const t = T();
const list = document.getElementById('plan-list');
if (!t.plan.length) {
list.innerHTML = '<div style="color:var(--text-faint);font-size:11px;padding:12px;text-align:center;letter-spacing:0.5px;">INGEN OPPGAVER I DENNE TRÅDEN</div>';
return;
}
list.innerHTML = t.plan.map((task, idx) => {
let cls = '';
if (t.done.includes(task.id)) cls = 'done';
else if (idx === t.currentIdx) cls = 'current';
return `
<div class="plan-list-item ${cls}" onclick="jumpToTask(${idx})" style="cursor:pointer">
<div class="plan-list-id">${task.id} · ${task.owner}${task.ghIssue ? ' <a href="' + task.ghIssue.url + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--accent);text-decoration:none;font-size:10px;" title="Issue #' + task.ghIssue.number + '">🔗#' + task.ghIssue.number + '</a>' : ''}</div>
<div class="plan-list-name">${escapeHtml(task.name)}</div>
<div class="plan-list-meta">${task.gate} · ${task.est} · dep: ${task.dep}</div>
</div>
`;
}).join('');
}
function jumpToTask(idx) {
const t = T();
t.currentIdx = idx;
const task = t.plan[idx];
t.done = t.done.filter(id => id !== task.id);
t.skipped = t.skipped.filter(id => id !== task.id);
renderCurrent();
}
function copyCurrentAsClaudeBrief() {
const t = T();
const task = t.plan[t.currentIdx];
if (!task) return;
const text = `Aktiv oppgave (tråd ${state.activeThread}): ${task.id}${task.name}
Eier: ${task.owner}
HITL-gate: ${task.gate}
Estimat: ${task.est}
Avhengighet: ${task.dep}
Bekreft mottak og foreslå konkret første handling.${brandFooter('claude-brief')}`;
copyText(text);
}
function generateBootPrompt() {
const t = T();
const task = t.plan[t.currentIdx];
const taskBlock = task ? `AKTIV OPPGAVE:
- ID: ${task.id}
- Navn: ${task.name}
- Eier: ${task.owner}
- Gate: ${task.gate}
- Estimat: ${task.est}
- Avhengig: ${task.dep}` : 'AKTIV OPPGAVE: ingen (tom plan)';
const text = `═══════════════════════════════════════════════════════════
BOOT-PROMPT · TRÅD ${state.activeThread} · ${new Date().toISOString().slice(0,16).replace('T',' ')} CEST
═══════════════════════════════════════════════════════════
Kjør boot-protokoll:
1. git pull origin autoflow-lag-1
2. bash .vauco-skills/skills/gcp-cost-guard-vauco/scripts/preflight.sh
3. Bekreft i én linje: "Boot OK · branch <X> · HEAD <Y>"
${taskBlock}
VENTENDE IDEER I DENNE TRÅDEN (${t.ideas.length}):
${t.ideas.slice(0, 5).map(i => `- [${i.cat.toUpperCase()}] ${i.text}`).join('\n') || '(ingen)'}
START.${brandFooter('boot-prompt')}`;
copyText(text);
}
function logCompletionToLearnings() {
const t = T();
const task = t.plan[t.currentIdx];
if (!task) return;
const date = new Date().toISOString().slice(0,10);
const text = `LEARNING-AUTO | ${date} | ${task.owner} | [${state.activeThread}] Fullført ${task.id}: ${task.name}
Append til docs/LEARNINGS.md på autoflow-lag-1:
- Hva fungerte: [fyll inn]
- Hva som tok lengre tid enn estimert (${task.est}): [fyll inn]
- Konkret regel for fremtiden: [fyll inn]
git add docs/LEARNINGS.md
git commit -m "docs(learnings): append entry for ${task.id} [${state.activeThread}]"
git push origin autoflow-lag-1${brandFooter('learnings-log')}`;
copyText(text);
}
// ─── IDEAS (per thread) ──────────────────────────────────────
function addIdea() {
const t = T();
const input = document.getElementById('idea-input');
const text = input.value.trim();
if (!text) return;
const ts = new Date().toISOString().slice(0,16).replace('T', ' ');
const linkedTask = t.plan[t.currentIdx] ? t.plan[t.currentIdx].id : '—';
t.ideas.unshift({ text, cat: t.selectedCat || 'note', ts, linkedTask });
input.value = '';
save();
renderIdeas();
showToast('💡 Idé fanget i ' + state.activeThread);
}
function deleteIdea(idx) {
T().ideas.splice(idx, 1);
save();
renderIdeas();
}
function renderIdeas() {
const t = T();
const list = document.getElementById('idea-list');
if (!t.ideas.length) {
list.innerHTML = '<div style="color:var(--text-faint);font-size:11px;padding:12px;text-align:center;letter-spacing:0.5px;">INGEN IDEER I ' + state.activeThread + '</div>';
} else {
list.innerHTML = t.ideas.map((i, idx) => {
const ghIcon = i.ghIssue ? `<a href="${i.ghIssue.url}" target="_blank" style="color:var(--accent);text-decoration:none;" title="Åpen issue #${i.ghIssue.number}">🐙 #${i.ghIssue.number}</a>` : `<button class="idea-del" onclick="openIssuePreview(${idx})" style="position:static;color:var(--idea);font-size:11px;background:none;border:none;cursor:pointer;letter-spacing:0.5px;" title="Push til GitHub som issue">→🐙</button>`;
return `
<div class="idea-item cat-${i.cat}">
<div class="idea-meta">
<span>${i.cat.toUpperCase()} · ${i.linkedTask}</span>
<span>${i.ts}</span>
</div>
<div class="idea-text">${escapeHtml(i.text)}</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;">
${ghIcon}
<button class="idea-del" onclick="deleteIdea(${idx})" style="position:static;">×</button>
</div>
</div>
`;
}).join('');
}
// Set category buttons active state
document.querySelectorAll('.cat-btn').forEach(b => {
b.classList.toggle('active', b.dataset.cat === (t.selectedCat || 'note'));
});
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function exportIdeas(target) {
const t = T();
if (!t.ideas.length) { showToast('Ingen ideer å eksportere'); return; }
const groups = {};
t.ideas.forEach(i => { (groups[i.cat] ||= []).push(i); });
const targetName = {
'ops': 'OPS-Computer-Hub',
'plan': 'PLAN-Claude-Architect',
'learnings': 'docs/LEARNINGS.md (append)'
}[target];
let md = `## Suggestions fra Chris til ${targetName}\nKilde: tråd ${state.activeThread} (${t.desc || ''}). Dato: ${new Date().toISOString().slice(0,10)}\n\n`;
for (const cat of Object.keys(groups)) {
md += `### ${cat.toUpperCase()}\n`;
groups[cat].forEach(i => { md += `- [${i.linkedTask}] ${i.text} _(${i.ts})_\n`; });
md += '\n';
}
if (target === 'learnings') {
md += `Format hvert punkt som: LEARNING-NNN | ${new Date().toISOString().slice(0,10)} | <kategori> | <observasjon> | <regel>`;
} else {
md += `Kategoriser, vurder, og foreslå hvilke som inn i ROADMAP/ARCHITECTURE eller forkastes.`;
}
md += brandFooter('ideas-export → ' + target);
copyText(md);
}
function clearIdeas() {
const t = T();
if (!confirm('Slette alle ' + t.ideas.length + ' ideer i ' + state.activeThread + '?')) return;
t.ideas = [];
save();
renderIdeas();
}
// ─── FAVORITES (global) ──────────────────────────────────────
function toggleFavorite(cmd, e) {
if (e) e.stopPropagation();
const idx = state.favorites.indexOf(cmd);
if (idx >= 0) {
state.favorites.splice(idx, 1);
showToast('★ Fjernet favoritt');
} else {
state.favorites.push(cmd);
showToast('★ Lagt til favoritt');
}
save();
renderFavorites();
updateStars();
}
function renderFavorites() {
const section = document.getElementById('favorites-section');
const grid = document.getElementById('favorites-grid');
if (!state.favorites.length) {
section.style.display = 'none';
return;
}
section.style.display = '';
grid.innerHTML = state.favorites.map(cmd => {
const original = document.querySelector(`.btn[data-cmd="${cmd.replace(/"/g, '\\"')}"]`);
const cls = original ? Array.from(original.classList).filter(c => c !== 'btn').join(' ') : '';
const desc = original ? original.querySelector('.desc').textContent : '';
return `
<button class="btn ${cls}" data-cmd="${escapeHtml(cmd)}">
<div class="cmd">${escapeHtml(cmd)}</div>
<div class="desc">${escapeHtml(desc)}</div>
</button>
`;
}).join('');
grid.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', () => copyText(btn.dataset.cmd, btn));
});
}
function updateStars() {
document.querySelectorAll('section:not(.favorites-section) .btn').forEach(btn => {
let star = btn.querySelector('.fav-star');
if (!star) {
star = document.createElement('button');
star.className = 'fav-star';
star.innerHTML = '★';
star.addEventListener('click', (e) => toggleFavorite(btn.dataset.cmd, e));
btn.appendChild(star);
}
star.classList.toggle('active', state.favorites.includes(btn.dataset.cmd));
});
}
// ─── HISTORY (per thread) ────────────────────────────────────
function addToHistory(cmd) {
const t = T();
t.history.unshift({ cmd, ts: new Date().toISOString().slice(11,16) });
if (t.history.length > 20) t.history.pop();
save();
renderHistory();
}
function renderHistory() {
const t = T();
const list = document.getElementById('history-list');
if (!t.history.length) {
list.innerHTML = '<div style="color:var(--text-faint);font-size:11px;padding:12px;text-align:center;letter-spacing:0.5px;">INGEN HISTORIKK I ' + state.activeThread + '</div>';
return;
}
list.innerHTML = t.history.map(h => `
<div class="history-item" onclick="copyText('${escapeHtml(h.cmd).replace(/'/g, "\\'")}')">
<div class="history-cmd">${escapeHtml(h.cmd.length > 50 ? h.cmd.slice(0,50)+'...' : h.cmd)}</div>
<div class="history-time">${h.ts}</div>
</div>
`).join('');
}
function clearHistory() {
const t = T();
if (!confirm('Tøm historikk i ' + state.activeThread + '?')) return;
t.history = [];
save();
renderHistory();
}
// ─── COPY ────────────────────────────────────────────────────
// ─── PROMPT LIBRARY MODAL ──────────────────────────────────────
function openPromptLibrary() {
const body = document.getElementById('prompt-library-body');
const grouped = {};
Object.entries(PROMPT_LIBRARY).forEach(([key, p]) => {
if (!grouped[p.thread]) grouped[p.thread] = [];
grouped[p.thread].push({ key, ...p });
});
const threadColors = { PCP: '#00d4a8', EXEC: '#e6a817', OPS: '#20b2aa', PLAN: '#8ab4f8', NMT: '#b39ddb' };
let html = '';
Object.keys(grouped).forEach(threadKey => {
const color = threadColors[threadKey] || '#888';
html += `<div style="margin-bottom:16px;">
<div style="font-size:11px; font-weight:600; color:${color}; letter-spacing:0.5px; margin-bottom:6px;">● ${threadKey}</div>`;
grouped[threadKey].forEach(p => {
const preview = p.text.split('\n')[0].substring(0, 80);
html += `<div onclick="copyPromptFromLibrary('${p.key}', this)"
style="padding:10px 12px; margin-bottom:6px; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.08); border-left:3px solid ${color}; border-radius:6px; cursor:pointer; transition:background 0.15s;"
onmouseover="this.style.background='rgba(255,255,255,0.06)'"
onmouseout="this.style.background='rgba(255,255,255,0.03)'">
<div style="font-size:13px; font-weight:500; margin-bottom:3px;">${p.label}</div>
<div style="font-size:11px; opacity:0.6;">${preview}…</div>
</div>`;
});
html += '</div>';
});
body.innerHTML = html;
document.getElementById('prompt-library-modal').classList.add('show');
}
function closePromptLibrary() {
document.getElementById('prompt-library-modal').classList.remove('show');
}
function copyPromptFromLibrary(key, el) {
const p = PROMPT_LIBRARY[key];
if (!p) return;
navigator.clipboard.writeText(p.text).then(() => {
const orig = el.style.background;
const origBorder = el.style.borderLeft;
el.style.background = 'rgba(0, 212, 168, 0.18)';
el.style.borderLeft = '3px solid #00d4a8';
const label = el.querySelector('div');
const origLabel = label.textContent;
label.textContent = '✓ Kopiert til utklipp';
setTimeout(() => {
el.style.background = orig;
el.style.borderLeft = origBorder;
label.textContent = origLabel;
}, 1200);
});
}
function copyText(t, btn) {
navigator.clipboard.writeText(t).then(() => {
addToHistory(t);
showToast(t.length > 40 ? '✓ Kopiert (' + t.length + ' tegn)' : '✓ ' + t);
if (btn) showCheckmark(btn);
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = t;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
addToHistory(t);
showToast('✓ Kopiert');
if (btn) showCheckmark(btn);
});
}
function showCheckmark(btn) {
let check = btn.querySelector('.copy-check');
if (!check) {
check = document.createElement('span');
check.className = 'copy-check';
check.textContent = '✓';
btn.appendChild(check);
}
check.classList.add('show');
setTimeout(() => check.classList.remove('show'), 800);
}
const toast = document.getElementById('toast');
function showToast(text) {
toast.textContent = text;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1500);
}
// ─── BUTTON LISTENERS ───────────────────────────────────────
function attachButtons() {
document.querySelectorAll('section:not(.favorites-section) .btn').forEach(btn => {
if (btn.dataset.attached) return;
btn.dataset.attached = '1';
btn.addEventListener('click', (e) => {
if (e.target.classList.contains('fav-star')) return;
copyText(btn.dataset.cmd, btn);
});
});
updateStars();
}
// Categories
document.querySelectorAll('.cat-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
T().selectedCat = btn.dataset.cat;
save();
});
});
// Side panel tabs
document.querySelectorAll('.tab-row .tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-row .tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.querySelector(`[data-tab-content="${btn.dataset.tab}"]`).classList.add('active');
});
});
// ─── STATS ───────────────────────────────────────────────────
function renderStats() {
const t = T();
const totalIdeas = Object.values(state.threads).reduce((a, x) => a + x.ideas.length, 0);
document.getElementById('stats').innerHTML = `
<span>[${state.activeThread}]</span>
<span>✓ <strong>${t.done.length}</strong> ferdig</span>
<span>⤳ <strong>${t.skipped.length}</strong> hoppet</span>
<span>💡 <strong>${t.ideas.length}</strong>/${totalIdeas} ideer</span>
<span>★ <strong>${state.favorites.length}</strong> fav</span>
<span>🧵 <strong>${Object.keys(state.threads).length}</strong> tråder</span>
`;
document.getElementById('panel-context').innerHTML = `TRÅD: <strong style="color:${t.color}">${state.activeThread}</strong>${t.desc ? ' · ' + escapeHtml(t.desc) : ''}`;
document.getElementById('panel-context').style.borderLeftColor = t.color;
}
// ─── SEARCH ──────────────────────────────────────────────────
const search = document.getElementById('search');
search.addEventListener('input', (e) => {
const q = e.target.value.toLowerCase().trim();
document.querySelectorAll('section .btn').forEach(btn => {
btn.style.display = (!q || btn.textContent.toLowerCase().includes(q)) ? '' : 'none';
});
});
// ─── THEME / COMPACT / ANIMATION ─────────────────────────────
function toggleTheme() {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
document.body.classList.toggle('light', state.theme === 'light');
save();
}
function toggleCompact() {
state.compact = !state.compact;
document.body.classList.toggle('compact', state.compact);
save();
}
function toggleAnimation() {
state.animate = !state.animate;
document.getElementById('plan-anchor').classList.toggle('animate', state.animate);
save();
}
// ─── KEYBOARD ────────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
const active = document.activeElement.tagName;
const isInput = active === 'INPUT' || active === 'TEXTAREA';
const modalOpen = document.getElementById('thread-modal').classList.contains('show');
if (modalOpen && e.key === 'Escape') { closeThreadModal(); return; }
if (document.getElementById('tasr-modal').classList.contains('show') && e.key === 'Escape') { closeTasr(); return; }
if (document.getElementById('prov-modal').classList.contains('show') && e.key === 'Escape') { closeProvisioningModal(); return; }
if (modalOpen && e.key === 'Enter' && document.activeElement.id !== 'thread-desc') { e.preventDefault(); createThread(); return; }
if (modalOpen) return;
if (e.ctrlKey && e.key.toLowerCase() === 'k') { e.preventDefault(); search.focus(); search.select(); }
if (e.key === 'Escape') {
if (document.activeElement === search) { search.value = ''; search.dispatchEvent(new Event('input')); search.blur(); }
if (document.activeElement.id === 'idea-input') { document.getElementById('idea-input').value = ''; document.getElementById('idea-input').blur(); }
}
if (e.ctrlKey && e.key.toLowerCase() === 'i') { e.preventDefault(); document.getElementById('idea-input').focus(); }
if (e.ctrlKey && e.key === 'ArrowRight') { e.preventDefault(); nextTask(); }
if (e.ctrlKey && e.key === 'ArrowLeft') { e.preventDefault(); prevTask(); }
if (e.ctrlKey && e.key.toLowerCase() === 'd') { e.preventDefault(); completeCurrent(); }
if (e.ctrlKey && e.key.toLowerCase() === 'b') { e.preventDefault(); generateBootPrompt(); }
// Ctrl+Tab → switch thread
if (e.ctrlKey && e.key === 'Tab') {
e.preventDefault();
const keys = Object.keys(state.threads);
const i = keys.indexOf(state.activeThread);
switchThread(keys[(i + 1) % keys.length]);
}
});
document.getElementById('idea-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addIdea(); }
});
document.getElementById('quick-add-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); quickAddTask(); }
});
document.getElementById('thread-desc').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); createThread(); }
});
document.getElementById('thread-modal').addEventListener('click', (e) => {
if (e.target.id === 'thread-modal') closeThreadModal();
});
// ─── INIT ────────────────────────────────────────────────────
// ═══ GITHUB INTEGRATION ═══════════════════════════════════════════
state.gh = state.gh || { token: null, owner: '', repo: '', branch: 'main', cache: null, cacheTs: null, threadIssues: [] };
function ghConfigured() { return state.gh && state.gh.token && state.gh.owner && state.gh.repo; }
function updateGhStatus() {
const dot = document.getElementById('gh-status-dot');
if (!dot) return;
dot.style.background = ghConfigured() ? '#10B981' : '#e05050';
document.getElementById('gh-not-configured').style.display = ghConfigured() ? 'none' : '';
document.getElementById('gh-configured').style.display = ghConfigured() ? '' : 'none';
if (ghConfigured()) {
document.getElementById('gh-repo-label').textContent = `${state.gh.owner}/${state.gh.repo}`;
document.getElementById('gh-branch-label').textContent = state.gh.branch;
}
}
function openGhSettings() {
document.getElementById('gh-token-input').value = state.gh.token || '';
document.getElementById('gh-owner-input').value = state.gh.owner || 'chrischristiansen-glitch';
document.getElementById('gh-repo-input').value = state.gh.repo || 'vauco-os';
// Branch-default per repo
const branchDefaults = { 'vauco-os': 'autoflow-lag-1', 'vauco-bootstrap': 'main', 'vauco-gemini-tui-bridge': 'main' };
document.getElementById('gh-repo-input').onchange = (e) => {
const def = branchDefaults[e.target.value] || 'main';
document.getElementById('gh-branch-input').value = def;
};
document.getElementById('gh-branch-input').value = state.gh.branch || 'autoflow-lag-1';
document.getElementById('gh-modal').classList.add('show');
}
function closeGhSettings() { document.getElementById('gh-modal').classList.remove('show'); }
function clearGhConfig() {
if (!confirm('Slett GitHub-token og config?')) return;
state.gh = { token: null, owner: '', repo: '', branch: 'main', cache: null, cacheTs: null, threadIssues: [] };
save();
updateGhStatus();
closeGhSettings();
showToast('GitHub-config slettet');
}
async function saveGhConfig() {
const token = document.getElementById('gh-token-input').value.trim();
const owner = document.getElementById('gh-owner-input').value.trim();
const repo = document.getElementById('gh-repo-input').value.trim();
const branch = document.getElementById('gh-branch-input').value.trim() || 'main';
if (!token || !owner || !repo) { showToast('Token, eier og repo må fylles ut'); return; }
try {
const r = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers: ghHeaders(token) });
if (!r.ok) { showToast('✗ Token/repo virker ikke: ' + r.status); return; }
} catch (e) { showToast('✗ Nettverksfeil: ' + e.message); return; }
state.gh = { token, owner, repo, branch, cache: null, cacheTs: null, threadIssues: [] };
save();
updateGhStatus();
closeGhSettings();
showToast('✓ GitHub tilkoblet');
refreshGhContext();
}
function ghHeaders(t) {
return { 'Authorization': `Bearer ${t || state.gh.token}`, 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
}
async function ghApi(path, opts = {}) {
const r = await fetch(`https://api.github.com${path}`, { ...opts, headers: { ...ghHeaders(), ...(opts.headers || {}) } });
if (!r.ok) throw new Error(`GitHub ${r.status}: ${await r.text()}`);
return r.json();
}
async function fetchDocFile(path) {
try {
const r = await ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/contents/${path}?ref=${state.gh.branch}`);
return atob(r.content.replace(/\n/g, ''));
} catch (e) { return null; }
}
async function refreshGhContext() {
if (!ghConfigured()) { openGhSettings(); return; }
showToast('↻ Synkroniserer GitHub...');
try {
const [commits, prs, learnings, rulebook, roadmap] = await Promise.all([
ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/commits?sha=${state.gh.branch}&per_page=5`).catch(() => []),
ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/pulls?state=open&per_page=10`).catch(() => []),
fetchDocFile('docs/LEARNINGS.md'),
fetchDocFile('docs/AGENT_RULEBOOK.md'),
fetchDocFile('docs/VAUCO_OS_ROADMAP.md')
]);
state.gh.cache = { commits, prs, learnings, rulebook, roadmap };
state.gh.cacheTs = new Date().toISOString();
await refreshThreadIssues();
save();
renderGhPanel();
showToast(`✓ Sync OK · ${commits.length} commits · ${prs.length} PRs`);
} catch (e) {
showToast('✗ Sync feilet: ' + e.message);
}
}
async function refreshThreadIssues() {
const label = THREAD_LABEL[state.activeThread] || `thread:${state.activeThread.toLowerCase()}`;
try {
const issues = await ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/issues?state=open&labels=${label}&per_page=10`).catch(() => []);
state.gh.threadIssues = (issues || []).filter(i => !i.pull_request);
} catch (e) { state.gh.threadIssues = []; }
}
function renderGhPanel() {
if (!ghConfigured() || !state.gh.cache) return;
const c = state.gh.cache;
const commitsEl = document.getElementById('gh-commits');
if (!commitsEl) return;
commitsEl.innerHTML = (c.commits || []).map(cm => `
<div class="history-item" onclick="window.open('${cm.html_url}','_blank')">
<div class="history-cmd">${escapeHtml((cm.commit.message || '').split('\n')[0].slice(0, 60))}</div>
<div class="history-time">${cm.sha.slice(0,7)} · ${escapeHtml((cm.commit.author || {}).name || '?')}</div>
</div>
`).join('') || '<div style="color:var(--text-faint);font-size:11px;padding:8px;text-align:center;">INGEN COMMITS</div>';
document.getElementById('gh-prs').innerHTML = (c.prs || []).map(p => `
<div class="history-item" onclick="window.open('${p.html_url}','_blank')">
<div class="history-cmd">#${p.number} ${escapeHtml(p.title.slice(0,50))}</div>
<div class="history-time">${escapeHtml(p.head.ref)}${escapeHtml(p.base.ref)} · ${escapeHtml(p.user.login)}</div>
</div>
`).join('') || '<div style="color:var(--text-faint);font-size:11px;padding:8px;text-align:center;">INGEN ÅPNE PRS</div>';
document.getElementById('gh-issues').innerHTML = (state.gh.threadIssues || []).map(i => `
<div class="history-item" onclick="window.open('${i.html_url}','_blank')">
<div class="history-cmd">#${i.number} ${escapeHtml(i.title.slice(0,50))}</div>
<div class="history-time">${escapeHtml((i.labels || []).map(l => l.name).join(' · ')) || '—'}</div>
</div>
`).join('') || '<div style="color:var(--text-faint);font-size:11px;padding:8px;text-align:center;">INGEN ÅPNE ISSUES MED LABEL</div>';
}
// ═══ NEMOTRON ISSUE GENERATOR ═════════════════════════════════════
let pendingIssueIdx = -1;
function openIssuePreview(ideaIdx) {
if (!ghConfigured()) { showToast('Sett opp GitHub først'); openGhSettings(); return; }
const t = T();
const idea = t.ideas[ideaIdx];
if (!idea) return;
pendingIssueIdx = ideaIdx;
const task = t.plan[t.currentIdx];
const severity = SEVERITY_MAP[idea.cat] || 'low';
const cache = state.gh.cache || {};
const learningsAll = (cache.learnings || '').match(/LEARNING-\d+[^\n]*/g) || [];
const roadmapIds = (cache.roadmap || '').match(/D[1-9]-\d{3}/g) || [];
const words = idea.text.toLowerCase().split(/\s+/).filter(w => w.length > 4);
const relevantLearnings = learningsAll.filter(l => words.some(w => l.toLowerCase().includes(w))).slice(0, 3);
const relatedRoadmap = [...new Set(roadmapIds)].slice(0, 3);
const title = `[${state.activeThread}] ${idea.cat.toUpperCase()} · ${idea.text.slice(0, 60)}${idea.text.length > 60 ? '...' : ''}`;
const body = `[${state.activeThread}] · ${idea.cat.toUpperCase()} · ${idea.ts}
## CONTEXT
- Active task: ${task ? `${task.id}${task.name}` : '—'}
- Thread: ${state.activeThread}${T().desc ? ' (' + T().desc + ')' : ''}
- Related learnings: ${relevantLearnings.length ? relevantLearnings.join(', ') : '—'}
- Roadmap anchors: ${relatedRoadmap.length ? relatedRoadmap.join(', ') : '—'}
## OBSERVATION
${idea.text}
## PROPOSED ACTION
${proposeAction(idea)}
## GATE
${task ? task.gate : '—'}
## SEVERITY
${severity}${brandFooter('nemotron-issue · ' + (state.gh.cacheTs ? 'docs-cache ' + state.gh.cacheTs.slice(0,16) : 'no cache'))}`;
const labels = [
THREAD_LABEL[state.activeThread] || `thread:${state.activeThread.toLowerCase()}`,
`category:${idea.cat}`,
`severity:${severity}`
].join(', ');
document.getElementById('issue-title').value = title;
document.getElementById('issue-body').value = body;
document.getElementById('issue-labels').value = labels;
document.getElementById('issue-modal').classList.add('show');
}
function proposeAction(idea) {
const verbMap = { bug: 'FIX', risk: 'MITIGATE', arch: 'DESIGN', roadmap: 'PLAN', note: 'REVIEW' };
const verb = verbMap[idea.cat] || 'REVIEW';
const obj = idea.text.split(/[.!?\n]/)[0].slice(0, 60).trim();
return `${verb} ${obj} [${state.activeThread}]`;
}
function closeIssueModal() { document.getElementById('issue-modal').classList.remove('show'); pendingIssueIdx = -1; }
async function pushIssueToGh() {
if (pendingIssueIdx < 0) return;
const title = document.getElementById('issue-title').value.trim();
const body = document.getElementById('issue-body').value.trim();
const labels = document.getElementById('issue-labels').value.split(',').map(s => s.trim()).filter(Boolean);
if (!title) { showToast('Tittel mangler'); return; }
showToast('🚀 Pusher...');
try {
const issue = await ghApi(`/repos/${state.gh.owner}/${state.gh.repo}/issues`, {
method: 'POST',
body: JSON.stringify({ title, body, labels })
});
const t = T();
t.ideas[pendingIssueIdx].ghIssue = { number: issue.number, url: issue.html_url };
save();
renderIdeas();
closeIssueModal();
showToast(`✓ Issue #${issue.number} opprettet`);
refreshThreadIssues().then(renderGhPanel);
} catch (e) {
showToast('✗ Push feilet: ' + e.message);
}
}
document.getElementById('gh-modal').addEventListener('click', (e) => { if (e.target.id === 'gh-modal') closeGhSettings(); });
document.getElementById('issue-modal').addEventListener('click', (e) => { if (e.target.id === 'issue-modal') closeIssueModal(); });
// ═══ INIT ═════════════════════════════════════════════════════════
function renderAll() {
renderCurrent();
renderIdeas();
renderHistory();
renderStats();
renderGhPanel();
}
document.body.classList.toggle('light', state.theme === 'light');
document.body.classList.toggle('compact', state.compact);
document.getElementById('plan-anchor').classList.toggle('animate', state.animate);
// ─── ADMIN MODE ACTIVATE ─────────────────────────────────────
document.body.classList.toggle('admin-mode', VAUCO_ADMIN_MODE);
if (VAUCO_ADMIN_MODE) {
// Admin mode: make admin-only flex items visible
document.querySelectorAll('.admin-only').forEach(el => {
el.style.removeProperty('display');
});
}
attachButtons();
renderThreadBar();
renderAll();
renderFavorites();
updateGhStatus();
renderTasrHistory();
renderProvCustomers();
if (ghConfigured()) refreshGhContext();
// ═══ TASR — TAB-AWARENESS & SELF-REFLECTION ═══════════════════════════════
function openTasr() {
document.getElementById('tasr-modal').classList.add('show');
renderTasrHistory();
}
function closeTasr() {
document.getElementById('tasr-modal').classList.remove('show');
}
function generateMasterPrompt() {
const countRaw = parseInt(document.getElementById('tasr-tab-count').value) || 1;
const responsesRaw = document.getElementById('tasr-responses').value.trim();
const threads = responsesRaw ? responsesRaw.split('\n---\n').map(s => s.trim()).filter(Boolean) : [];
const count = Math.max(countRaw, threads.length || 1);
const ts = new Date().toISOString().slice(0,16).replace('T', ' ') + ' CEST';
// Extract one-line summaries
const summaries = [];
for (let i = 0; i < count; i++) {
const raw = threads[i] || '';
const firstLine = raw.split('\n').find(l => l.trim().length > 10) || raw.slice(0, 80) || '(tom)';
summaries.push(firstLine.trim().slice(0, 120));
}
// Detect priority by length + keywords
const priorityKeywords = ['kritisk','viktig','blocker','urgent','deploy','patent','deadline','asap','nå','fix','bug'];
const scored = summaries.map((s, i) => {
let score = s.length;
priorityKeywords.forEach(kw => { if (s.toLowerCase().includes(kw)) score += 200; });
return { i, s, score };
});
scored.sort((a, b) => b.score - a.score);
const top3 = scored.slice(0, 3).map(x => 'Tråd ' + (x.i + 1)).join(', ');
let prompt = '# MASTER-PROMPT generert ' + ts + '\n'
+ '## Aktive tråder: ' + count + '\n'
+ '## Kategorier:\n'
+ summaries.map((s, i) => '- [Tråd ' + (i+1) + ']: ' + s).join('\n') + '\n'
+ '## Anbefalt neste steg:\n'
+ scored.slice(0, Math.min(count, 5)).map((x, rank) => (rank+1) + '. [Tråd ' + (x.i+1) + '] ' + x.s.slice(0, 80)).join('\n') + '\n'
+ '## Master-instruks:\n'
+ 'Du skal fokusere på ' + top3 + '. Skip side-tracks. Følg HITL-gates.\n'
+ 'Komprimert kontekst fra alle aktive tråder er lastet ovenfor.\n'
+ 'Bekreft mottak med: "MASTER SYNC OK · ' + count + ' tråder lastet · klar for instruks."';
document.getElementById('tasr-output').textContent = prompt;
// Save to history (max 20)
const hist = JSON.parse(localStorage.getItem('tasr_history') || '[]');
hist.unshift({ ts: ts, count: count, prompt: prompt.slice(0, 300) + '...', full: prompt });
if (hist.length > 20) hist.splice(20);
localStorage.setItem('tasr_history', JSON.stringify(hist));
renderTasrHistory();
showToast('🔮 Master-Prompt generert');
}
function copyTasrOutput() {
var txt = document.getElementById('tasr-output').textContent;
if (!txt || txt.startsWith('—')) { showToast('Generer en prompt først'); return; }
copyText(txt);
}
function renderTasrHistory() {
var list = document.getElementById('tasr-history-list');
if (!list) return;
var hist = JSON.parse(localStorage.getItem('tasr_history') || '[]');
if (!hist.length) {
list.innerHTML = '<div style="color:var(--text-faint);font-size:11px;padding:8px;text-align:center;">INGEN HISTORIKK</div>';
return;
}
list.innerHTML = hist.slice(0, 5).map(function(h, i) {
return '<div class="tasr-history-item" onclick="loadTasrFromHistory(' + i + ')">'
+ '<div style="color:var(--accent);font-size:10px;margin-bottom:3px;">' + h.ts + ' · ' + h.count + ' tråd' + (h.count !== 1 ? 'er' : '') + '</div>'
+ '<div style="opacity:0.7;">' + h.prompt.slice(0, 100) + '...</div>'
+ '</div>';
}).join('');
}
function loadTasrFromHistory(idx) {
var hist = JSON.parse(localStorage.getItem('tasr_history') || '[]');
if (hist[idx]) {
document.getElementById('tasr-output').textContent = hist[idx].full || hist[idx].prompt;
}
}
function clearTasrHistory() {
if (!confirm('Tøm TASR-historikk?')) return;
localStorage.removeItem('tasr_history');
renderTasrHistory();
showToast('TASR-historikk tømt');
}
// ═══ PROVISIONING WIZARD ══════════════════════════════════════════════════
var provLicId = '';
function openProvisioning() {
loadProvState();
document.getElementById('prov-modal').classList.add('show');
updateProvProgress();
renderProvCustomers();
}
function closeProvisioningModal() {
saveProvState();
document.getElementById('prov-modal').classList.remove('show');
}
function saveProvState() {
var stateData = {
name: document.getElementById('prov-cust-name').value,
email: document.getElementById('prov-cust-email').value,
checks: [1,2,3,4].map(function(i) { return document.getElementById('prov-check-' + i).checked; }),
licId: provLicId
};
localStorage.setItem('prov_wizard_state', JSON.stringify(stateData));
}
function loadProvState() {
try {
var raw = JSON.parse(localStorage.getItem('prov_wizard_state') || 'null');
if (!raw) return;
document.getElementById('prov-cust-name').value = raw.name || '';
document.getElementById('prov-cust-email').value = raw.email || '';
if (raw.checks) {
[1,2,3,4].forEach(function(n, i) {
document.getElementById('prov-check-' + n).checked = raw.checks[i] || false;
});
}
if (raw.licId) {
provLicId = raw.licId;
var disp = document.getElementById('prov-lic-display');
var btn = document.getElementById('prov-lic-copy');
disp.textContent = provLicId;
disp.style.display = '';
btn.style.display = '';
}
} catch(e) {}
}
function resetProvisioning() {
if (!confirm('Reset wizard? Lagret state slettes.')) return;
localStorage.removeItem('prov_wizard_state');
provLicId = '';
document.getElementById('prov-cust-name').value = '';
document.getElementById('prov-cust-email').value = '';
[1,2,3,4].forEach(function(n) { document.getElementById('prov-check-' + n).checked = false; });
var disp = document.getElementById('prov-lic-display');
var btn = document.getElementById('prov-lic-copy');
disp.style.display = 'none';
btn.style.display = 'none';
updateProvProgress();
showToast('Wizard reset');
}
function updateProvProgress() {
var checks = [1,2,3,4].map(function(i) { return document.getElementById('prov-check-' + i).checked; });
var done = checks.filter(Boolean).length;
document.getElementById('prov-step-display').textContent = done;
document.getElementById('prov-progress-fill').style.width = (done / 4 * 100) + '%';
[1,2,3,4].forEach(function(n, i) {
var el = document.getElementById('prov-step-' + n);
el.classList.toggle('done', checks[i]);
el.classList.toggle('active', !checks[i] && (i === 0 || checks[i - 1]));
});
saveProvState();
if (done === 4) showToast('✓ Kunde fullt provisjonert!');
}
function generateLicId() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var id = 'LIC-';
for (var i = 0; i < 8; i++) id += chars[Math.floor(Math.random() * chars.length)];
provLicId = id;
var disp = document.getElementById('prov-lic-display');
var btn = document.getElementById('prov-lic-copy');
disp.textContent = id;
disp.style.display = '';
btn.style.display = '';
saveProvState();
showToast('✓ LIC-ID generert: ' + id);
}
function copyLicId() {
if (!provLicId) { showToast('Generer LIC-ID først'); return; }
copyText(provLicId);
}
function openWelcomeEmail() {
var name = document.getElementById('prov-cust-name').value.trim() || 'Kunde';
var email = document.getElementById('prov-cust-email').value.trim() || '';
var lic = provLicId || '[LIC-ID ikke generert ennå]';
var subject = encodeURIComponent('Velkommen til Threadstone — din Vauco-konto er klar');
var body = encodeURIComponent(
'Hei ' + name + ',\n\nVelkommen til Threadstone!\n\nDin lisensnøkkel: ' + lic + '\n\nLogg inn på: https://vauco.no/login\n\nHvis du har spørsmål, kontakt oss på support@vauco.no\n\nMed vennlig hilsen,\nChris Christiansen\nVauco AS · Org.nr 935 989 779\nPowered by Jason · Crafted by Vauco'
);
window.open('mailto:' + email + '?subject=' + subject + '&body=' + body, '_blank');
}
function markProvisioned() {
var name = document.getElementById('prov-cust-name').value.trim();
var email = document.getElementById('prov-cust-email').value.trim();
if (!name || !email) { showToast('Fyll inn navn og epost først'); return; }
if (!provLicId) { showToast('Generer LIC-ID først (steg 2)'); return; }
var list = JSON.parse(localStorage.getItem('provisioned_customers') || '[]');
var entry = {
name: name,
email: email,
licId: provLicId,
ts: new Date().toISOString(),
tsDisplay: new Date().toLocaleString('no-NO', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' })
};
list.unshift(entry);
localStorage.setItem('provisioned_customers', JSON.stringify(list));
document.getElementById('prov-check-4').checked = true;
updateProvProgress();
renderProvCustomers();
showToast('✓ Kunde provisjonert: ' + name);
localStorage.removeItem('prov_wizard_state');
}
function renderProvCustomers() {
var el = document.getElementById('prov-customers-list');
if (!el) return;
var list = JSON.parse(localStorage.getItem('provisioned_customers') || '[]');
if (!list.length) { el.innerHTML = ''; return; }
el.innerHTML = '<div style="color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:8px;margin-top:4px;">PROVISJONERTE KUNDER (' + list.length + ')</div>'
+ list.slice(0, 5).map(function(c) {
return '<div class="prov-customer-entry">'
+ '<span>' + c.name + ' &lt;' + c.email + '&gt;</span>'
+ '<span style="color:var(--accent);font-size:10px;">' + c.licId + ' · ' + c.tsDisplay + '</span>'
+ '</div>';
}).join('');
}
// ═══ GITHUB ISSUES-SYNK ═══════════════════════════════════════════════════
async function syncIssues() {
if (!ghConfigured()) { showToast('Sett opp GitHub-token først'); openGhSettings(); return; }
var t = T();
if (!t.plan.length) { showToast('Ingen oppgaver i denne tråden'); return; }
showToast('🔄 Synkroniserer issues...');
var label = THREAD_LABEL[state.activeThread] || ('thread:' + state.activeThread.toLowerCase());
var threadName = state.activeThread;
var existingIssues = [];
try {
existingIssues = await ghApi('/repos/' + state.gh.owner + '/' + state.gh.repo + '/issues?state=open&labels=' + encodeURIComponent(label) + '&per_page=50');
existingIssues = (existingIssues || []).filter(function(i) { return !i.pull_request; });
} catch(e) {
showToast('✗ Kunne ikke hente eksisterende issues: ' + e.message);
return;
}
var existingByTaskId = {};
existingIssues.forEach(function(issue) {
var m = issue.title.match(/^\[([^\]]+)\]/);
if (m) existingByTaskId[m[1]] = issue;
});
var created = 0;
var existed = 0;
var errors = [];
for (var idx = 0; idx < t.plan.length; idx++) {
var task = t.plan[idx];
if (existingByTaskId[task.id]) {
if (!task.ghIssue) {
task.ghIssue = { number: existingByTaskId[task.id].number, url: existingByTaskId[task.id].html_url };
}
existed++;
continue;
}
if (task.ghIssue && task.ghIssue.number) { existed++; continue; }
var issueTitle = '[' + task.id + '] ' + task.name;
var issueBody = '**Tråd**: ' + threadName + '\n**Eier**: ' + task.owner + '\n**Gate**: ' + task.gate + '\n**Estimat**: ' + task.est + '\n**Avhengighet**: ' + task.dep + '\n\n---\n*Auto-opprettet fra Vauco OS Multi-Thread Hub v3.3*';
try {
var issue = await ghApi('/repos/' + state.gh.owner + '/' + state.gh.repo + '/issues', {
method: 'POST',
body: JSON.stringify({ title: issueTitle, body: issueBody, labels: [label] })
});
task.ghIssue = { number: issue.number, url: issue.html_url };
created++;
} catch(e) {
errors.push(task.id + ': ' + e.message);
}
await new Promise(function(r) { setTimeout(r, 120); });
}
save();
renderPlanList();
var msg = '✓ Opprettet ' + created + ' issues, ' + existed + ' fantes fra før';
if (errors.length) msg += ' · ' + errors.length + ' feil';
showToast(msg);
refreshThreadIssues().then(renderGhPanel);
}
</script>
<!-- ═══ TASR MODAL ═══════════════════════════════════════════════ -->
<div class="modal-backdrop" id="tasr-modal" onclick="if(event.target===this)closeTasr()">
<div class="tasr-modal-inner">
<h3>🧠 TASR — TAB-AWARENESS &amp; SELF-REFLECTION</h3>
<label style="display:block;color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:6px;">ANTALL PERPLEXITY-FANER ÅPNE NÅ</label>
<input type="number" id="tasr-tab-count" min="1" max="20" value="1"
style="width:100%;background:var(--bg);border:1px solid var(--line2);border-radius:8px;padding:9px 12px;color:var(--text);font-family:var(--mono);font-size:13px;margin-bottom:14px;">
<label style="display:block;color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:6px;">LIM INN SISTE AI-RESPONS FRA HVER FANE (SEPARER MED ---)</label>
<textarea id="tasr-responses" rows="6" placeholder="Svar fra fane 1...
---
Svar fra fane 2...
---
Svar fra fane 3..."></textarea>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;">
<button class="modal-btn primary" onclick="generateMasterPrompt()">🔮 Generer Master-Prompt</button>
<button class="modal-btn" onclick="copyTasrOutput()">📋 Kopier</button>
<button class="modal-btn" onclick="closeTasr()" style="margin-left:auto;">✕ Lukk</button>
</div>
<div style="margin-top:14px;">
<div style="color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:6px;">GENERERT MASTER-PROMPT</div>
<div class="tasr-output" id="tasr-output">— Fyll inn faner og svar, trykk Generer —</div>
</div>
<div style="margin-top:18px;" id="tasr-history-section">
<div style="color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
<span>TIDLIGERE MASTER-PROMPTS</span>
<button class="modal-btn" onclick="clearTasrHistory()" style="padding:3px 8px;font-size:9px;">🗑 Tøm</button>
</div>
<div id="tasr-history-list"></div>
</div>
</div>
</div>
<!-- ═══ PROVISIONING WIZARD MODAL ════════════════════════════════ -->
<div class="modal-backdrop" id="prov-modal" onclick="if(event.target===this)closeProvisioningModal()">
<div class="prov-modal-inner">
<h3>👤 PROVISJONER NY KUNDE</h3>
<div style="color:var(--text-faint);font-size:10px;letter-spacing:0.5px;margin-bottom:12px;">Steg <span id="prov-step-display">0</span> av 4 fullført</div>
<div class="prov-progress-track">
<div class="prov-progress-fill" id="prov-progress-fill" style="width:0%"></div>
</div>
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;">
<div style="flex:1;min-width:140px;">
<label style="display:block;color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:5px;">KUNDENAVN</label>
<input type="text" id="prov-cust-name" placeholder="Ola Nordmann"
style="width:100%;background:var(--bg);border:1px solid var(--line2);border-radius:7px;padding:8px 11px;color:var(--text);font-family:var(--mono);font-size:12px;">
</div>
<div style="flex:1;min-width:160px;">
<label style="display:block;color:var(--text-faint);font-size:10px;letter-spacing:0.8px;margin-bottom:5px;">KUNDE-EPOST</label>
<input type="email" id="prov-cust-email" placeholder="ola@bedrift.no"
style="width:100%;background:var(--bg);border:1px solid var(--line2);border-radius:7px;padding:8px 11px;color:var(--text);font-family:var(--mono);font-size:12px;">
</div>
</div>
<!-- Steg 1 -->
<div class="prov-step" id="prov-step-1">
<div class="prov-step-header">
<div class="prov-step-num">1</div>
<div class="prov-step-title">Opprett bruker i Google Workspace</div>
</div>
<div class="prov-step-desc">Opprett ny bruker-konto i Google Admin Console for kunden.</div>
<div class="prov-step-actions">
<a href="https://admin.google.com/ac/users/add" target="_blank" rel="noopener">
<button class="prov-action-btn primary" type="button">🔗 Åpne Google Admin</button>
</a>
<label class="prov-check-label">
<input type="checkbox" id="prov-check-1" onchange="updateProvProgress()"> ✓ Ferdig
</label>
</div>
</div>
<!-- Steg 2 -->
<div class="prov-step" id="prov-step-2">
<div class="prov-step-header">
<div class="prov-step-num">2</div>
<div class="prov-step-title">Generer LIC-ID</div>
</div>
<div class="prov-step-desc">Opprett unik lisensnøkkel for kunden. Lagres automatisk.</div>
<div class="prov-step-actions">
<button class="prov-action-btn primary" onclick="generateLicId()" type="button">⚡ Generer LIC-ID</button>
<div class="prov-lic-display" id="prov-lic-display" style="display:none;"></div>
<button class="prov-action-btn" id="prov-lic-copy" onclick="copyLicId()" style="display:none;" type="button">📋 Kopier</button>
<label class="prov-check-label">
<input type="checkbox" id="prov-check-2" onchange="updateProvProgress()"> ✓ Ferdig
</label>
</div>
</div>
<!-- Steg 3 -->
<div class="prov-step" id="prov-step-3">
<div class="prov-step-header">
<div class="prov-step-num">3</div>
<div class="prov-step-title">Send velkomst-epost</div>
</div>
<div class="prov-step-desc">Send forhåndsutfylt velkomst-epost til kunden med LIC-ID og instruksjoner.</div>
<div class="prov-step-actions">
<button class="prov-action-btn primary" onclick="openWelcomeEmail()" type="button">📧 Åpne e-post</button>
<label class="prov-check-label">
<input type="checkbox" id="prov-check-3" onchange="updateProvProgress()"> ✓ Ferdig
</label>
</div>
</div>
<!-- Steg 4 -->
<div class="prov-step" id="prov-step-4">
<div class="prov-step-header">
<div class="prov-step-num">4</div>
<div class="prov-step-title">Marker som provisjonert</div>
</div>
<div class="prov-step-desc">Lagrer kundeinfo i systemet. Denne handlingen er ikke reversibel uten manuell redigering.</div>
<div class="prov-step-actions">
<button class="prov-action-btn primary" onclick="markProvisioned()" type="button">✅ Marker fullført</button>
<label class="prov-check-label">
<input type="checkbox" id="prov-check-4" onchange="updateProvProgress()"> ✓ Ferdig
</label>
</div>
</div>
<div class="prov-customers-list" id="prov-customers-list"></div>
<div style="display:flex;gap:8px;margin-top:18px;justify-content:flex-end;">
<button class="modal-btn" onclick="resetProvisioning()">↺ Reset</button>
<button class="modal-btn" onclick="closeProvisioningModal()">✕ Lukk</button>
</div>
</div>
</div>
</body>
</html>