Legg til dagsplan + kommentar ved statusbytte

Dagsplan: tredje visning ved siden av tavle/tabell. To-kolonne planlegger
der man plukker oppgaver fra backlog og bygger en ordnet plan for dagen
(drag-sortering, kapasitet i timer, datovelger). Per bruker, per dato,
lagret i plans.json; synkes mellom egne faner via plan-WS-melding.

Kommentar ved statusbytte: når en oppgave markeres ferdig/blokkert (knapp,
kanban-drag eller modal) tilbys en valgfri kommentar som festes til oppgaven
(kind:status) og legges i aktivitetsloggen — atomisk i ett update-kall.

20 integrasjonstester (8 nye), alle grønne. README + CLAUDE.md oppdatert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-15 13:18:33 +02:00
parent 6b9dbf8254
commit be166034de
8 changed files with 686 additions and 32 deletions

View file

@ -93,6 +93,15 @@ function createServer({ dataDir = path.join(__dirname, 'data') } = {}) {
}
}
// Send kun til samme brukers tilkoblinger (dagsplan er privat, men skal synke
// mellom f.eks. desktop og laptop hos samme bruker).
function sendToUser(user, obj) {
const msg = JSON.stringify(obj);
for (const [ws, meta] of clients) {
if (meta.user === user && ws.readyState === ws.OPEN) ws.send(msg);
}
}
function onlineUsers() {
const set = new Set();
for (const meta of clients.values()) set.add(meta.user);
@ -154,10 +163,14 @@ function createServer({ dataDir = path.join(__dirname, 'data') } = {}) {
heartbeat.unref();
// ---------- REST API ----------
const todayStr = () => new Date().toISOString().slice(0, 10);
const validDate = d => typeof d === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(d);
app.get('/healthz', (req, res) => res.json({ ok: true, tasks: store.tasks.length }));
app.get('/api/state', (req, res) => {
store.rememberUser(req.user);
const today = todayStr();
res.json({
user: req.user,
canSetName: COOKIE_AUTH_ENABLED,
@ -165,9 +178,22 @@ function createServer({ dataDir = path.join(__dirname, 'data') } = {}) {
activity: store.activity,
known_users: [...store.knownUsers].sort(),
online: onlineUsers(),
plan: { date: today, items: store.getPlan(req.user, today) },
});
});
app.get('/api/plan', (req, res) => {
const date = validDate(req.query.date) ? req.query.date : todayStr();
res.json({ date, items: store.getPlan(req.user, date) });
});
app.put('/api/plan', (req, res) => {
const date = validDate(req.body && req.body.date) ? req.body.date : todayStr();
const items = store.setPlan(req.user, date, req.body && req.body.items);
sendToUser(req.user, { type: 'plan', date, items });
res.json({ date, items });
});
app.post('/api/login', (req, res) => {
if (!COOKIE_AUTH_ENABLED) return res.status(403).json({ error: 'Navnevalg er deaktivert — auth styres av proxy' });
const raw = req.body && req.body.name;
@ -188,7 +214,10 @@ function createServer({ dataDir = path.join(__dirname, 'data') } = {}) {
});
app.put('/api/tasks/:id', (req, res) => {
const { task, activity } = store.update(req.params.id, req.body, req.user);
// Valgfri `comment` følger med ved statusbytte; normalizeTask ignorerer
// ukjente felter, så den blir ikke en del av selve oppgaven.
const comment = req.body && typeof req.body.comment === 'string' ? req.body.comment : '';
const { task, activity } = store.update(req.params.id, req.body, req.user, { comment });
broadcast({ type: 'task', op: 'update', task, by: req.user, activity });
res.json({ task });
});