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>
This commit is contained in:
parent
f6268440d9
commit
3f69cd4248
9 changed files with 518 additions and 36 deletions
|
|
@ -48,7 +48,7 @@ function catName(id) { return (catById(id) || {}).name || id; }
|
|||
const state = {
|
||||
user: 'anon',
|
||||
canSetName: false,
|
||||
auth: { mode: 'proxy', via: null, hasPassword: false },
|
||||
auth: { mode: 'proxy', via: null, hasPassword: false, role: 'standard', isAdmin: false },
|
||||
tasks: [],
|
||||
activity: [],
|
||||
knownUsers: [],
|
||||
|
|
@ -845,6 +845,65 @@ function renderPlan() {
|
|||
if (own) bindPlanDnD();
|
||||
}
|
||||
|
||||
// ---------- Søkbar planliste ----------
|
||||
// Avled plansammendrag fra badge-indeksen (allerede filtrert til synlige planer).
|
||||
function derivePlanList() {
|
||||
const map = new Map();
|
||||
for (const entries of Object.values(state.planIndex)) {
|
||||
for (const e of entries) {
|
||||
const key = e.user + '|' + e.date;
|
||||
let g = map.get(key);
|
||||
if (!g) { g = { user: e.user, date: e.date, name: e.name || '', count: 0 }; map.set(key, g); }
|
||||
g.count++;
|
||||
if (!g.name && e.name) g.name = e.name;
|
||||
}
|
||||
}
|
||||
return [...map.values()].sort((a, b) => {
|
||||
const am = a.user === state.user ? 0 : 1, bm = b.user === state.user ? 0 : 1;
|
||||
if (am !== bm) return am - bm; // mine planer først
|
||||
return b.date.localeCompare(a.date); // så nyeste dato først
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlanPicker(filter = '') {
|
||||
const q = filter.trim().toLowerCase();
|
||||
const list = derivePlanList().filter(p => {
|
||||
if (!q) return true;
|
||||
return [p.name, p.date, p.user === state.user ? 'meg' : p.user].join(' ').toLowerCase().includes(q);
|
||||
});
|
||||
const el = $('plan-picker-list');
|
||||
if (!list.length) {
|
||||
el.innerHTML = `<div class="plan-pick-empty">${q ? 'Ingen planer matcher søket.' : 'Ingen planer ennå.'}</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map(p => {
|
||||
const me = p.user === state.user;
|
||||
const d = p.date.slice(8, 10) + '.' + p.date.slice(5, 7) + '.' + p.date.slice(0, 4);
|
||||
return `<div class="plan-pick-row" onclick="pickPlan('${escapeHtml(p.user)}','${p.date}')">
|
||||
<span class="pp-date">${d}</span>
|
||||
<span class="pp-name ${p.name ? '' : 'unnamed'}">${p.name ? escapeHtml(p.name) : '(uten navn)'}</span>
|
||||
<span class="pp-who ${me ? 'me' : ''}">${me ? 'meg' : escapeHtml(p.user)}</span>
|
||||
<span class="pp-count">${p.count}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function togglePlanPicker(open) {
|
||||
const panel = $('plan-picker-panel');
|
||||
const show = open != null ? open : !panel.classList.contains('open');
|
||||
panel.classList.toggle('open', show);
|
||||
if (show) {
|
||||
$('plan-picker-search').value = '';
|
||||
renderPlanPicker('');
|
||||
setTimeout(() => $('plan-picker-search').focus(), 30);
|
||||
}
|
||||
}
|
||||
|
||||
window.pickPlan = function (user, date) {
|
||||
togglePlanPicker(false);
|
||||
refreshPlan(date, user);
|
||||
};
|
||||
|
||||
function populatePlanUserSelect() {
|
||||
const sel = $('plan-view-user');
|
||||
const users = new Set([state.user, ...state.knownUsers, ...state.online]);
|
||||
|
|
@ -1413,11 +1472,78 @@ function openAccount() {
|
|||
$('account-current').value = '';
|
||||
$('account-newpw').value = '';
|
||||
$('account-newpw2').value = '';
|
||||
// Logg ut alle enheter krever en lagret konto (ikke ren proxy-bypass)
|
||||
$('account-logout-all').style.display = has ? '' : 'none';
|
||||
// Rolle-badge
|
||||
const badge = $('account-role-badge');
|
||||
badge.textContent = state.auth.role || 'standard';
|
||||
badge.className = 'role-badge ' + (state.auth.isAdmin ? 'admin' : 'standard');
|
||||
badge.style.display = '';
|
||||
// Admin-seksjon
|
||||
$('account-admin').style.display = state.auth.isAdmin ? '' : 'none';
|
||||
if (state.auth.isAdmin) loadUserList();
|
||||
$('account-modal').classList.add('open');
|
||||
setTimeout(() => $('account-newpw').focus(), 50);
|
||||
}
|
||||
function closeAccount() { $('account-modal').classList.remove('open'); }
|
||||
|
||||
// ---------- Kontoadministrasjon (admin) ----------
|
||||
async function loadUserList() {
|
||||
const box = $('admin-userlist');
|
||||
try {
|
||||
const { users } = await api('GET', '/api/auth/users');
|
||||
box.innerHTML = users.map(u => {
|
||||
const me = u.user === state.user;
|
||||
const toRole = u.role === 'admin' ? 'standard' : 'admin';
|
||||
return `<div class="admin-user">
|
||||
<span class="au-name">${escapeHtml(u.user)}</span>
|
||||
<span class="role-badge ${u.role}">${u.role}</span>
|
||||
${me ? '<span class="au-me">deg</span>' : `<div class="au-actions">
|
||||
<button class="icon-btn" title="Gjør til ${toRole}" onclick="adminSetRole('${escapeHtml(u.user)}','${toRole}')">⇅</button>
|
||||
<button class="icon-btn" title="Tilbakestill passord" onclick="adminResetPassword('${escapeHtml(u.user)}')">🔑</button>
|
||||
<button class="icon-btn del" title="Slett konto" onclick="adminDeleteUser('${escapeHtml(u.user)}')">🗑</button>
|
||||
</div>`}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { box.innerHTML = `<div class="plan-empty">Kunne ikke laste brukere: ${escapeHtml(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function adminCreateUser() {
|
||||
const user = $('admin-new-user').value.trim();
|
||||
const password = $('admin-new-pass').value;
|
||||
const role = $('admin-new-role').value;
|
||||
if (!user) { toast('Skriv et brukernavn', { kind: 'warn' }); return; }
|
||||
if (password.length < 6) { toast('Passord må være minst 6 tegn', { kind: 'warn' }); return; }
|
||||
try {
|
||||
await api('POST', '/api/auth/users', { user, password, role });
|
||||
$('admin-new-user').value = ''; $('admin-new-pass').value = '';
|
||||
loadUserList();
|
||||
toast(`Konto «${user}» (${role}) opprettet`);
|
||||
} catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
window.adminSetRole = async function (user, role) {
|
||||
try { await api('PUT', `/api/auth/users/${encodeURIComponent(user)}/role`, { role }); loadUserList(); toast(`${user} er nå ${role}`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
window.adminResetPassword = async function (user) {
|
||||
const pw = prompt(`Nytt passord for ${user} (min 6 tegn). Brukeren blir logget ut overalt:`);
|
||||
if (pw == null) return;
|
||||
try { await api('POST', `/api/auth/users/${encodeURIComponent(user)}/password`, { password: pw }); toast(`Passord tilbakestilt for ${user}`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
window.adminDeleteUser = async function (user) {
|
||||
if (!confirm(`Slette kontoen «${user}»? Dette kan ikke angres.`)) return;
|
||||
try { await api('DELETE', `/api/auth/users/${encodeURIComponent(user)}`); loadUserList(); toast(`Konto «${user}» slettet`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
|
||||
async function logoutAll() {
|
||||
if (!confirm('Logg ut alle enheter? Du forblir innlogget her, men alle andre økter avsluttes.')) return;
|
||||
try { await api('POST', '/api/auth/logout-all'); toast('Alle andre økter er logget ut'); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
async function saveAccountPassword() {
|
||||
const pw = $('account-newpw').value;
|
||||
const pw2 = $('account-newpw2').value;
|
||||
|
|
@ -1426,9 +1552,10 @@ async function saveAccountPassword() {
|
|||
const body = { password: pw };
|
||||
if ($('account-current-field').style.display !== 'none') body.current = $('account-current').value;
|
||||
try {
|
||||
await api('POST', '/api/auth/password', body);
|
||||
const r = await api('POST', '/api/auth/password', body);
|
||||
state.auth.hasPassword = true;
|
||||
state.auth.via = 'session';
|
||||
if (r.role) { state.auth.role = r.role; state.auth.isAdmin = r.role === 'admin'; }
|
||||
closeAccount();
|
||||
renderHeader();
|
||||
toast('Passord lagret — du er nå innlogget med app-sesjon');
|
||||
|
|
@ -1479,6 +1606,10 @@ function bindUI() {
|
|||
$('plan-name').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } });
|
||||
$('plan-shared').addEventListener('change', togglePlanShared);
|
||||
$('plan-move-btn').addEventListener('click', movePlanToDate);
|
||||
$('plan-picker-btn').addEventListener('click', e => { e.stopPropagation(); togglePlanPicker(); });
|
||||
$('plan-picker-search').addEventListener('input', e => renderPlanPicker(e.target.value));
|
||||
$('plan-picker-panel').addEventListener('click', e => e.stopPropagation());
|
||||
document.addEventListener('click', () => togglePlanPicker(false)); // klikk utenfor lukker
|
||||
$('fieldBtn').addEventListener('click', toggleFieldMode);
|
||||
bindPlanContainer();
|
||||
|
||||
|
|
@ -1550,6 +1681,9 @@ function bindUI() {
|
|||
$('account-cancel').addEventListener('click', closeAccount);
|
||||
$('account-save').addEventListener('click', saveAccountPassword);
|
||||
$('account-logout').addEventListener('click', logout);
|
||||
$('account-logout-all').addEventListener('click', logoutAll);
|
||||
$('admin-create-btn').addEventListener('click', adminCreateUser);
|
||||
$('admin-new-pass').addEventListener('keydown', e => { if (e.key === 'Enter') adminCreateUser(); });
|
||||
$('account-modal').addEventListener('click', e => { if (e.target.id === 'account-modal') closeAccount(); });
|
||||
$('account-newpw2').addEventListener('keydown', e => { if (e.key === 'Enter') saveAccountPassword(); });
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@
|
|||
<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"
|
||||
|
|
@ -327,9 +334,9 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-overlay" id="account-modal">
|
||||
<div class="modal" style="max-width:440px">
|
||||
<div class="modal" style="max-width:520px">
|
||||
<div class="modal-head">
|
||||
<h2>Konto</h2>
|
||||
<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>
|
||||
|
|
@ -346,12 +353,29 @@
|
|||
<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">
|
||||
<button class="danger" id="account-logout">Logg ut</button>
|
||||
<div style="flex:1"></div>
|
||||
<button id="account-cancel">Avbryt</button>
|
||||
<button class="primary" id="account-save">Lagre passord</button>
|
||||
<button id="account-cancel">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,21 +47,54 @@
|
|||
.card {
|
||||
position: relative; z-index: 2; width: 100%; max-width: 420px;
|
||||
background: rgba(10,14,22,0.72); backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(79,230,255,0.25); border-radius: 14px; padding: 34px 30px 28px;
|
||||
box-shadow: 0 0 0 1px rgba(255,92,244,0.08), 0 24px 70px rgba(0,0,0,0.6), 0 0 60px rgba(79,230,255,0.12);
|
||||
border: 1px solid rgba(79,230,255,0.35); border-radius: 14px; padding: 34px 30px 28px;
|
||||
box-shadow: 0 0 0 1px rgba(255,92,244,0.1), 0 24px 70px rgba(0,0,0,0.6), 0 0 60px rgba(79,230,255,0.12);
|
||||
animation: borderpulse 3.6s ease-in-out infinite;
|
||||
}
|
||||
.prompt { font-family: var(--mono); font-size: 12px; color: var(--cyan); letter-spacing: 1px; margin-bottom: 8px; opacity: 0.85; }
|
||||
@keyframes borderpulse {
|
||||
0%,100% { border-color: rgba(79,230,255,0.35); box-shadow: 0 0 0 1px rgba(255,92,244,0.1), 0 24px 70px rgba(0,0,0,0.6), 0 0 50px rgba(79,230,255,0.12); }
|
||||
50% { border-color: rgba(79,230,255,0.6); box-shadow: 0 0 0 1px rgba(255,92,244,0.18), 0 24px 70px rgba(0,0,0,0.6), 0 0 80px rgba(79,230,255,0.28); }
|
||||
}
|
||||
/* Tynn scanline over kortet + svak CRT-støy */
|
||||
.card::before {
|
||||
content: ""; position: absolute; inset: 0; border-radius: 14px; pointer-events: none;
|
||||
background: repeating-linear-gradient(0deg, transparent 0, transparent 2px, rgba(79,230,255,0.025) 3px, transparent 4px);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
/* HUD-hjørner */
|
||||
.corner { position: absolute; width: 18px; height: 18px; border: 2px solid var(--cyan);
|
||||
filter: drop-shadow(0 0 4px var(--cyan)); pointer-events: none; }
|
||||
.corner.tl { top: -1px; left: -1px; border-right: 0; border-bottom: 0; border-top-left-radius: 14px; }
|
||||
.corner.tr { top: -1px; right: -1px; border-left: 0; border-bottom: 0; border-top-right-radius: 14px; border-color: var(--magenta); filter: drop-shadow(0 0 4px var(--magenta)); }
|
||||
.corner.bl { bottom: -1px; left: -1px; border-right: 0; border-top: 0; border-bottom-left-radius: 14px; border-color: var(--magenta); filter: drop-shadow(0 0 4px var(--magenta)); }
|
||||
.corner.br { bottom: -1px; right: -1px; border-left: 0; border-top: 0; border-bottom-right-radius: 14px; }
|
||||
|
||||
.hud { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.prompt { font-family: var(--mono); font-size: 12px; color: var(--cyan); letter-spacing: 1px; opacity: 0.85; }
|
||||
.prompt::before { content: "› "; }
|
||||
.lock { font-family: var(--mono); font-size: 10px; letter-spacing: 1.5px; color: var(--magenta);
|
||||
border: 1px solid rgba(255,92,244,0.4); border-radius: 4px; padding: 3px 7px;
|
||||
text-shadow: 0 0 8px rgba(255,92,244,0.7); animation: lockblink 2.4s steps(1) infinite; }
|
||||
@keyframes lockblink { 0%,70%,100% { opacity: 1; } 78%,88% { opacity: 0.3; } }
|
||||
h1 {
|
||||
font-size: 40px; font-weight: 800; letter-spacing: 1px; line-height: 1;
|
||||
color: #eafcff; text-shadow: 0 0 8px var(--cyan), 0 0 22px rgba(79,230,255,0.7), 0 0 40px rgba(79,230,255,0.4);
|
||||
animation: flicker 4.5s infinite;
|
||||
font-size: 42px; font-weight: 800; letter-spacing: 1px; line-height: 1; position: relative;
|
||||
color: #eafcff;
|
||||
text-shadow: -1.5px 0 rgba(255,92,244,0.55), 1.5px 0 rgba(79,230,255,0.55),
|
||||
0 0 8px var(--cyan), 0 0 22px rgba(79,230,255,0.75), 0 0 44px rgba(79,230,255,0.45);
|
||||
animation: flicker 4.5s infinite, glitch 7s steps(1) infinite;
|
||||
}
|
||||
h1 .x { color: var(--magenta); text-shadow: 0 0 8px var(--magenta), 0 0 22px rgba(255,92,244,0.7), 0 0 40px rgba(255,92,244,0.4); }
|
||||
h1 .x { color: var(--magenta); text-shadow: 0 0 8px var(--magenta), 0 0 22px rgba(255,92,244,0.75), 0 0 44px rgba(255,92,244,0.45); }
|
||||
@keyframes flicker {
|
||||
0%,18%,22%,25%,53%,57%,100% { opacity: 1; }
|
||||
20%,24%,55% { opacity: 0.72; }
|
||||
}
|
||||
/* Sporadisk kromatisk «glitch»-forskyvning */
|
||||
@keyframes glitch {
|
||||
0%,90%,100% { transform: translate(0,0); text-shadow: -1.5px 0 rgba(255,92,244,0.55), 1.5px 0 rgba(79,230,255,0.55), 0 0 8px var(--cyan), 0 0 22px rgba(79,230,255,0.75); }
|
||||
91% { transform: translate(2px,-1px); text-shadow: -3px 0 var(--magenta), 3px 0 var(--cyan), 0 0 26px rgba(79,230,255,0.9); }
|
||||
93% { transform: translate(-2px,1px); text-shadow: 3px 0 var(--magenta), -3px 0 var(--cyan), 0 0 26px rgba(79,230,255,0.9); }
|
||||
95% { transform: translate(1px,0); }
|
||||
}
|
||||
.teaser { color: var(--dim); font-size: 13.5px; line-height: 1.6; margin: 14px 0 22px; }
|
||||
.teaser b { color: var(--text); font-weight: 600; }
|
||||
.ticker { font-family: var(--mono); font-size: 11px; color: var(--green);
|
||||
|
|
@ -87,7 +120,8 @@
|
|||
background: linear-gradient(90deg, var(--cyan), #7fe9ff);
|
||||
box-shadow: 0 0 22px rgba(79,230,255,0.5); transition: all 0.18s;
|
||||
}
|
||||
button:hover { box-shadow: 0 0 34px rgba(79,230,255,0.85); transform: translateY(-1px); }
|
||||
button:hover { box-shadow: 0 0 40px rgba(79,230,255,1); transform: translateY(-1px); letter-spacing: 1.5px; }
|
||||
button::after { content: ""; }
|
||||
button:disabled { opacity: 0.6; cursor: wait; }
|
||||
.msg { min-height: 1.2em; font-size: 13px; color: var(--magenta); font-family: var(--mono);
|
||||
text-shadow: 0 0 10px rgba(255,92,244,0.5); text-align: center; }
|
||||
|
|
@ -105,7 +139,12 @@
|
|||
<div class="scan"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="prompt">network-engineer@nms:~$ ssh netops</div>
|
||||
<span class="corner tl"></span><span class="corner tr"></span>
|
||||
<span class="corner bl"></span><span class="corner br"></span>
|
||||
<div class="hud">
|
||||
<div class="prompt">network-engineer@nms:~$ ssh netops</div>
|
||||
<span class="lock">● LOCKED</span>
|
||||
</div>
|
||||
<h1>NetOps<span class="x">//</span></h1>
|
||||
<p class="teaser">
|
||||
Bak denne porten <b>puster nettet</b>. Pakker flyr, rutene konvergerer,
|
||||
|
|
|
|||
|
|
@ -387,6 +387,26 @@ tbody tr.status-done .task-title { text-decoration: line-through; }
|
|||
.plan-move label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); font-weight: 600; }
|
||||
.plan-move input[type="date"] { padding: 5px 7px; font-size: 12px; }
|
||||
.plan-move button { padding: 6px 10px; font-size: 12px; }
|
||||
/* Søkbar planvelger */
|
||||
.plan-picker { position: relative; }
|
||||
.plan-picker > button { padding: 6px 10px; font-size: 12px; }
|
||||
.plan-picker-panel { display: none; position: absolute; z-index: 30; top: calc(100% + 6px); left: 0;
|
||||
width: 320px; max-width: 80vw; background: var(--card); border: 1px solid var(--border-strong);
|
||||
border-radius: 8px; box-shadow: 0 16px 40px rgba(0,0,0,0.5); padding: 10px; }
|
||||
.plan-picker-panel.open { display: block; }
|
||||
.plan-picker-panel input { width: 100%; margin-bottom: 8px; }
|
||||
.plan-picker-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
|
||||
.plan-pick-row { display: flex; align-items: center; gap: 8px; padding: 7px 9px; border-radius: 6px;
|
||||
cursor: pointer; background: var(--bg-soft); border: 1px solid var(--border); font-size: 13px; }
|
||||
.plan-pick-row:hover { border-color: var(--accent); background: var(--card-hover); }
|
||||
.plan-pick-row .pp-date { font-family: var(--mono); font-size: 12px; color: var(--accent); white-space: nowrap; }
|
||||
.plan-pick-row .pp-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.plan-pick-row .pp-name.unnamed { color: var(--text-faint); font-style: italic; }
|
||||
.plan-pick-row .pp-who { font-family: var(--mono); font-size: 11px; color: var(--purple); }
|
||||
.plan-pick-row .pp-who.me { color: var(--ok); }
|
||||
.plan-pick-row .pp-count { font-family: var(--mono); font-size: 11px; color: var(--text-faint); }
|
||||
.plan-pick-empty { color: var(--text-faint); font-size: 12px; font-style: italic; padding: 12px 8px; text-align: center; }
|
||||
|
||||
.plan-owner-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.plan-owner-row > label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); font-weight: 600; }
|
||||
.plan-owner-row select { padding: 5px 8px; font-size: 12px; }
|
||||
|
|
@ -404,6 +424,22 @@ tbody tr.status-done .task-title { text-decoration: line-through; }
|
|||
.plan-badge.me { background: rgba(74,222,128,0.1); color: var(--ok); border-color: rgba(74,222,128,0.3); }
|
||||
.plan-badge.more { opacity: 0.8; }
|
||||
|
||||
/* ---------- Konto / admin ---------- */
|
||||
.role-badge { font-family: var(--mono); font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 4px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px; vertical-align: middle; }
|
||||
.role-badge.admin { background: rgba(255,92,244,0.14); color: #ff8af6; border: 1px solid rgba(255,92,244,0.35); }
|
||||
.role-badge.standard { background: rgba(139,152,169,0.12); color: var(--text-dim); border: 1px solid var(--border); }
|
||||
.admin-divider { grid-column: 1 / -1; margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--border);
|
||||
font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-dim); font-weight: 600; }
|
||||
.admin-create { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
|
||||
.admin-create input { flex: 1; min-width: 110px; }
|
||||
.admin-userlist { display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow-y: auto; }
|
||||
.admin-user { display: flex; align-items: center; gap: 8px; padding: 7px 10px; background: var(--bg-soft);
|
||||
border: 1px solid var(--border); border-radius: 6px; font-size: 13px; }
|
||||
.admin-user .au-name { font-family: var(--mono); flex: 1; }
|
||||
.admin-user .au-actions { display: flex; gap: 4px; }
|
||||
.admin-user .au-me { color: var(--text-faint); font-size: 11px; font-family: var(--mono); }
|
||||
|
||||
/* ---------- Toast ---------- */
|
||||
.toast-stack { position: fixed; bottom: 20px; right: 20px; z-index: 200;
|
||||
display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
|
||||
|
|
@ -466,6 +502,19 @@ body.field-mode .row-actions { gap: 8px; }
|
|||
/* Modaler fyller skjermen bedre på mobil */
|
||||
body.field-mode .modal { max-width: 100%; }
|
||||
|
||||
/* ---------- Neon-aksenter (samme tema som login) ---------- */
|
||||
header h1 { text-shadow: 0 0 10px rgba(79,195,247,0.4), 0 0 26px rgba(79,195,247,0.18); }
|
||||
#conn-dot.live { box-shadow: 0 0 12px var(--ok), 0 0 20px rgba(74,222,128,0.5); }
|
||||
button.primary { box-shadow: 0 0 16px rgba(79,195,247,0.35); }
|
||||
button.primary:hover { box-shadow: 0 0 28px rgba(79,195,247,0.6); }
|
||||
button.active-view { box-shadow: inset 0 0 0 1px rgba(79,195,247,0.4), 0 0 14px rgba(79,195,247,0.22); }
|
||||
.user-chip { box-shadow: 0 0 14px rgba(79,195,247,0.18); }
|
||||
.kpi::before { box-shadow: 0 0 10px rgba(79,195,247,0.45); }
|
||||
.kpi.kpi-p1::before { box-shadow: 0 0 12px var(--p1); }
|
||||
.kpi.kpi-ok::before { box-shadow: 0 0 12px var(--ok); }
|
||||
.kpi.kpi-me::before { box-shadow: 0 0 12px var(--purple); }
|
||||
.badge.prio-P1 { box-shadow: 0 0 10px rgba(255,83,112,0.3); }
|
||||
|
||||
@media (max-width: 1250px) { .board { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 1100px) { .chart-row { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 820px) { .planner { grid-template-columns: 1fr; } }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue