diff --git a/src/databases/officer_db/src/dashboards/queries.ts b/src/databases/officer_db/src/dashboards/queries.ts index d2533314..e5ed65b4 100644 --- a/src/databases/officer_db/src/dashboards/queries.ts +++ b/src/databases/officer_db/src/dashboards/queries.ts @@ -86,22 +86,47 @@ function buildDashboardSet(data: UpsertDashboardData, now: Date): Record` (the layout) as two separate + * PATCHes. Both call this, both found no row, both INSERTed, and the loser hit the primary key. Observed + * 2026-08-15 — the surviving row and the failing insert were stamped ONE MILLISECOND apart, and the user + * saw "Internal Server Error" for a dashboard that had in fact been created. + * + * `ON CONFLICT DO UPDATE` makes the two orderings equivalent instead of one of them fatal. `set` only + * carries the fields the caller actually passed, so whichever request lands second updates its own column + * and leaves the other's alone. + * + * The `where` is not decoration. `dashboards.id` is a GLOBAL primary key fed by a slug, so two accounts + * naming a dashboard the same thing collide — and without this, the second one's write would silently + * take over the first one's row. With it, the update matches nothing and the write is dropped. That is + * still wrong, but it is wrong in the direction of not handing someone another account's dashboard, and + * the real fix is a composite key (see TODO.md). + */ export async function upsertDashboard(userId: number, id: string, data: UpsertDashboardData): Promise { const now = new Date(); - if (await updateDashboard(userId, id, data)) return; - await db.insert(dashboards).values({ - id, - userId, - name: data.name ?? id, - config: data.config ?? {}, - layout: data.layout ?? null, - terminals: data.terminals ?? {}, - hostTerminals: data.hostTerminals ?? {}, - sortOrder: data.sortOrder ?? 0, - createdAt: now, - updatedAt: now, - }); + await db + .insert(dashboards) + .values({ + id, + userId, + name: data.name ?? id, + config: data.config ?? {}, + layout: data.layout ?? null, + terminals: data.terminals ?? {}, + hostTerminals: data.hostTerminals ?? {}, + sortOrder: data.sortOrder ?? 0, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: dashboards.id, + set: buildDashboardSet(data, now), + where: eq(dashboards.userId, userId), + }); } /**