rework pwa cc
Build and Publish Docker Image / build-and-push-image (push) Successful in 1m48s

This commit is contained in:
Zed
2026-07-11 02:01:08 +02:00
parent 9ae37f98ed
commit ce1db937f2
52 changed files with 5210 additions and 48 deletions
+154 -2
View File
@@ -1,10 +1,30 @@
import Dexie, { Table } from "dexie";
import { GameSession, AppSettings, SavedPlayer } from "../types";
import {
GameSession,
AppSettings,
SavedPlayer,
Location,
Profile,
SyncState,
} from "../types";
import { generateId } from "../utils/id";
// When the sync engine applies records pulled/acked from the server, it flips
// this flag so the auto-dirty hooks don't re-mark those writes as dirty.
export const remoteApply = { active: false };
// The sync engine registers a (debounced) callback here so any local change to
// a synced table promptly schedules a push. Kept as a hook to avoid a circular
// import between db.ts and syncEngine.ts.
export const localChange = { notify: null as null | (() => void) };
export class BoardScoreDatabase extends Dexie {
sessions!: Table<GameSession, string>;
settings!: Table<AppSettings, number>;
players!: Table<SavedPlayer, string>;
locations!: Table<Location, string>;
profiles!: Table<Profile, string>;
syncState!: Table<SyncState, string>;
constructor() {
super("BoardScoreDatabase");
@@ -20,16 +40,148 @@ export class BoardScoreDatabase extends Dexie {
settings: "id",
players: "id, name, createdAt",
});
// Version 3: Added locations table, updatedAt tracking for future sync
this.version(3)
.stores({
sessions: "id, gameId, dateStart, status, locationId, updatedAt",
settings: "id",
players: "id, name, createdAt, updatedAt",
locations: "id, name, createdAt, updatedAt",
})
.upgrade(async (tx) => {
await tx
.table("sessions")
.toCollection()
.modify((session) => {
session.updatedAt = session.dateEnd ?? session.dateStart;
});
await tx
.table("players")
.toCollection()
.modify((player) => {
player.updatedAt = player.createdAt;
});
});
// Version 4: Added local profiles; scope existing data to a default profile
this.version(4)
.stores({
sessions:
"id, gameId, dateStart, status, locationId, updatedAt, profileId",
settings: "id",
players: "id, name, createdAt, updatedAt, profileId",
locations: "id, name, createdAt, updatedAt, profileId",
profiles: "id, name, createdAt, updatedAt",
})
.upgrade(async (tx) => {
const now = Date.now();
const defaultProfileId = generateId();
await tx.table("profiles").add({
id: defaultProfileId,
name: "Moi",
createdAt: now,
updatedAt: now,
});
await tx
.table("sessions")
.toCollection()
.modify((session) => {
session.profileId = defaultProfileId;
});
await tx
.table("players")
.toCollection()
.modify((player) => {
player.profileId = defaultProfileId;
});
await tx
.table("locations")
.toCollection()
.modify((location) => {
location.profileId = defaultProfileId;
});
const settings = await tx.table("settings").get(1);
if (settings) {
await tx
.table("settings")
.update(1, { activeProfileId: defaultProfileId });
} else {
await tx.table("settings").add({
id: 1,
theme: "system",
language: "fr",
activeProfileId: defaultProfileId,
});
}
});
// Version 5: Sync bookkeeping (dirty flag + sync cursor per profile)
this.version(5)
.stores({
sessions:
"id, gameId, dateStart, status, locationId, updatedAt, profileId, dirty",
settings: "id",
players: "id, name, createdAt, updatedAt, profileId, dirty",
locations: "id, name, createdAt, updatedAt, profileId, dirty",
profiles: "id, name, createdAt, updatedAt",
syncState: "profileId",
})
.upgrade(async (tx) => {
// Mark all pre-existing records dirty so they push on the first sync.
for (const table of ["sessions", "players", "locations"]) {
await tx
.table(table)
.toCollection()
.modify((row) => {
row.dirty = 1;
});
}
});
}
}
export const db = new BoardScoreDatabase();
// Initialize default settings if empty
// ---- Auto-stamp updatedAt + dirty on every local write to synced tables ----
// Skipped while the sync engine is applying server data (remoteApply.active).
for (const table of [db.sessions, db.players, db.locations]) {
table.hook("creating", (_primKey, obj: any) => {
if (remoteApply.active) return;
if (obj.updatedAt === undefined) obj.updatedAt = Date.now();
if (obj.dirty === undefined) obj.dirty = 1;
localChange.notify?.();
});
table.hook("updating", (modifications: any) => {
if (remoteApply.active) return;
// Don't clobber an explicit dirty/updatedAt already in this update.
const extra: Record<string, unknown> = {};
if (modifications.updatedAt === undefined) extra.updatedAt = Date.now();
if (modifications.dirty === undefined) extra.dirty = 1;
localChange.notify?.();
return extra;
});
}
// Initialize default profile + settings on a fresh install
db.on("populate", async () => {
const now = Date.now();
const defaultProfileId = generateId();
await db.profiles.add({
id: defaultProfileId,
name: "Moi",
createdAt: now,
updatedAt: now,
});
await db.settings.add({
id: 1,
theme: "system",
language: "fr",
activeProfileId: defaultProfileId,
});
});