Files
gerbeur/api/db/migrate.ts
khannurien 1cb904d2cf
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 47s
v3: generated placeholder thumbnails for pages with no preview image, and a fix for hotlink-protected ones
Thumbnails were failing in two different ways that both ended as an empty box.

Cloudflare hotlink protection answers a cross-site Referer with 403, so images
we had extracted correctly (dles.aukspot.com's og:image among them) never
rendered — every onError handler set display:none and swallowed it. The new
Thumbnail component loads with referrerPolicy="no-referrer", retries once
through /api/proxy-image for hosts that reject an empty referrer too, and only
then falls back to a placeholder.

Separately, the extraction cascade ended at the page's icon and then a guessed
/favicon.ico, so thumbnailUrl was almost never empty — just a 16x16 icon
cover-cropped into a 128x72 box. It now stops at real artwork, with faviconUrl
and accentColor (theme-color / msapplication-TileColor / mask-icon) as their own
fields. An absent thumbnailUrl finally means "no artwork", which is what makes
the placeholder possible: the site's own color mixed into the theme surface,
with its favicon centered on it, or its initial. Contrast holds for any
third-party color by construction rather than by luminance math, so nyt only
has to set --thumb-tint-strength to 0% to stay monochrome and geocities only
has to raise it. Missing accents fall back to a stable hostname-derived hue, so
rows saved before this get a tint with no backfill.

Migration 0011 reclassifies favicon-shaped thumbnailUrls on existing dumps.
The journal mosaic keeps its pull-quote and text fallbacks — the placeholder
appears there only to repair a broken image.

Also fixed: refresh silently overwrote good metadata with a failure stub, the
refresh button swallowed every error, refresh never broadcast the update,
extractBestIcon ranked SVG icons below 16x16 PNGs, and shared links with no
artwork carried no og:image at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:43:55 +00:00

64 lines
2.6 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import { up as up0001CommentLikes } from "./migrations/0001_comment_likes.ts";
import { up as up0002DumpThumbnail } from "./migrations/0002_dump_thumbnail.ts";
import { up as up0003DumpBacklinks } from "./migrations/0003_dump_backlinks.ts";
import { up as up0004UserRoles } from "./migrations/0004_user_roles.ts";
import { up as up0005DropIsAdmin } from "./migrations/0005_drop_is_admin.ts";
import { up as up0006Categories } from "./migrations/0006_categories.ts";
import { up as up0007PasswordResetTokens } from "./migrations/0007_password_reset_tokens.ts";
import { up as up0008ChatMessages } from "./migrations/0008_chat_messages.ts";
import { up as up0009ChatReply } from "./migrations/0009_chat_reply.ts";
import { up as up0010YoutubeEmbedStart } from "./migrations/0010_youtube_embed_start.ts";
import { up as up0011SplitFaviconThumbnail } from "./migrations/0011_split_favicon_thumbnail.ts";
interface Migration {
name: string;
up: (db: DatabaseSync) => void;
}
// Append-only — never edit or reorder a migration once it has shipped.
// Each `up` must be idempotent: it also runs against fresh databases created
// straight from schema.sql, where the change it makes already exists.
const MIGRATIONS: Migration[] = [
{ name: "0001_comment_likes", up: up0001CommentLikes },
{ name: "0002_dump_thumbnail", up: up0002DumpThumbnail },
{ name: "0003_dump_backlinks", up: up0003DumpBacklinks },
{ name: "0004_user_roles", up: up0004UserRoles },
{ name: "0005_drop_is_admin", up: up0005DropIsAdmin },
{ name: "0006_categories", up: up0006Categories },
{ name: "0007_password_reset_tokens", up: up0007PasswordResetTokens },
{ name: "0008_chat_messages", up: up0008ChatMessages },
{ name: "0009_chat_reply", up: up0009ChatReply },
{ name: "0010_youtube_embed_start", up: up0010YoutubeEmbedStart },
{ name: "0011_split_favicon_thumbnail", up: up0011SplitFaviconThumbnail },
];
export function runMigrations(db: DatabaseSync): void {
db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL
);`);
const applied = new Set(
(db.prepare(`SELECT name FROM schema_migrations;`).all() as {
name: string;
}[]).map((r) => r.name),
);
for (const { name, up } of MIGRATIONS) {
if (applied.has(name)) continue;
db.exec("BEGIN;");
try {
up(db);
db.prepare(
`INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?);`,
).run(name, new Date().toISOString());
db.exec("COMMIT;");
console.log(`[migrate] applied ${name}`);
} catch (err) {
db.exec("ROLLBACK;");
throw err;
}
}
}