always set lane

This commit is contained in:
2026-02-25 20:00:43 +01:00
parent ea22da9e5a
commit f3b645ac53
13 changed files with 325 additions and 102 deletions

View File

@@ -16,6 +16,7 @@
"db:migrate": "drizzle-kit migrate",
"check-types": "tsc --noEmit",
"lint": "eslint .",
"db:fix-lanes": "node --env-file=../../apps/web/.env --import=tsx src/scripts/fix-lanes.ts",
"db:start": "docker compose up -d",
"db:watch": "docker compose up",
"db:stop": "docker compose stop",
@@ -34,6 +35,7 @@
"@zendegi/eslint-config": "workspace:*",
"drizzle-kit": "^0.31.8",
"eslint": "^9.17.0",
"tsx": "^4.21.0",
"typescript": "catalog:"
}
}

View File

@@ -42,6 +42,7 @@ export const timelineItem = pgTable("timeline_item", {
start: timestamp("start", { withTimezone: true }).notNull().defaultNow(),
// Allow null to denote a event without duration
end: timestamp("end", { withTimezone: true }),
lane: integer("lane").notNull().default(1),
...timestamps,
});

View File

@@ -0,0 +1,88 @@
/**
* One-time script to backfill lane values for existing timeline items.
*
* For each group: fetches items ordered by start, assigns lanes with
* the greedy algorithm, and updates the DB.
*
* Run from repo root:
* node --env-file=apps/web/.env --import=tsx packages/db/src/scripts/fix-lanes.ts
*/
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { timelineGroup, timelineItem } from "../schema/timeline";
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
throw new Error("DATABASE_URL is required");
}
interface ItemForLane {
id: string;
start: Date;
end: Date | null;
lane: number;
}
function assignLane(
existing: ItemForLane[],
newStart: Date,
newEnd: Date | null
): number {
const end = newEnd ?? new Date(newStart.getTime() + 24 * 60 * 60 * 1000);
for (let lane = 1; lane <= 100; lane++) {
const hasConflict = existing.some((item) => {
if (item.lane !== lane) return false;
const itemEnd =
item.end ?? new Date(item.start.getTime() + 24 * 60 * 60 * 1000);
return newStart < itemEnd && end > item.start;
});
if (!hasConflict) return lane;
}
return 1;
}
async function main() {
const db = drizzle(DATABASE_URL!);
const groups = await db.select({ id: timelineGroup.id }).from(timelineGroup);
console.log(`Found ${groups.length} groups`);
for (const group of groups) {
const items = await db
.select()
.from(timelineItem)
.where(eq(timelineItem.timelineGroupId, group.id))
.orderBy(timelineItem.start);
if (items.length === 0) continue;
const assigned: ItemForLane[] = [];
let updated = 0;
for (const item of items) {
const lane = assignLane(assigned, item.start, item.end);
assigned.push({ id: item.id, start: item.start, end: item.end, lane });
if (item.lane !== lane) {
await db
.update(timelineItem)
.set({ lane })
.where(eq(timelineItem.id, item.id));
updated++;
}
}
console.log(
`Group ${group.id}: ${items.length} items, ${updated} lanes updated`
);
}
console.log("Done");
process.exit(0);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});