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:
38 files changed
+2069
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user