Compare commits

...
4 Commits
Author SHA1 Message Date
ginnoir b935ff209c chore: release v0.4.10
Release / build-and-push (push) Has been cancelled
2026-06-03 16:16:01 -05:00
ginnoir 3fb5ddc5dd chore: upgrade node 22 to 24 lts 2026-06-03 16:15:00 -05:00
ginnoir ca536b5c33 chore: release v0.4.9
Release / build-and-push (push) Has been cancelled
2026-06-03 16:00:38 -05:00
ginnoir ec2f3930ad feat: proper logo, favicon, and pwa icons
- replace placeholder house svg with clean geometric house mark
  on indigo-700 (#4338CA) — roof triangle, body, door cutout
- add favicon.svg with prefers-color-scheme dark variant
- add icon-16/32 sizes; regenerate all pngs from svg via sharp
- update BrandMark component to use inline svg house instead of 'f'
- wire favicon + sized pngs into layout metadata
- fix middleware to allow icon/manifest/favicon paths without auth
  (android launcher fetches icons in a cookieless system context)
- fix getCurrentSession() to redirect to /login instead of throwing
  when session cookie is stale/invalid
2026-06-03 15:59:34 -05:00
24 changed files with 178 additions and 205 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 24
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
@@ -39,7 +39,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 24
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
+1 -1
View File
@@ -1 +1 @@
22 24
+2 -2
View File
@@ -31,7 +31,7 @@ Codex and Claude Code both work on this project. Keep `AGENTS.md`, `CLAUDE.md`,
| Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users | | Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users |
| Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik | | Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik |
| Package manager | pnpm | Fast, content-addressed store, strict deps | | Package manager | pnpm | Fast, content-addressed store, strict deps |
| Runtime | Node 22 LTS | In `node:22-alpine` container | | Runtime | Node 24 LTS | In `node:24-alpine` container |
| Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack | | Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack |
Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`. Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`.
@@ -106,7 +106,7 @@ All household-scoped entity tables include `household_id`. Sub-entities (`calend
```yaml ```yaml
# compose.yaml (sketch — final lives in /deploy) # compose.yaml (sketch — final lives in /deploy)
services: services:
famapp: # node:22-alpine, Next.js famapp: # node:24-alpine, Next.js
famapp-db: # postgres:16 famapp-db: # postgres:16
authentik-server: authentik-server:
authentik-worker: authentik-worker:
+2
View File
@@ -1,5 +1,7 @@
# Changelog # Changelog
## [0.4.10](https://github.com/ginnoir/famapp/compare/v0.4.9...v0.4.10) (2026-06-03)
## [0.4.8](https://github.com/ginnoir/famapp/compare/v0.4.6...v0.4.8) (2026-06-03) ## [0.4.8](https://github.com/ginnoir/famapp/compare/v0.4.6...v0.4.8) (2026-06-03)
### Features ### Features
+2 -2
View File
@@ -31,7 +31,7 @@ Codex and Claude Code both work on this project. Keep `AGENTS.md`, `CLAUDE.md`,
| Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users | | Realtime | Postgres `LISTEN/NOTIFY` → SSE | Simpler than WebSockets, fine for two users |
| Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik | | Reverse proxy | Caddy (existing) | `fam.ginnoir.com` → famapp:3000, `auth.ginnoir.com` → authentik |
| Package manager | pnpm | Fast, content-addressed store, strict deps | | Package manager | pnpm | Fast, content-addressed store, strict deps |
| Runtime | Node 22 LTS | In `node:22-alpine` container | | Runtime | Node 24 LTS | In `node:24-alpine` container |
| Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack | | Host | Ubuntu Server 24.04 x64, Docker + Compose | Existing self-host stack |
Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`. Domain: `fam.ginnoir.com`. SSO: `auth.ginnoir.com`.
@@ -106,7 +106,7 @@ All household-scoped entity tables include `household_id`. Sub-entities (`calend
```yaml ```yaml
# compose.yaml (sketch — final lives in /deploy) # compose.yaml (sketch — final lives in /deploy)
services: services:
famapp: # node:22-alpine, Next.js famapp: # node:24-alpine, Next.js
famapp-db: # postgres:16 famapp-db: # postgres:16
authentik-server: authentik-server:
authentik-worker: authentik-worker:
+2 -2
View File
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ── Stage 1: fetch dependencies ─────────────────────────────────────────────── # ── Stage 1: fetch dependencies ───────────────────────────────────────────────
FROM node:22-alpine AS deps FROM node:24-alpine AS deps
RUN corepack enable && corepack prepare pnpm@10.33.3 --activate RUN corepack enable && corepack prepare pnpm@10.33.3 --activate
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./ COPY package.json pnpm-lock.yaml .npmrc ./
@@ -20,7 +20,7 @@ RUN --mount=type=cache,id=famapp-nextjs,target=/app/.next/cache \
pnpm build pnpm build
# ── Stage 3: production runtime ─────────────────────────────────────────────── # ── Stage 3: production runtime ───────────────────────────────────────────────
FROM node:22-alpine AS runner FROM node:24-alpine AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PORT=3000 ENV PORT=3000
+1 -1
View File
@@ -2,7 +2,7 @@
## Prerequisites ## Prerequisites
- **Node.js** 22 LTS (`node --version` should print `v22.x.x`) - **Node.js** 24 LTS (`node --version` should print `v24.x.x`)
- **pnpm** 10 (`pnpm --version` should print `10.x.x`) - **pnpm** 10 (`pnpm --version` should print `10.x.x`)
- **Docker** with the Compose plugin (used for the local Postgres + MinIO containers) - **Docker** with the Compose plugin (used for the local Postgres + MinIO containers)
+3 -3
View File
@@ -1,11 +1,11 @@
{ {
"name": "famapp", "name": "famapp",
"version": "0.4.8", "version": "0.4.10",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@10.33.3", "packageManager": "pnpm@10.33.3",
"engines": { "engines": {
"node": ">=22" "node": ">=24"
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@@ -53,7 +53,7 @@
"@release-it/conventional-changelog": "^11.0.1", "@release-it/conventional-changelog": "^11.0.1",
"@tailwindcss/postcss": "^4.2.4", "@tailwindcss/postcss": "^4.2.4",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^22.9.0", "@types/node": "^24.0.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
+94 -94
View File
@@ -73,7 +73,7 @@ importers:
version: 2.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 2.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
shadcn: shadcn:
specifier: ^4.7.0 specifier: ^4.7.0
version: 4.7.0(@types/node@22.19.17)(typescript@5.9.3) version: 4.7.0(@types/node@24.12.4)(typescript@5.9.3)
tailwind-merge: tailwind-merge:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
@@ -89,7 +89,7 @@ importers:
devDependencies: devDependencies:
'@commitlint/cli': '@commitlint/cli':
specifier: ^21.0.2 specifier: ^21.0.2
version: 21.0.2(@types/node@22.19.17)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@5.9.3) version: 21.0.2(@types/node@24.12.4)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@5.9.3)
'@commitlint/config-conventional': '@commitlint/config-conventional':
specifier: ^21.0.2 specifier: ^21.0.2
version: 21.0.2 version: 21.0.2
@@ -101,7 +101,7 @@ importers:
version: 1.59.1 version: 1.59.1
'@release-it/conventional-changelog': '@release-it/conventional-changelog':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(release-it@20.2.0(@types/node@22.19.17)) version: 11.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(release-it@20.2.0(@types/node@24.12.4))
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.2.4 specifier: ^4.2.4
version: 4.2.4 version: 4.2.4
@@ -109,8 +109,8 @@ importers:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
'@types/node': '@types/node':
specifier: ^22.9.0 specifier: ^24.0.0
version: 22.19.17 version: 24.12.4
'@types/react': '@types/react':
specifier: ^19.2.14 specifier: ^19.2.14
version: 19.2.14 version: 19.2.14
@@ -149,7 +149,7 @@ importers:
version: 3.8.3 version: 3.8.3
release-it: release-it:
specifier: ^20.2.0 specifier: ^20.2.0
version: 20.2.0(@types/node@22.19.17) version: 20.2.0(@types/node@24.12.4)
tailwindcss: tailwindcss:
specifier: ^4.2.4 specifier: ^4.2.4
version: 4.2.4 version: 4.2.4
@@ -1862,8 +1862,8 @@ packages:
'@types/json5@0.0.29': '@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/node@22.19.17': '@types/node@24.12.4':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
'@types/normalize-package-data@2.4.4': '@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -4895,8 +4895,8 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
undici-types@6.21.0: undici-types@7.16.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici@7.24.5: undici@7.24.5:
resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==} resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
@@ -5347,11 +5347,11 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@commitlint/cli@21.0.2(@types/node@22.19.17)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': '@commitlint/cli@21.0.2(@types/node@24.12.4)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@5.9.3)':
dependencies: dependencies:
'@commitlint/format': 21.0.1 '@commitlint/format': 21.0.1
'@commitlint/lint': 21.0.2 '@commitlint/lint': 21.0.2
'@commitlint/load': 21.0.2(@types/node@22.19.17)(typescript@5.9.3) '@commitlint/load': 21.0.2(@types/node@24.12.4)(typescript@5.9.3)
'@commitlint/read': 21.0.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) '@commitlint/read': 21.0.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
'@commitlint/types': 21.0.1 '@commitlint/types': 21.0.1
tinyexec: 1.2.4 tinyexec: 1.2.4
@@ -5396,14 +5396,14 @@ snapshots:
'@commitlint/rules': 21.0.2 '@commitlint/rules': 21.0.2
'@commitlint/types': 21.0.1 '@commitlint/types': 21.0.1
'@commitlint/load@21.0.2(@types/node@22.19.17)(typescript@5.9.3)': '@commitlint/load@21.0.2(@types/node@24.12.4)(typescript@5.9.3)':
dependencies: dependencies:
'@commitlint/config-validator': 21.0.1 '@commitlint/config-validator': 21.0.1
'@commitlint/execute-rule': 21.0.1 '@commitlint/execute-rule': 21.0.1
'@commitlint/resolve-extends': 21.0.1 '@commitlint/resolve-extends': 21.0.1
'@commitlint/types': 21.0.1 '@commitlint/types': 21.0.1
cosmiconfig: 9.0.1(typescript@5.9.3) cosmiconfig: 9.0.1(typescript@5.9.3)
cosmiconfig-typescript-loader: 6.3.0(@types/node@22.19.17)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) cosmiconfig-typescript-loader: 6.3.0(@types/node@24.12.4)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3)
es-toolkit: 1.47.0 es-toolkit: 1.47.0
is-plain-obj: 4.1.0 is-plain-obj: 4.1.0
picocolors: 1.1.1 picocolors: 1.1.1
@@ -5942,140 +5942,140 @@ snapshots:
'@inquirer/ansi@2.0.7': {} '@inquirer/ansi@2.0.7': {}
'@inquirer/checkbox@5.2.1(@types/node@22.19.17)': '@inquirer/checkbox@5.2.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/ansi': 2.0.7 '@inquirer/ansi': 2.0.7
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/figures': 2.0.7 '@inquirer/figures': 2.0.7
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/confirm@6.0.12(@types/node@22.19.17)': '@inquirer/confirm@6.0.12(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.1.9(@types/node@22.19.17) '@inquirer/core': 11.1.9(@types/node@24.12.4)
'@inquirer/type': 4.0.5(@types/node@22.19.17) '@inquirer/type': 4.0.5(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/core@11.1.9(@types/node@22.19.17)': '@inquirer/core@11.1.9(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/ansi': 2.0.5 '@inquirer/ansi': 2.0.5
'@inquirer/figures': 2.0.5 '@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@22.19.17) '@inquirer/type': 4.0.5(@types/node@24.12.4)
cli-width: 4.1.0 cli-width: 4.1.0
fast-wrap-ansi: 0.2.0 fast-wrap-ansi: 0.2.0
mute-stream: 3.0.0 mute-stream: 3.0.0
signal-exit: 4.1.0 signal-exit: 4.1.0
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/core@11.2.1(@types/node@22.19.17)': '@inquirer/core@11.2.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/ansi': 2.0.7 '@inquirer/ansi': 2.0.7
'@inquirer/figures': 2.0.7 '@inquirer/figures': 2.0.7
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
cli-width: 4.1.0 cli-width: 4.1.0
fast-wrap-ansi: 0.2.0 fast-wrap-ansi: 0.2.0
mute-stream: 3.0.0 mute-stream: 3.0.0
signal-exit: 4.1.0 signal-exit: 4.1.0
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/editor@5.2.2(@types/node@22.19.17)': '@inquirer/editor@5.2.2(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/external-editor': 3.0.3(@types/node@22.19.17) '@inquirer/external-editor': 3.0.3(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/expand@5.1.1(@types/node@22.19.17)': '@inquirer/expand@5.1.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/external-editor@3.0.3(@types/node@22.19.17)': '@inquirer/external-editor@3.0.3(@types/node@24.12.4)':
dependencies: dependencies:
chardet: 2.1.1 chardet: 2.1.1
iconv-lite: 0.7.2 iconv-lite: 0.7.2
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/figures@2.0.5': {} '@inquirer/figures@2.0.5': {}
'@inquirer/figures@2.0.7': {} '@inquirer/figures@2.0.7': {}
'@inquirer/input@5.1.2(@types/node@22.19.17)': '@inquirer/input@5.1.2(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/number@4.1.1(@types/node@22.19.17)': '@inquirer/number@4.1.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/password@5.1.1(@types/node@22.19.17)': '@inquirer/password@5.1.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/ansi': 2.0.7 '@inquirer/ansi': 2.0.7
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/prompts@8.4.2(@types/node@22.19.17)': '@inquirer/prompts@8.4.2(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/checkbox': 5.2.1(@types/node@22.19.17) '@inquirer/checkbox': 5.2.1(@types/node@24.12.4)
'@inquirer/confirm': 6.0.12(@types/node@22.19.17) '@inquirer/confirm': 6.0.12(@types/node@24.12.4)
'@inquirer/editor': 5.2.2(@types/node@22.19.17) '@inquirer/editor': 5.2.2(@types/node@24.12.4)
'@inquirer/expand': 5.1.1(@types/node@22.19.17) '@inquirer/expand': 5.1.1(@types/node@24.12.4)
'@inquirer/input': 5.1.2(@types/node@22.19.17) '@inquirer/input': 5.1.2(@types/node@24.12.4)
'@inquirer/number': 4.1.1(@types/node@22.19.17) '@inquirer/number': 4.1.1(@types/node@24.12.4)
'@inquirer/password': 5.1.1(@types/node@22.19.17) '@inquirer/password': 5.1.1(@types/node@24.12.4)
'@inquirer/rawlist': 5.3.1(@types/node@22.19.17) '@inquirer/rawlist': 5.3.1(@types/node@24.12.4)
'@inquirer/search': 4.2.1(@types/node@22.19.17) '@inquirer/search': 4.2.1(@types/node@24.12.4)
'@inquirer/select': 5.2.1(@types/node@22.19.17) '@inquirer/select': 5.2.1(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/rawlist@5.3.1(@types/node@22.19.17)': '@inquirer/rawlist@5.3.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/search@4.2.1(@types/node@22.19.17)': '@inquirer/search@4.2.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/figures': 2.0.7 '@inquirer/figures': 2.0.7
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/select@5.2.1(@types/node@22.19.17)': '@inquirer/select@5.2.1(@types/node@24.12.4)':
dependencies: dependencies:
'@inquirer/ansi': 2.0.7 '@inquirer/ansi': 2.0.7
'@inquirer/core': 11.2.1(@types/node@22.19.17) '@inquirer/core': 11.2.1(@types/node@24.12.4)
'@inquirer/figures': 2.0.7 '@inquirer/figures': 2.0.7
'@inquirer/type': 4.0.7(@types/node@22.19.17) '@inquirer/type': 4.0.7(@types/node@24.12.4)
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/type@4.0.5(@types/node@22.19.17)': '@inquirer/type@4.0.5(@types/node@24.12.4)':
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@inquirer/type@4.0.7(@types/node@22.19.17)': '@inquirer/type@4.0.7(@types/node@24.12.4)':
optionalDependencies: optionalDependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
dependencies: dependencies:
@@ -6431,7 +6431,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@release-it/conventional-changelog@11.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(release-it@20.2.0(@types/node@22.19.17))': '@release-it/conventional-changelog@11.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(release-it@20.2.0(@types/node@24.12.4))':
dependencies: dependencies:
'@conventional-changelog/git-client': 2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) '@conventional-changelog/git-client': 2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
concat-stream: 2.0.0 concat-stream: 2.0.0
@@ -6439,7 +6439,7 @@ snapshots:
conventional-changelog-angular: 8.3.1 conventional-changelog-angular: 8.3.1
conventional-changelog-conventionalcommits: 9.3.1 conventional-changelog-conventionalcommits: 9.3.1
conventional-recommended-bump: 11.2.0 conventional-recommended-bump: 11.2.0
release-it: 20.2.0(@types/node@22.19.17) release-it: 20.2.0(@types/node@24.12.4)
semver: 7.7.4 semver: 7.7.4
transitivePeerDependencies: transitivePeerDependencies:
- conventional-commits-filter - conventional-commits-filter
@@ -6551,9 +6551,9 @@ snapshots:
'@types/json5@0.0.29': {} '@types/json5@0.0.29': {}
'@types/node@22.19.17': '@types/node@24.12.4':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 7.16.0
'@types/normalize-package-data@2.4.4': {} '@types/normalize-package-data@2.4.4': {}
@@ -6571,7 +6571,7 @@ snapshots:
'@types/set-cookie-parser@2.4.10': '@types/set-cookie-parser@2.4.10':
dependencies: dependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@types/statuses@2.0.6': {} '@types/statuses@2.0.6': {}
@@ -6579,7 +6579,7 @@ snapshots:
'@types/web-push@3.6.4': '@types/web-push@3.6.4':
dependencies: dependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
dependencies: dependencies:
@@ -7159,9 +7159,9 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
cosmiconfig-typescript-loader@6.3.0(@types/node@22.19.17)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): cosmiconfig-typescript-loader@6.3.0(@types/node@24.12.4)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3):
dependencies: dependencies:
'@types/node': 22.19.17 '@types/node': 24.12.4
cosmiconfig: 9.0.1(typescript@5.9.3) cosmiconfig: 9.0.1(typescript@5.9.3)
jiti: 2.6.1 jiti: 2.6.1
typescript: 5.9.3 typescript: 5.9.3
@@ -8658,9 +8658,9 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
msw@2.14.3(@types/node@22.19.17)(typescript@5.9.3): msw@2.14.3(@types/node@24.12.4)(typescript@5.9.3):
dependencies: dependencies:
'@inquirer/confirm': 6.0.12(@types/node@22.19.17) '@inquirer/confirm': 6.0.12(@types/node@24.12.4)
'@mswjs/interceptors': 0.41.8 '@mswjs/interceptors': 0.41.8
'@open-draft/deferred-promise': 3.0.0 '@open-draft/deferred-promise': 3.0.0
'@types/statuses': 2.0.6 '@types/statuses': 2.0.6
@@ -9233,9 +9233,9 @@ snapshots:
gopd: 1.2.0 gopd: 1.2.0
set-function-name: 2.0.2 set-function-name: 2.0.2
release-it@20.2.0(@types/node@22.19.17): release-it@20.2.0(@types/node@24.12.4):
dependencies: dependencies:
'@inquirer/prompts': 8.4.2(@types/node@22.19.17) '@inquirer/prompts': 8.4.2(@types/node@24.12.4)
'@octokit/rest': 22.0.1 '@octokit/rest': 22.0.1
'@phun-ky/typeof': 2.0.3 '@phun-ky/typeof': 2.0.3
async-retry: 1.3.3 async-retry: 1.3.3
@@ -9401,7 +9401,7 @@ snapshots:
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
shadcn@4.7.0(@types/node@22.19.17)(typescript@5.9.3): shadcn@4.7.0(@types/node@24.12.4)(typescript@5.9.3):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/parser': 7.29.3 '@babel/parser': 7.29.3
@@ -9422,7 +9422,7 @@ snapshots:
fuzzysort: 3.1.0 fuzzysort: 3.1.0
https-proxy-agent: 7.0.6 https-proxy-agent: 7.0.6
kleur: 4.1.5 kleur: 4.1.5
msw: 2.14.3(@types/node@22.19.17)(typescript@5.9.3) msw: 2.14.3(@types/node@24.12.4)(typescript@5.9.3)
node-fetch: 3.3.2 node-fetch: 3.3.2
open: 11.0.0 open: 11.0.0
ora: 8.2.0 ora: 8.2.0
@@ -9860,7 +9860,7 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
which-boxed-primitive: 1.1.1 which-boxed-primitive: 1.1.1
undici-types@6.21.0: {} undici-types@7.16.0: {}
undici@7.24.5: {} undici@7.24.5: {}
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<style>
@media (prefers-color-scheme: dark) { .bg { fill: #6366F1; } }
</style>
<rect class="bg" width="512" height="512" rx="96" fill="#4338CA"/>
<rect x="130" y="238" width="252" height="190" fill="white"/>
<polygon points="256,82 68,254 444,254" fill="white"/>
<rect x="216" y="338" width="80" height="90" rx="8" fill="#4338CA"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+4 -10
View File
@@ -1,12 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- Background --> <rect width="512" height="512" rx="96" fill="#4338CA"/>
<rect width="512" height="512" rx="80" fill="#4F46E5"/> <rect x="130" y="238" width="252" height="190" fill="white"/>
<!-- House body --> <polygon points="256,82 68,254 444,254" fill="white"/>
<polygon points="256,108 396,234 364,234 364,392 148,392 148,234 116,234" fill="white"/> <rect x="216" y="338" width="80" height="90" rx="8" fill="#4338CA"/>
<!-- Door -->
<rect x="212" y="296" width="88" height="96" rx="6" fill="#4F46E5"/>
<!-- Left window -->
<rect x="164" y="254" width="66" height="54" rx="6" fill="#4F46E5" opacity="0.55"/>
<!-- Right window -->
<rect x="282" y="254" width="66" height="54" rx="6" fill="#4F46E5" opacity="0.55"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 345 B

+8 -5
View File
@@ -4,23 +4,26 @@
"description": "Family coordination app", "description": "Family coordination app",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#ffffff", "background_color": "#4338CA",
"theme_color": "#4F46E5", "theme_color": "#4338CA",
"icons": [ "icons": [
{ {
"src": "/icon-192.png", "src": "/icon-192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png",
"purpose": "any"
}, },
{ {
"src": "/icon-384.png", "src": "/icon-384.png",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png",
"purpose": "any"
}, },
{ {
"src": "/icon-512.png", "src": "/icon-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png",
"purpose": "any"
}, },
{ {
"src": "/icon-512-maskable.png", "src": "/icon-512-maskable.png",
+27 -79
View File
@@ -1,100 +1,48 @@
/** /**
* Generates placeholder PNG icons for the famapp PWA. * Rasterizes public/icon.svg into all required PNG icon sizes.
* *
* Usage: node scripts/generate-icons.mjs * Usage: node scripts/generate-icons.mjs
* *
* Produces: * Produces:
* public/icon-16.png
* public/icon-32.png
* public/icon-180.png (apple-touch-icon)
* public/icon-192.png * public/icon-192.png
* public/icon-384.png * public/icon-384.png
* public/icon-512.png * public/icon-512.png
* public/icon-512-maskable.png (same image; OS applies its own mask) * public/icon-512-maskable.png (square bg, content in safe zone)
* 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 sharp from "sharp";
import { writeFileSync } from "fs"; import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dir = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const publicDir = resolve(__dir, "../public"); const pub = join(__dirname, "..", "public");
// Build CRC-32 lookup table once. const svg = readFileSync(join(pub, "icon.svg"));
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) { const sizes = [
let crc = 0xffffffff; { name: "icon-16.png", size: 16 },
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); { name: "icon-32.png", size: 32 },
return (crc ^ 0xffffffff) >>> 0; { name: "icon-180.png", size: 180 },
}
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-192.png", size: 192 },
{ name: "icon-384.png", size: 384 }, { name: "icon-384.png", size: 384 },
{ name: "icon-512.png", size: 512 }, { 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) { for (const { name, size } of sizes) {
const out = resolve(publicDir, name); await sharp(svg).resize(size, size).png().toFile(join(pub, name));
writeFileSync(out, solidPNG(size, R, G, B));
console.log(` ✓ public/${name} (${size}×${size})`); console.log(` ✓ public/${name} (${size}×${size})`);
} }
// Maskable: remove rounded corners so the background fills the full canvas,
// allowing the OS to apply any mask shape without clipping the icon corners.
const maskableSvg = readFileSync(join(pub, "icon.svg"), "utf-8").replace('rx="96"', "");
await sharp(Buffer.from(maskableSvg))
.resize(512, 512)
.png()
.toFile(join(pub, "icon-512-maskable.png"));
console.log(` ✓ public/icon-512-maskable.png (512×512 maskable)`);
+5
View File
@@ -54,6 +54,11 @@ export const metadata: Metadata = {
title: "famapp", title: "famapp",
}, },
icons: { icons: {
icon: [
{ url: "/favicon.svg", type: "image/svg+xml" },
{ url: "/icon-32.png", sizes: "32x32", type: "image/png" },
{ url: "/icon-16.png", sizes: "16x16", type: "image/png" },
],
apple: "/icon-180.png", apple: "/icon-180.png",
}, },
}; };
+12 -1
View File
@@ -1,12 +1,23 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function BrandMark({ size = "md", className }: { size?: "sm" | "md"; className?: string }) { export function BrandMark({ size = "md", className }: { size?: "sm" | "md"; className?: string }) {
const iconSize = size === "sm" ? 14 : 17;
return ( return (
<span <span
className={cn("brand-mark", size === "sm" && "brand-mark-sm", className)} className={cn("brand-mark", size === "sm" && "brand-mark-sm", className)}
aria-hidden="true" aria-hidden="true"
> >
f <svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
width={iconSize}
height={iconSize}
fill="currentColor"
>
<polygon points="12,2 1,12 23,12" />
<rect x="3.5" y="11" width="17" height="12" />
<rect x="9.5" y="16" width="5" height="7" rx="1" style={{ fill: "var(--ink)" }} />
</svg>
</span> </span>
); );
} }
+2 -1
View File
@@ -1,4 +1,5 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { householdMembers, households, users } from "@/modules/_core/schema"; import { householdMembers, households, users } from "@/modules/_core/schema";
@@ -13,7 +14,7 @@ export interface CurrentSession {
export async function getCurrentSession(): Promise<CurrentSession> { export async function getCurrentSession(): Promise<CurrentSession> {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Not authenticated"); if (!session?.user?.id) redirect("/login");
const [row] = await db const [row] = await db
.select({ .select({
+2 -2
View File
@@ -1,8 +1,8 @@
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { consume } from "@/lib/rate-limit"; import { consume } from "@/lib/rate-limit";
const PUBLIC_PREFIXES = ["/api/auth/", "/s/"]; const PUBLIC_PREFIXES = ["/api/auth/", "/s/", "/icon-", "/favicon"];
const PUBLIC_PATHS = new Set(["/login"]); const PUBLIC_PATHS = new Set(["/login", "/manifest.webmanifest", "/offline.html"]);
const SESSION_COOKIE_NAMES = ["authjs.session-token", "__Secure-authjs.session-token"]; const SESSION_COOKIE_NAMES = ["authjs.session-token", "__Secure-authjs.session-token"];
// Token-prefix length used as part of the rate-limit bucket key. // Token-prefix length used as part of the rate-limit bucket key.