2887 lines
116 KiB
HTML
2887 lines
116 KiB
HTML
<!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 & 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 (1–7) · 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 <X></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 <Y></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 <fil></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 <tekst></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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 + ' <' + c.email + '></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 & 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>
|