import { activity, loadLook, motionAllowed } from './look.js';
import { watchCommands } from './changes.js';
function stored(key, fallback) {
try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; }
}
function remember(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); } catch { }
}
const visitKey = "thirdshift:console:last-look";
const visit = stored(visitKey, Date.now());
const lastLook = typeof visit === "number" && Number.isFinite(visit) && visit >= 0 && visit <= Date.now() ? visit : Date.now();
function markNew() {
for (const row of document.querySelectorAll("#inbox .command")) {
row.querySelector(":scope > .row-head > .new").hidden = Number(row.dataset.startedMs) <= lastLook;
}
}
markNew();
remember(visitKey, Date.now());
window.addEventListener("pagehide", () => remember(visitKey, Date.now()));
const foldKey = "thirdshift:console:folded";
let folded = stored(foldKey, {});
if (typeof folded !== "object" || Array.isArray(folded)) folded = {};
for (const section of document.querySelectorAll("details[data-group]")) {
if (typeof folded[section.dataset.group] === "boolean") section.open = folded[section.dataset.group];
}
document.addEventListener("toggle", event => {
const section = event.target;
if (!section.matches("details[data-group]")) return;
folded[section.dataset.group] = section.open;
remember(foldKey, folded);
if (section.open && selected && section.contains(selected)) selected.focus({ preventScroll: true });
}, true);
document.addEventListener("focusin", event => {
const row = event.target.closest(".command");
if (row) select(row, { scroll: false });
});
document.addEventListener("click", event => {
if (!event.target.closest('a, button, input, select, textarea')) {
event.target.closest(".command")?.focus({ preventScroll: true });
}
});
function logLengths(row) {
return row.dataset.logGrowth.split(":").map(Number);
}
function growth() {
return new Map([...document.querySelectorAll(".command")].map(row => [row.id, logLengths(row)]));
}
function morph(html) {
const next = new DOMParser().parseFromString(html, "text/html");
const current = document.getElementById("inbox");
const replacement = next.getElementById("inbox");
if (!replacement) throw new Error("Missing Inbox");
const active = document.activeElement;
const focused = active?.id || active?.closest("details")?.id;
const wasSummary = active?.matches("summary");
const scroll = [...current.querySelectorAll("details")].map(section => [section.id, section.querySelector(".commands").scrollTop]);
const position = [scrollX, scrollY];
const selection = selected?.id || selected?.closest('details')?.id;
const selectedSummary = selected?.matches('summary');
const previousGrowth = growth();
Idiomorph.morph(current, replacement, {
callbacks: {
beforeAttributeUpdated(name, node) {
if (name === "open" && node.matches("details[data-group]")) return false;
}
}
});
const focusedNode = document.getElementById(focused);
(wasSummary ? focusedNode?.querySelector("summary") : focusedNode)?.focus({ preventScroll: true });
const selectedNode = document.getElementById(selection);
select(selectedSummary ? selectedNode?.querySelector('summary') : selectedNode, { scroll: false });
for (const [id, top] of scroll) document.getElementById(id).querySelector(".commands").scrollTop = top;
window.scrollTo(...position);
document.title = next.title;
document.querySelector('link[rel="icon"]').href = next.querySelector('link[rel="icon"]').getAttribute("href");
markNew();
activity();
loadLook();
for (const row of document.querySelectorAll('.command[data-logging="true"]')) {
const previous = previousGrowth.get(row.id) ?? [0, 0];
const increased = logLengths(row).some((size, index) => size > previous[index]);
if (increased && row.dataset.live === 'true' && motionAllowed()) {
row.querySelector(":scope > .row-head > .heartbeat")?.animate([
{ transform: "scale(1)", opacity: 0.4 },
{ transform: "scale(1.8)", opacity: 1, offset: 0.3 },
{ transform: "scale(1)", opacity: 0.4 }
], { duration: 900 });
row.querySelector(':scope > .now')?.animate([
{ outline: '2px solid var(--working)' }, { outline: '2px solid transparent' }
], { duration: 900 });
}
}
}
const timePage = document.querySelector('main[data-time-page]');
function morphTimePage(html) {
const next = new DOMParser().parseFromString(html, 'text/html');
const replacement = next.querySelector('main[data-time-page]');
if (!replacement) throw new Error('Missing time page');
const selection = selected?.dataset.command;
const focused = timePage.contains(document.activeElement);
const position = [scrollX, scrollY];
Idiomorph.morph(timePage, replacement);
if (selection) {
const row = [...timePage.querySelectorAll('.command')].find(row => row.dataset.command === selection);
select(row, { scroll: false });
if (focused) row?.focus({ preventScroll: true });
}
window.scrollTo(...position);
activity();
loadLook();
}
async function refresh() {
if (observesCommands) {
const response = await fetch(timePage ? location.href : '/', { cache: "no-store" });
if (!response.ok) throw new Error("Inbox unavailable");
const html = await response.text();
if (timePage) morphTimePage(html);
else morph(html);
}
await refreshPeek();
}
let stopWatching;
function startUpdates() {
stopWatching ??= watchCommands(refresh);
}
const observesCommands = document.getElementById('inbox') || timePage;
if (observesCommands) startUpdates();
function stopPeekUpdates() {
if (observesCommands) return;
stopWatching?.();
stopWatching = undefined;
}
const peek = document.querySelector('#peek');
const content = document.querySelector('#peek-content');
const help = document.querySelector('#keys');
const jumps = document.querySelector('#jumps');
let selected = null;
let peekCommand = null;
let pendingPeek = null;
function select(element, { scroll = true } = {}) {
selected?.classList.remove('is-selected');
selected?.removeAttribute('aria-current');
selected = element;
selected?.classList.add('is-selected');
if (selected?.matches('.command')) selected.setAttribute('aria-current', 'true');
if (scroll) selected?.scrollIntoView({ block: 'nearest' });
}
function closePeek() {
stopPeekUpdates();
pendingPeek?.abort();
pendingPeek = null;
peekCommand = null;
peek.hidden = true;
document.body.classList.remove('peek-open');
}
async function togglePeek(row) {
if (!row.dataset.command) return;
if (peekCommand === row.dataset.command) { closePeek(); return; }
peekCommand = row.dataset.command;
content.textContent = 'Loading details…';
peek.hidden = false;
document.body.classList.add('peek-open');
await loadPeek(row.dataset.peek);
}
async function refreshPeek() {
if (peekCommand && !pendingPeek) {
await loadPeek(`/peek?${new URLSearchParams({ command: peekCommand })}`);
}
}
async function loadPeek(url) {
pendingPeek?.abort();
const request = new AbortController();
pendingPeek = request;
try {
const response = await fetch(url, { signal: request.signal, cache: 'no-store' });
if (!response.ok) throw new Error('Details unavailable');
const html = await response.text();
if (request.signal.aborted) return;
content.replaceChildren(document.createRange().createContextualFragment(html));
if (content.querySelector('[data-live="true"]')) startUpdates();
else stopPeekUpdates();
} catch (error) {
if (!request.signal.aborted && error.name !== 'AbortError') content.textContent = 'Details unavailable. Reload to try again.';
} finally {
if (pendingPeek === request) pendingPeek = null;
}
}
function items() {
return [...document.querySelectorAll('main .group > summary, main .command, main .log-row')]
.filter(element => {
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
if (parent.hidden || (parent.tagName === 'DETAILS' && !parent.open && element !== parent.firstElementChild)) return false;
}
return true;
});
}
const sectionLetters = { 'needs you': 'n', 'ready for review': 'r', landing: 'l', working: 'w', done: 'd', resolved: 'v' };
const targets = new Map();
for (const link of document.querySelectorAll('nav[aria-label="Pages"] a')) {
targets.set(link.dataset.jump, { label: link.textContent.replace(/^\d+ /, ''), go: () => link.click() });
}
for (const section of document.querySelectorAll('.group')) {
const letter = sectionLetters[section.dataset.group];
if (letter) targets.set(letter, {
label: section.querySelector('summary span').textContent,
go: () => {
const current = section.id ? document.getElementById(section.id) : section;
current.open = true;
select(current.querySelector('summary'));
}
});
}
for (const [letter, target] of targets) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = `${letter} · ${target.label}`;
button.addEventListener('click', () => { jumps.hidden = true; target.go(); });
jumps.append(button);
}
document.querySelector('[data-close-peek]').addEventListener('click', closePeek);
document.querySelector('[data-close-keys]').addEventListener('click', () => help.close());
document.querySelector('[data-open-keys]').addEventListener('click', () => help.showModal());
document.addEventListener('click', event => {
const link = event.target.closest('.peek-link');
const row = event.target.closest('main .command, main .log-row');
if (link) { event.preventDefault(); select(row); togglePeek(row); }
else if (row && !event.target.closest('a, button, input, select, textarea')) select(row);
else if (event.target.closest('main .group > summary')) {
const summary = event.target.closest('summary');
if (summary.parentElement.open || !selected?.matches('.command') || !summary.parentElement.contains(selected)) select(summary);
}
});
document.addEventListener('keydown', event => {
if (event.ctrlKey || event.metaKey || event.altKey || event.target.closest('input, select, textarea, [contenteditable="true"]')) return;
if (event.key === 'Enter' && event.target.closest('a, button')) return;
if (event.key === ' ' && event.target.closest('button')) return;
if (document.querySelector('#themes[open], #sections-chooser[open]')) return;
if (event.key === 'T' && event.shiftKey) { event.preventDefault(); document.querySelector('[data-open-themes]').click(); return; }
if (event.key === 'Escape') {
event.preventDefault();
if (help.open) help.close();
else if (!jumps.hidden) jumps.hidden = true;
else if (!peek.hidden) closePeek();
else select(null);
return;
}
if (help.open) return;
if (!jumps.hidden) {
event.preventDefault();
jumps.hidden = true;
targets.get(event.key.toLowerCase())?.go();
return;
}
const page = document.querySelector(`nav[aria-label="Pages"] a[data-key="${CSS.escape(event.key)}"]`);
if (page) { event.preventDefault(); page.click(); return; }
if (event.key === '?') { event.preventDefault(); help.showModal(); return; }
if (event.key === 'g') { event.preventDefault(); jumps.hidden = false; return; }
if (event.key === 'j' || event.key === 'k') {
event.preventDefault();
const visible = items();
const index = visible.indexOf(selected);
const next = index < 0 ? (event.key === 'j' ? 0 : visible.length - 1)
: (index + (event.key === 'j' ? 1 : -1) + visible.length) % visible.length;
select(visible[next] ?? null);
} else if (event.key === ' ' && selected) {
event.preventDefault();
if (selected.tagName === 'SUMMARY') selected.parentElement.open = !selected.parentElement.open;
else togglePeek(selected);
} else if (event.key === 'z' && selected) {
event.preventDefault();
const section = selected.closest('.group');
if (!section) return;
section.open = false;
select(section.querySelector('summary'));
} else if (event.key === 'Enter' && selected) {
event.preventDefault();
const row = selected.matches('.command, .log-row') ? selected : selected.parentElement.querySelector('.command');
if (row?.dataset.log) location.assign(row.dataset.log);
}
});