feat(webui): unread-cursor helpers for the error-log bell
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
LAST_SEEN_STORAGE_KEY,
|
||||
countUnread,
|
||||
formatBadgeCount,
|
||||
latestLogId,
|
||||
readLastSeenId,
|
||||
writeLastSeenId,
|
||||
} from "./errorLogUnread";
|
||||
|
||||
function fakeStorage(initial: Record<string, string> = {}) {
|
||||
const map = new Map(Object.entries(initial));
|
||||
return {
|
||||
getItem: (key: string) => map.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
map.set(key, value);
|
||||
},
|
||||
map,
|
||||
};
|
||||
}
|
||||
|
||||
describe("errorLogUnread", () => {
|
||||
it("latestLogId returns the max id, 0 for empty", () => {
|
||||
expect(latestLogId([])).toBe(0);
|
||||
expect(latestLogId([{ id: 3 }, { id: 9 }, { id: 5 }])).toBe(9);
|
||||
});
|
||||
|
||||
it("countUnread counts entries newer than the cursor", () => {
|
||||
const logs = [{ id: 10 }, { id: 8 }, { id: 4 }];
|
||||
expect(countUnread(logs, 0)).toBe(3);
|
||||
expect(countUnread(logs, 8)).toBe(1);
|
||||
expect(countUnread(logs, 10)).toBe(0);
|
||||
});
|
||||
|
||||
it("formatBadgeCount caps at 99+ and hides zero", () => {
|
||||
expect(formatBadgeCount(0)).toBe("");
|
||||
expect(formatBadgeCount(7)).toBe("7");
|
||||
expect(formatBadgeCount(99)).toBe("99");
|
||||
expect(formatBadgeCount(150)).toBe("99+");
|
||||
});
|
||||
|
||||
it("readLastSeenId returns 0 for null storage, missing, or corrupt values", () => {
|
||||
expect(readLastSeenId(null)).toBe(0);
|
||||
expect(readLastSeenId(fakeStorage())).toBe(0);
|
||||
expect(
|
||||
readLastSeenId(fakeStorage({ [LAST_SEEN_STORAGE_KEY]: "abc" }))
|
||||
).toBe(0);
|
||||
expect(
|
||||
readLastSeenId(fakeStorage({ [LAST_SEEN_STORAGE_KEY]: "-5" }))
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("writeLastSeenId round-trips through storage", () => {
|
||||
const storage = fakeStorage();
|
||||
writeLastSeenId(storage, 42);
|
||||
expect(readLastSeenId(storage)).toBe(42);
|
||||
});
|
||||
|
||||
it("writeLastSeenId swallows storage failures", () => {
|
||||
const throwing = {
|
||||
setItem: () => {
|
||||
throw new Error("quota");
|
||||
},
|
||||
};
|
||||
expect(() => writeLastSeenId(throwing, 1)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/** Unread-badge state for the error-log bell. The server assigns each entry a
|
||||
* monotonically increasing id, so a single "last seen id" cursor is enough;
|
||||
* the cursor lives in localStorage (per browser, per user is unnecessary
|
||||
* because ids are global and monotonic). */
|
||||
|
||||
export const LAST_SEEN_STORAGE_KEY = "evoscientist.errorLogs.lastSeenId";
|
||||
|
||||
const BADGE_CAP = 99;
|
||||
|
||||
export function latestLogId(logs: ReadonlyArray<{ id: number }>): number {
|
||||
let max = 0;
|
||||
for (const log of logs) {
|
||||
if (log.id > max) max = log.id;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
export function countUnread(
|
||||
logs: ReadonlyArray<{ id: number }>,
|
||||
lastSeenId: number
|
||||
): number {
|
||||
let count = 0;
|
||||
for (const log of logs) {
|
||||
if (log.id > lastSeenId) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function formatBadgeCount(count: number): string {
|
||||
if (count <= 0) return "";
|
||||
return count > BADGE_CAP ? `${BADGE_CAP}+` : String(count);
|
||||
}
|
||||
|
||||
export function readLastSeenId(
|
||||
storage: Pick<Storage, "getItem"> | null
|
||||
): number {
|
||||
if (!storage) return 0;
|
||||
const raw = storage.getItem(LAST_SEEN_STORAGE_KEY);
|
||||
if (raw === null) return 0;
|
||||
const value = Number(raw);
|
||||
if (!Number.isInteger(value) || value < 0) return 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function writeLastSeenId(
|
||||
storage: Pick<Storage, "setItem"> | null,
|
||||
id: number
|
||||
): void {
|
||||
if (!storage) return;
|
||||
try {
|
||||
storage.setItem(LAST_SEEN_STORAGE_KEY, String(id));
|
||||
} catch {
|
||||
// Private-mode quota errors must not break the bell.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user