src/lib/dev-login-config.ts — startup assertion: throws if NODE_ENV=production + ENABLE_DEV_LOGIN=true, scoped to runtime (skipped during next build).
Container
scripts/migrate.mjs — runs Drizzle migrations against DATABASE_URL.
deploy/docker-entrypoint.sh — runs migrations then exec node server.js. Skip with RUN_MIGRATIONS=false.
Dockerfile — copies drizzle/, scripts/migrate.mjs, entrypoint into runner stage; ENTRYPOINT now points at the script.
Compose
deploy/compose.yaml — famapp now image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} (build still works locally as fallback). Authentik pinned via AUTHENTIK_IMAGE_TAG (default 2024.12.3). New RUN_MIGRATIONS env passed through.
.env.production.example — documents FAMAPP_IMAGE, AUTHENTIK_IMAGE_TAG, RUN_MIGRATIONS.
CI/CD
.github/workflows/ci.yml — push/PR: typecheck + lint + format:check + build.
.github/workflows/release.yml — v* tag: build + push ghcr.io/ginnoir/famapp:vX.Y.Z, :X.Y, :latest to GHCR.
Docs
deploy/README.md — full deploy/rollback/release runbook.
CHANGELOG.md — release log seeded with an Unreleased entry.
docs/tasks/09-pre-deploy-checklist.md — task 09 reframed from one-shot removal to a recurring pre-deploy checklist.
STATUS.md — updated.
Verified: pnpm typecheck, pnpm format, pnpm build, and docker compose config all clean.
101 lines
2.7 KiB
JavaScript
101 lines
2.7 KiB
JavaScript
/**
|
||
* Generates placeholder PNG icons for the famapp PWA.
|
||
*
|
||
* Usage: node scripts/generate-icons.mjs
|
||
*
|
||
* Produces:
|
||
* public/icon-192.png
|
||
* public/icon-384.png
|
||
* public/icon-512.png
|
||
* public/icon-512-maskable.png (same image; OS applies its own mask)
|
||
* public/icon-180.png (apple-touch-icon)
|
||
*
|
||
* All images are solid indigo (#4F46E5) squares — replace with a real
|
||
* branded export from icon.svg when assets are finalised.
|
||
*/
|
||
|
||
import { deflateSync } from "zlib";
|
||
import { writeFileSync } from "fs";
|
||
import { resolve, dirname } from "path";
|
||
import { fileURLToPath } from "url";
|
||
|
||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||
const publicDir = resolve(__dir, "../public");
|
||
|
||
// Build CRC-32 lookup table once.
|
||
const CRC_TABLE = new Uint32Array(256);
|
||
for (let i = 0; i < 256; i++) {
|
||
let c = i;
|
||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||
CRC_TABLE[i] = c;
|
||
}
|
||
|
||
function crc32(buf) {
|
||
let crc = 0xffffffff;
|
||
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||
return (crc ^ 0xffffffff) >>> 0;
|
||
}
|
||
|
||
function u32(n) {
|
||
const b = Buffer.alloc(4);
|
||
b.writeUInt32BE(n, 0);
|
||
return b;
|
||
}
|
||
|
||
function pngChunk(type, data) {
|
||
const typeBytes = Buffer.from(type, "ascii");
|
||
const crc = u32(crc32(Buffer.concat([typeBytes, data])));
|
||
return Buffer.concat([u32(data.length), typeBytes, data, crc]);
|
||
}
|
||
|
||
/**
|
||
* Returns a Buffer containing a valid PNG of size×size filled with (r, g, b).
|
||
*/
|
||
function solidPNG(size, r, g, b) {
|
||
const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
|
||
const ihdr = pngChunk(
|
||
"IHDR",
|
||
Buffer.concat([
|
||
u32(size),
|
||
u32(size),
|
||
Buffer.from([8, 2, 0, 0, 0]), // 8-bit RGB, no interlace
|
||
]),
|
||
);
|
||
|
||
// One filter byte (0 = None) followed by size×3 RGB bytes per row.
|
||
const rowLen = 1 + size * 3;
|
||
const raw = Buffer.alloc(size * rowLen);
|
||
for (let y = 0; y < size; y++) {
|
||
const base = y * rowLen;
|
||
raw[base] = 0;
|
||
for (let x = 0; x < size; x++) {
|
||
raw[base + 1 + x * 3] = r;
|
||
raw[base + 2 + x * 3] = g;
|
||
raw[base + 3 + x * 3] = b;
|
||
}
|
||
}
|
||
|
||
const idat = pngChunk("IDAT", deflateSync(raw));
|
||
const iend = pngChunk("IEND", Buffer.alloc(0));
|
||
|
||
return Buffer.concat([PNG_SIG, ihdr, idat, iend]);
|
||
}
|
||
|
||
// Indigo-600 (#4F46E5)
|
||
const [R, G, B] = [0x4f, 0x46, 0xe5];
|
||
|
||
const icons = [
|
||
{ name: "icon-192.png", size: 192 },
|
||
{ name: "icon-384.png", size: 384 },
|
||
{ name: "icon-512.png", size: 512 },
|
||
{ name: "icon-512-maskable.png", size: 512 },
|
||
{ name: "icon-180.png", size: 180 },
|
||
];
|
||
|
||
for (const { name, size } of icons) {
|
||
const out = resolve(publicDir, name);
|
||
writeFileSync(out, solidPNG(size, R, G, B));
|
||
console.log(` ✓ public/${name} (${size}×${size})`);
|
||
}
|