netops/netops-todo-node/public/index.html
Jon Vanvik 3f69cd4248 Auth-herding: roller, kontoadmin, token-revokering + søkbar planliste + neon
Auth (app-modus):
- Roller admin/standard. Admin oppretter/sletter kontoer, resetter passord,
  endrer roller (konto-modal). Eksisterende brukere → admin ved migrering;
  bootstrap-bruker via proxy-bypass → admin. Siste admin vernet.
- Tilbakekalling via per-bruker epoke i tokenet: passordbytte, admin-reset,
  «Logg ut alle enheter» og sletting dreper alle aktive sesjoner. Token-format
  endret (3- → 4-delt) — alle logges ut én gang ved oppgradering.

Dagsplan: søkbar/velgbar planliste («📋 Planer») avledet fra planIndex —
finn plan på navn/dato/person uten å gjette dato.

GUI: login-neon trukket inn i hovedappen (header/knapper/KPI-kanter), og
login-siden fikk mer tech (HUD-hjørner, glitch-tittel, scanlines).

40 integrasjonstester (6 nye), alle grønne. README + CLAUDE.md oppdatert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 14:56:39 +02:00

387 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetOps To-Do · Network Engineer</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1"></script>
</head>
<body>
<div class="container">
<header>
<div>
<h1><span id="conn-dot" title="Tilkoblingsstatus"></span> NetOps To-Do</h1>
<div class="sub" id="conn-text">kobler til …</div>
</div>
<div class="header-actions">
<span class="online-row" id="online-row" title="Pålogget nå"></span>
<span class="user-chip" id="user-chip" title="Pålogget bruker"></span>
<button id="fieldBtn" title="Feltmodus — større knapper for mobil (f)">📱 Felt</button>
<button id="activityBtn" title="Aktivitetslogg (a)">☰ Aktivitet</button>
<button id="exportBtn" title="Eksporter JSON"></button>
<button id="importBtn" title="Importer JSON (erstatter alt)"></button>
<input type="file" id="importFile" accept=".json" style="display:none">
<button class="primary" id="newTaskBtn" title="Ny oppgave (n)">+ Ny oppgave</button>
</div>
</header>
<div class="anon-banner" id="anon-banner">
<span>Du er pålogget som <b>anon</b>. Velg et navn så kollegene ser hvem som gjør hva.</span>
<button class="primary" id="setNameBtn">Sett navn</button>
</div>
<div class="quickadd">
<div class="quickadd-row">
<input type="text" id="quickadd-input" autocomplete="off"
placeholder='Hurtig-registrering: f.eks. "Bytt SFP i core-sw01 #l1 p2 @me !imorgen ~1h loc:DC-OSL-1"'>
<button class="primary" id="quickadd-btn">Legg til</button>
</div>
<div class="quickadd-preview" id="quickadd-preview"></div>
<div class="quickadd-hint">#kategori-eller-tag · @eier (@me) · p1p4 · !frist (2026-06-20, +3d, idag, imorgen) · ~timer · loc:sted · resten blir tittel</div>
</div>
<section class="kpi-row">
<div class="kpi">
<div class="kpi-label">Åpne oppgaver</div>
<div class="kpi-value" id="kpi-open">0</div>
<div class="kpi-sub" id="kpi-open-sub">av 0 totalt</div>
</div>
<div class="kpi kpi-me">
<div class="kpi-label">Mine åpne</div>
<div class="kpi-value" id="kpi-mine">0</div>
<div class="kpi-sub" id="kpi-mine-sub">tildelt meg</div>
</div>
<div class="kpi kpi-p1">
<div class="kpi-label">P1 Kritisk</div>
<div class="kpi-value" id="kpi-p1">0</div>
<div class="kpi-sub">umiddelbar handling</div>
</div>
<div class="kpi kpi-warn">
<div class="kpi-label">Est. tid igjen</div>
<div class="kpi-value" id="kpi-hours">0t</div>
<div class="kpi-sub" id="kpi-hours-sub">arbeidstimer</div>
</div>
<div class="kpi">
<div class="kpi-label">Forfalte</div>
<div class="kpi-value" id="kpi-overdue">0</div>
<div class="kpi-sub">over frist</div>
</div>
<div class="kpi kpi-ok">
<div class="kpi-label">Ferdig (7d)</div>
<div class="kpi-value" id="kpi-done">0</div>
<div class="kpi-sub">siste 7 dager</div>
</div>
</section>
<section class="chart-row" id="chart-row">
<div class="panel">
<h3>Åpne oppgaver per kategori <span id="cat-total" style="color:var(--text-faint);font-family:var(--mono);text-transform:none;letter-spacing:0"></span></h3>
<div class="chart-wrap"><canvas id="catChart"></canvas></div>
</div>
<div class="panel">
<h3>Fordeling etter prioritet</h3>
<div class="chart-wrap"><canvas id="prioChart"></canvas></div>
</div>
<div class="panel">
<h3>Åpne oppgaver per eier</h3>
<div class="chart-wrap"><canvas id="ownerChart"></canvas></div>
</div>
</section>
<div class="filters">
<div class="view-toggle">
<button id="view-board" title="Tavle (v)">▦ Tavle</button>
<button id="view-table" title="Tabell (v)">☰ Tabell</button>
<button id="view-plan" title="Dagsplan (p)">📅 Dagsplan</button>
</div>
<div class="filter-group" id="fg-status">
<label>Status</label>
<select id="f-status">
<option value="open">Åpne</option>
<option value="all">Alle</option>
<option value="todo">Å gjøre</option>
<option value="progress">Pågår</option>
<option value="blocked">Blokkert</option>
<option value="done">Ferdig</option>
</select>
</div>
<div class="filter-group">
<label>Kategori</label>
<select id="f-category"><option value="all">Alle</option></select>
</div>
<div class="filter-group">
<label>Prioritet</label>
<select id="f-priority">
<option value="all">Alle</option>
<option value="P1">P1 Kritisk</option>
<option value="P2">P2 Høy</option>
<option value="P3">P3 Middels</option>
<option value="P4">P4 Lav</option>
</select>
</div>
<div class="filter-group">
<label>Eier</label>
<select id="f-owner">
<option value="all">Alle</option>
<option value="__me__">Meg</option>
<option value="__unassigned__">Utildelt</option>
</select>
</div>
<input class="search-input" type="text" id="f-search" placeholder="Søk tittel, beskrivelse, lokasjon, tag … (/)">
<button id="clearFilters">Nullstill</button>
</div>
<section class="board" id="board" style="display:none"></section>
<section class="planner" id="planner" style="display:none">
<div class="plan-col" id="plan-available-col">
<div class="plan-col-head">
<h3>Tilgjengelige oppgaver <span class="col-meta" id="plan-avail-count"></span></h3>
<span class="plan-col-hint">klikk + for å planlegge</span>
</div>
<div class="plan-col-body" id="plan-available-body"></div>
</div>
<div class="plan-col plan-today">
<div class="plan-today-head">
<div class="plan-date-row">
<div class="plan-picker">
<button id="plan-picker-btn" type="button" title="Bla i / søk planer">📋 Planer ▾</button>
<div class="plan-picker-panel" id="plan-picker-panel">
<input type="text" id="plan-picker-search" placeholder="Søk navn, dato, person …" autocomplete="off">
<div class="plan-picker-list" id="plan-picker-list"></div>
</div>
</div>
<span class="plan-cal">📅</span>
<input type="date" id="plan-date" title="Velg dag å planlegge">
<input type="text" id="plan-name" class="plan-name-input" maxlength="80" autocomplete="off"
placeholder="Navngi planen (valgfritt), f.eks. «Vedlikeholdsvindu DC-OSL» …">
</div>
<div class="plan-controls">
<div class="plan-owner-row">
<label for="plan-view-user">Plan for</label>
<select id="plan-view-user" title="Se en annens dagsplan (skrivebeskyttet)"></select>
<label class="plan-share" title="Når på: andre kan se denne dagsplanen"><input type="checkbox" id="plan-shared"> Synlig for alle</label>
</div>
<span class="col-meta" id="plan-capacity"></span>
<div class="plan-move" id="plan-move">
<label for="plan-move-date">Flytt til</label>
<input type="date" id="plan-move-date" title="Måldato for hele planen">
<button id="plan-move-btn" title="Flytt hele planen til valgt dato">⇄ Flytt</button>
</div>
</div>
</div>
<div class="plan-readonly" id="plan-readonly"></div>
<div class="plan-col-body" id="plan-today-body"></div>
</div>
</section>
<div class="table-panel" id="table-panel" style="display:none">
<div class="table-head">
<h3>Oppgaveliste <span class="count" id="tbl-count">0</span></h3>
<div class="sub" style="color:var(--text-faint); font-family:var(--mono); font-size:11px">klikk kolonne for å sortere</div>
</div>
<div class="table-scroll">
<table id="tbl">
<thead>
<tr>
<th data-sort="priority" style="width:70px">Prio</th>
<th data-sort="title">Oppgave</th>
<th data-sort="category" style="width:110px">Kategori</th>
<th data-sort="status" style="width:100px">Status</th>
<th data-sort="owner" style="width:130px">Eier</th>
<th data-sort="location" style="width:120px">Lokasjon</th>
<th data-sort="estHours" style="width:70px;text-align:right">Est (t)</th>
<th data-sort="deadline" style="width:110px">Frist</th>
<th style="width:120px;text-align:right">Handling</th>
</tr>
</thead>
<tbody id="tbl-body"></tbody>
</table>
</div>
</div>
<div id="empty" class="empty-state" style="display:none">
Ingen oppgaver matcher filtrene.
<div id="empty-seed" style="display:none;margin-top:14px">
Tom database. <button id="seedBtn" class="primary">Last inn eksempel-data</button>
</div>
</div>
<footer>
Sanntidssynk via WebSocket · snarveier:
<span class="kbd">n</span> ny ·
<span class="kbd">/</span> søk ·
<span class="kbd">v</span> bytt visning ·
<span class="kbd">p</span> dagsplan ·
<span class="kbd">a</span> aktivitet ·
<span class="kbd">esc</span> lukk
</footer>
</div>
<aside class="drawer" id="drawer">
<div class="drawer-head">
<h2>Aktivitet</h2>
<button class="icon-btn" id="drawer-close" style="font-size:18px">×</button>
</div>
<div class="drawer-body" id="drawer-body"></div>
</aside>
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-head">
<h2 id="modal-title">Ny oppgave</h2>
<button class="icon-btn" id="modal-close" style="font-size:18px">×</button>
</div>
<div class="modal-warning" id="modal-warning"></div>
<div class="modal-body">
<div class="form-field full">
<label>Tittel *</label>
<input type="text" id="f-title" placeholder="F.eks. Konfigurer BGP-peer mot AS2119">
</div>
<div class="form-field full">
<label>Beskrivelse / notater</label>
<textarea id="f-desc" placeholder="Kommandoer, konfigurasjon, referanser …"></textarea>
</div>
<div class="form-field">
<label>Kategori *</label>
<select id="f-cat"></select>
</div>
<div class="form-field">
<label>Prioritet</label>
<select id="f-prio">
<option value="P1">P1 Kritisk</option>
<option value="P2" selected>P2 Høy</option>
<option value="P3">P3 Middels</option>
<option value="P4">P4 Lav</option>
</select>
</div>
<div class="form-field">
<label>Status</label>
<select id="f-stat">
<option value="todo" selected>Å gjøre</option>
<option value="progress">Pågår</option>
<option value="blocked">Blokkert</option>
<option value="done">Ferdig</option>
</select>
</div>
<div class="form-field">
<label>Est. tid (timer)</label>
<input type="number" id="f-est" min="0" step="0.5" placeholder="2">
</div>
<div class="form-field full">
<label>Eier / tildelt</label>
<div class="owner-input-row">
<input type="text" id="f-owner-input" placeholder="brukernavn, eller blank for utildelt">
<select id="f-owner-select" title="Velg eksisterende bruker">
<option value="">— velg —</option>
</select>
</div>
</div>
<div class="form-field">
<label>Lokasjon / site</label>
<input type="text" id="f-loc" placeholder="DC-OSL-1, Bergen POP, remote …">
</div>
<div class="form-field">
<label>Frist</label>
<input type="date" id="f-dl">
</div>
<div class="form-field full">
<label>Tags (komma-separert)</label>
<input type="text" id="f-tags" placeholder="vedlikeholdsvindu, kunde-X, audit">
</div>
<div class="form-field full status-note-field" id="f-statusnote-field" style="display:none">
<label id="f-statusnote-label">Avslutningskommentar (valgfritt)</label>
<input type="text" id="f-statusnote" placeholder="Legges som kommentar når du lagrer">
</div>
<div class="comments-section" id="comments-section" style="display:none">
<h4>Kommentarer <span id="comment-count" style="font-family:var(--mono)"></span></h4>
<div id="comments-list"></div>
<div class="comment-input-row">
<input type="text" id="comment-input" placeholder="Skriv en kommentar … (enter for å sende)">
<button id="comment-send">Send</button>
</div>
</div>
</div>
<div class="modal-foot">
<button class="danger" id="modal-delete" style="display:none">Slett</button>
<span id="modal-audit" style="flex:1;font-size:11px;color:var(--text-faint);font-family:var(--mono)"></span>
<button id="modal-cancel">Avbryt</button>
<button class="primary" id="modal-save">Lagre</button>
</div>
</div>
</div>
<div class="modal-overlay" id="status-modal">
<div class="modal" style="max-width:460px">
<div class="modal-head">
<h2 id="status-modal-title">Kommentar</h2>
<button class="icon-btn" id="status-modal-close" style="font-size:18px">×</button>
</div>
<div class="modal-body" style="grid-template-columns:1fr">
<div class="form-field full">
<label id="status-modal-label">Legg til en kommentar (valgfritt)</label>
<textarea id="status-comment" placeholder="F.eks. hva som ble gjort, eller hva som blokkerer …"></textarea>
</div>
</div>
<div class="modal-foot">
<span style="flex:1;font-size:11px;color:var(--text-faint)">⏎ for å lagre · Esc for å avbryte</span>
<button id="status-skip">Hopp over</button>
<button class="primary" id="status-save">Lagre</button>
</div>
</div>
</div>
<div class="modal-overlay" id="account-modal">
<div class="modal" style="max-width:520px">
<div class="modal-head">
<h2>Konto <span id="account-role-badge" class="role-badge" style="display:none"></span></h2>
<button class="icon-btn" id="account-close" style="font-size:18px">×</button>
</div>
<div class="modal-warning" id="account-status"></div>
<div class="modal-body" style="grid-template-columns:1fr">
<div class="form-field full" id="account-current-field" style="display:none">
<label>Nåværende passord</label>
<input type="password" id="account-current" autocomplete="current-password">
</div>
<div class="form-field full">
<label id="account-pw-label">Nytt passord</label>
<input type="password" id="account-newpw" autocomplete="new-password" placeholder="minst 6 tegn">
</div>
<div class="form-field full">
<label>Bekreft passord</label>
<input type="password" id="account-newpw2" autocomplete="new-password">
</div>
<div class="form-field full" style="flex-direction:row;gap:8px;align-items:center;margin-top:2px">
<button class="primary" id="account-save">Lagre passord</button>
<button id="account-logout">Logg ut</button>
<button class="danger" id="account-logout-all" title="Tilbakekall alle aktive økter">Logg ut alle enheter</button>
</div>
<div id="account-admin" style="display:none">
<div class="admin-divider">Administrer kontoer</div>
<div class="admin-create">
<input type="text" id="admin-new-user" placeholder="brukernavn" autocomplete="off">
<input type="password" id="admin-new-pass" placeholder="passord (min 6)" autocomplete="new-password">
<select id="admin-new-role">
<option value="standard">Standard</option>
<option value="admin">Admin</option>
</select>
<button class="primary" id="admin-create-btn">Opprett</button>
</div>
<div id="admin-userlist" class="admin-userlist"></div>
</div>
</div>
<div class="modal-foot">
<div style="flex:1"></div>
<button id="account-cancel">Lukk</button>
</div>
</div>
</div>
<div class="toast-stack" id="toast-stack"></div>
<script src="app.js"></script>
</body>
</html>