Add Timelog: show the timer in the tab title and simplify the code

This is the first commit of the project, at the state that runs on
timelog.minhtrannhat.com.

Changes in this state:
- Remove redundant UI text from the sidebar, timer and insights pages.
- Show the remaining time and tag in the browser tab title while a
  session runs.
- Put the shared record type and the due and end helpers in
  src/shared/record.ts.
- Remove the startup tag migration and the one-time Forest import
  script. The live data does not need them.
- Remove unused CSS, Playwright ignore rules and duplicate helpers.
- Do not track Forest CSV exports, because they contain private data.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
minhtrannhatandClaude Opus 5.5 committed 2026-11-05 00:42:58 -05:00
commit a6ca255835
38 files changed
+2069

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
import type { TimeRecord } from "../shared/record.ts";
import { allRecords, applyRemote, cursor, pendingOperations, removeOperations } from "./store.ts";
export type SyncStatus = "offline" | "syncing" | "synced" | "pending" | "signed-out";
let current: Promise<void> | null = null;
let rerunRequested = false;
class SignedOut extends Error {}
async function api<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
if (response.status === 401) throw new SignedOut();
if (!response.ok) throw new Error(`Sync failed (${response.status})`);
return response.json() as Promise<T>;
}
export function syncNow(onChange: (status: SyncStatus, records?: TimeRecord[]) => void): Promise<void> {
if (current) {
rerunRequested = true;
return current;
}
current = (async () => {
if (!navigator.onLine) { onChange("offline"); return; }
onChange("syncing");
try {
let pending = await pendingOperations();
while (pending.length) {
const batch = pending.slice(0, 200);
const body = await api<{ accepted: string[] }>("/api/operations", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ operations: batch.map(({ id, record }) => ({ id, record })) }),
});
await removeOperations(body.accepted);
pending = await pendingOperations();
}
let since = await cursor();
for (;;) {
const body = await api<{ records: TimeRecord[]; cursor: number; hasMore: boolean }>(
`/api/state?since=${since}`, { cache: "no-store" });
await applyRemote(body.records, body.cursor);
since = body.cursor;
if (!body.hasMore) break;
}
onChange("synced", await allRecords());
} catch (error) {
if (error instanceof SignedOut) { onChange("signed-out"); return; }
console.error(error);
onChange("pending");
}
})().finally(() => {
current = null;
if (rerunRequested) {
rerunRequested = false;
void syncNow(onChange);
}
});
return current;
}