enable dev|prod envs with simpler env vars system

This commit is contained in:
JCEEE
2026-08-06 18:09:47 +01:00
parent 2fc1668356
commit 4f384426d0
37 changed files with 447 additions and 256 deletions
+13 -10
View File
@@ -1,13 +1,16 @@
SERVER_IP=0.0.0.0 # Runtime env for the SvelteKit + Hono app.
PROXY_PORT=3456 #
FRONTEND_PORT=2080 # The app's own loopback URLs are constants in code (PROXY_URL / PB_ENDPOINT);
PB_PORT=8090 # ports live in config.ts for the proxy. Only these are real env vars:
# PB superuser (server-side only). Defaults in code: debug@famchamp.dev / debug123.
PB_EMAIL= PB_EMAIL=
PB_PASSWORD= PB_PASSWORD=
DEBUG_RECORD_ID= # Public: the dev machine's IP where PB + the dev proxy run. Change this when
PUBLIC_PB_URL= # your remote IP changes — the browser (pocketbase.ts) and pb-admin read it.
# Prod ignores this (uses /pb via nginx). Default: 192.168.1.225.
SERVER_IP=192.168.1.225
# docker-compose (staging) # docker-compose (staging) — host-side deploy config, never baked into the image.
PORT=3001 PORT=3001 # public port to publish (nginx container listens on 3001)
PB_DATA=./pb_data PB_DATA=./pb_data # where to persist PocketBase data on the host
PB_ENDPOINT=http://127.0.0.1:8090
+13 -12
View File
@@ -275,21 +275,22 @@ All admin operations go through PB admin API (Hono proxy or `+page.server.ts`).
## 7. Environment Variables ## 7. Environment Variables
### Frontend + Hono container ### Current (this repo)
``` ```
PUBLIC_PB_URL=https://pb.chores.app.com # .env (dev; loaded by vite for the frontend + tsx --env-file for the proxy)
PB_ADMIN_EMAIL=admin@chores.app PB_EMAIL=debug@famchamp.dev # PB superuser (server-only; pb-admin + proxy)
PB_ADMIN_PASSWORD=<super-admin-password> PB_PASSWORD=debug123
STRIPE_SECRET_KEY=sk_live_xxx SERVER_IP=192.168.1.225 # dev machine IP (Tailscale IP when away) — change when it changes
STRIPE_WEBHOOK_SECRET=whsec_xxx
DONATION_MODAL_INTERVAL=30
``` ```
- Ports live in root `config.ts` (`FRONTEND_PORT`/`PROXY_PORT`/`PB_PORT` = `2080`/`3456`/`8090`) — the proxy reads them; SvelteKit never imports `config.ts`.
- SvelteKit env vars are declared in `frontend/src/env.ts` (`PROXY_URL`, `SERVER_IP`, `PB_EMAIL`, `PB_PASSWORD`) and read via `$app/env/public` / `$app/env/private`.
- Compose/deploy: `PORT` (public port, default `3001`), `PB_DATA` (host data dir). `PUBLIC_PB_URL` no longer exists (docker bakes `/pb`; browser PB URL derives from `SERVER_IP` in dev).
- Not yet wired: `STRIPE_SECRET_KEY`, `DONATION_MODAL_INTERVAL`.
### PocketBase container ### Dev vs Prod PocketBase data (⚠️)
``` - **Dev (`pnpm dev`)**: permanent dev PB = container **`pb-dev`** at `:8090`, data in host `./pb_data`. This is what the code talks to via `SERVER_IP:8090`.
PB_SUPERUSER_EMAIL=you@email.com - **Prod/docker**: the app container bundles its **own internal PB**, published loopback-only at `127.0.0.1:8091`, and also bind-mounts `./pb_data`.
PB_SUPERUSER_PASSWORD=<your-password> - Never recreate `pb-dev` with a fresh volume — restore it with `-v "$PWD/pb_data:/pb_data"` (full command in `RULES.md`).
```
--- ---
+18
View File
@@ -159,3 +159,21 @@
- **Check**: frontend `svelte-check` stays at 12 baseline errors. Note: frontend dev server on :2080 was not running when verified (proxy :3456 up). - **Check**: frontend `svelte-check` stays at 12 baseline errors. Note: frontend dev server on :2080 was not running when verified (proxy :3456 up).
### 2026-08-06 — Env Consolidation: `SERVER_IP`, `PROXY_URL`, and SvelteKit env only
- **`config.ts` is proxy-only.** It now holds just the three ports (`FRONTEND_PORT`/`PROXY_PORT`/`PB_PORT` = `2080`/`3456`/`8090`). SvelteKit **never imports `config.ts`** — SvelteKit env vars are declared in `frontend/src/env.ts` and read via `$app/env/*`. Deleted the stale `config.js/.d.ts/.map` artifacts.
- **`frontend/src/env.ts`** declares: `PROXY_URL` (public, default `http://127.0.0.1:3456`), `SERVER_IP` (public, default `192.168.1.225`), `PB_EMAIL`/`PB_PASSWORD` (private, defaults). `PUBLIC_PB_URL` removed (was the source of a startup crash when unset).
- **Deleted `frontend/src/lib/server/env.ts`** (untracked). All server modules now `import { PROXY_URL } from '$app/env/public'` (`hono.ts`, `auth.ts`, `+layout.server.ts`, `+page.server.ts`, `preferences`, `join/[code]/[member]`). `admin/+page.server.ts` imports creds from `$app/env/private`.
- **`frontend/src/lib/pocketbase.ts` (browser) + `pb-admin.ts`**: `PB_ENDPOINT = import.meta.env.PROD ? '/pb' : \`http://${SERVER_IP}:8090\``. (Fixed a bug where `pocketbase.ts` used `import.meta.env.SERVER_IP` → undefined.)
- **`proxy/src/env.ts`** (new): `PB_ENDPOINT = SERVER_IP ? \`http://${SERVER_IP}:8090\` : \`http://127.0.0.1:8090\``. Dev env is loaded by the proxy's `dev`/`seed` scripts via `tsx --env-file-if-exists=../.env` (pnpm has no `--env-file`; `NODE_OPTIONS='--env-file=…'` is rejected by Node). No `loadEnvFile` hack in code.
- **Docker**: removed dead `ENV PB_ENDPOINT` from `Dockerfile`; `EXPOSE 3001` (was `3005 8090`); compose public port is `${PORT:-3001}:3001`, creds default to the code fallback, redundant `FRONTEND_PORT`/`PROXY_PORT` passthrough dropped; `entrypoint.sh` simplified (`PB_DATA=/app/pb_data`, `PORT=$FRONTEND_PORT`, no `:-` fallbacks).
- **Frontend deps added** (were missing imports): `chart.js`, `qrcode`, `@hiseb/confetti`.
- **Build checks**: proxy + frontend `pnpm build` clean.
### 2026-08-06 — Dev PB data incident (pb-dev) — see RULES.md "Dev vs Prod PocketBase data"
- **Symptom**: `pnpm dev` proxy migrate failed; PB superuser auth returned `HTTP 500 "Something went wrong"`; could not log into the PB admin UI.
- **Root cause**: the permanent dev PB container **`pb-dev`** (publishes `:8090`, data in host `./pb_data`) had a **broken bind mount** — it was serving an empty throwaway store, so the `debug@famchamp.dev` superuser didn't exist. The real `data.db` was on the host but the container wasn't seeing it.
- **Fix**: recreated `pb-dev` with the mount correctly attached (`-v "$PWD/pb_data:/pb_data"`, `pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data`). Superuser auth then returned 200 on both `127.0.0.1:8090` and the Tailscale `SERVER_IP:8090`.
- **Watch-out**: a careless `docker run` with a **fresh volume** (my first attempt, aborted in time) would have wiped the permanent PB data. Restore command is in RULES.md. Two containers (`pb-dev` :8090 and the docker app's internal PB :8091) currently **share the same host `./pb_data`** — be careful with both.
+29 -12
View File
@@ -6,20 +6,37 @@
- `deviceToken` stored as SHA-256 hash; never log raw tokens - `deviceToken` stored as SHA-256 hash; never log raw tokens
## Config ## Config
- Use `.env` at root for runtime config values (ports, secrets). `.env.example` committed as template. - **SvelteKit must never import `config.ts`.** `config.ts` at root owns the three service **ports** for the Hono proxy (`FRONTEND_PORT`/`PROXY_PORT`/`PB_PORT` = `2080`/`3456`/`8090`).
- `config.ts` at root for dev/build-time shared config (e.g. `PROXY_PORT`), reads from env with fallback defaults. - SvelteKit env vars are declared in `frontend/src/env.ts` via `defineEnvVars`, read with `$app/env/public` / `$app/env/private`. Loopback URLs are code constants; the only real env vars are the PB creds and `SERVER_IP`.
- Never commit `.env` files (already in `.gitignore`). - Runtime env values live in `.env` at root (symlinked to `frontend/.env`; loaded by the proxy dev via `tsx --env-file=../.env`). `.env.example` is the committed template. Never commit `.env`.
- `.env` currently holds only: `PB_EMAIL`, `PB_PASSWORD`, `SERVER_IP`. Ports come from `config.ts`, not `.env`.
## Ports ## Ports
- Frontend: `2080` - Frontend: `2080`
- Proxy: `3456` - Proxy: `3456`
- Container external: `3001` (port `3000` is reserved) - Container external: `3001` (port `3000` is reserved) — chosen at deploy via compose `PORT`
- Must not use port `3000` for anything. - Must not use port `3000` for anything.
## Docker ## Docker
- Prod: `docker/Dockerfile` (multi-stage + nginx) - Prod: `docker/Dockerfile` (multi-stage + nginx, bundles internal PB)
- Dev: `docker/Dockerfile.dev` (PocketBase) - Dev PB: `docker/Dockerfile.dev` (standalone PocketBase)
- Nginx routes: `/api/*` → Hono (`:3456`), `/*` → SvelteKit (`:2080`) - Nginx routes: `/api/*` → Hono (`:3456`), `/pb/api/*` → PB (`:8090`), `/pb/` → 404 (admin UI kept internal), `/*` → SvelteKit (`:2080`)
- `docker-compose.yaml`: public port is `${PORT:-3001}:3001`; internal PB published loopback-only as `127.0.0.1:8091:8090`.
## ⚠️ Dev vs Prod PocketBase data (READ BEFORE TOUCHING CONTAINERS)
- **Permanent dev PB** = container **`pb-dev`**, publishes **`:8090`**, data in host **`./pb_data`** (root-owned). This is what `pnpm dev` talks to (via `SERVER_IP:8090`).
- **Docker app** (`famchamp-app-1`) has its **own internal PB**, published **`:8091`** (loopback), and it **also bind-mounts `./pb_data` → `/app/pb_data`**. So `pb-dev` and the docker app currently **share the same host data dir** — two PB processes on one store. Keep this in mind; the docker version is not the dev target.
- **NEVER `docker rm`/recreate `pb-dev` with a fresh volume** — that wipes the permanent dev PB. To restore it, recreate with its data intact:
```
docker rm -f pb-dev
docker run -d --name pb-dev \
-v "$PWD/pb_data:/pb_data" \
-p 8090:8090 \
pb-dev \
pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data
```
- Superuser creds: `debug@famchamp.dev` / `debug123` (fallback baked in code). If superuser auth returns HTTP 500, the container is almost certainly serving an **empty store** (broken/missing mount) — restore the mount, don't reseed a new store.
- `SERVER_IP`: same LAN = `192.168.1.225`, away = your Tailscale IP (e.g. `100.103.22.104`). Both hit the same `pb-dev:8090`. Update `.env` when your IP changes.
## Monorepo ## Monorepo
- SvelteKit in `frontend/`, Hono in `proxy/` - SvelteKit in `frontend/`, Hono in `proxy/`
@@ -27,8 +44,8 @@
- Decisions tracked in `MEMORY.md` - Decisions tracked in `MEMORY.md`
## Environment Variables ## Environment Variables
- `FRONTEND_PORT` — frontend server port - `SERVER_IP` — dev machine IP where PB + proxy run (browser `pocketbase.ts`, `pb-admin`, proxy all read it). Dev only; prod ignores it.
- `PROXY_PORT` — Hono proxy port - `PB_EMAIL` / `PB_PASSWORD` — PB superuser (fallback `debug@famchamp.dev`/`debug123`)
- `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` — PocketBase admin - `PROXY_URL` — public env in `env.ts` (default `http://127.0.0.1:3456`); SSR → proxy
- `STRIPE_SECRET_KEY` — Stripe - Compose/deploy: `PORT` (public port, default `3001`), `PB_DATA` (host data dir)
- `DONATION_MODAL_INTERVAL` — donation modal frequency - Not currently wired (future): `STRIPE_SECRET_KEY`, `DONATION_MODAL_INTERVAL`
Vendored
-9
View File
@@ -1,9 +0,0 @@
export declare const FRONTEND_PORT = "2080";
export declare const PROXY_PORT = "3456";
export declare const SERVER_IP = "192.168.1.225";
export declare const PB_PORT = "8090";
export declare const PB_EMAIL = "debug@famchamp.dev";
export declare const PB_PASSWORD = "debug123";
export declare const DEBUG_RECORD_ID = "0747qjl16m6o529";
export declare const PUBLIC_PB_URL = "http://192.168.1.225:8090";
//# sourceMappingURL=config.d.ts.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,SAAS,CAAC;AACpC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,SAAS,kBAAkB,CAAC;AACzC,eAAO,MAAM,OAAO,SAAS,CAAC;AAC9B,eAAO,MAAM,QAAQ,uBAAuB,CAAC;AAC7C,eAAO,MAAM,WAAW,aAAa,CAAC;AACtC,eAAO,MAAM,eAAe,oBAAoB,CAAC;AACjD,eAAO,MAAM,aAAa,8BAAmC,CAAC"}
-9
View File
@@ -1,9 +0,0 @@
export const FRONTEND_PORT = "2080";
export const PROXY_PORT = "3456";
export const SERVER_IP = "192.168.1.225";
export const PB_PORT = "8090";
export const PB_EMAIL = "debug@famchamp.dev";
export const PB_PASSWORD = "debug123";
export const DEBUG_RECORD_ID = "0747qjl16m6o529";
export const PUBLIC_PB_URL = `http://${SERVER_IP}:${PB_PORT}`;
//# sourceMappingURL=config.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC;AACjC,MAAM,CAAC,MAAM,SAAS,GAAG,eAAe,CAAC;AACzC,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC;AAC9B,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;AAC7C,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AACjD,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,SAAS,IAAI,OAAO,EAAE,CAAC"}
+2 -5
View File
@@ -1,8 +1,5 @@
// Single source of truth for the three service ports (dev/build-time only,
// used by the Hono proxy). Runtime URLs are set via env (see proxy/src/env.ts).
export const FRONTEND_PORT = "2080"; export const FRONTEND_PORT = "2080";
export const PROXY_PORT = "3456"; export const PROXY_PORT = "3456";
export const SERVER_IP = "192.168.1.225";
export const PB_PORT = "8090"; export const PB_PORT = "8090";
export const PB_EMAIL = "debug@famchamp.dev";
export const PB_PASSWORD = "debug123";
export const DEBUG_RECORD_ID = "0747qjl16m6o529";
export const PUBLIC_PB_URL = `http://${SERVER_IP}:${PB_PORT}`;
+5 -18
View File
@@ -3,25 +3,12 @@ services:
build: build:
context: . context: .
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
args:
# Client-side PocketBase URL. Browser reaches the in-container PB through
# nginx at /pb (default for docker). Local dev uses .env instead.
PUBLIC_PB_URL: ${PUBLIC_PB_URL:-/pb}
PB_ENDPOINT: http://127.0.0.1:8090
SERVER_IP: 127.0.0.1
PB_PORT: 8090
ports: ports:
- "${PORT:-3001}:3001" # nginx / app (public) - "3001:80" # nginx / app (public)
- "127.0.0.1:8091:8090" # PocketBase admin UI (loopback only — SSH tunnel) - "8091:8090" # PocketBase admin UI (loopback only — SSH tunnel)
volumes: volumes:
- "${PB_DATA:-./pb_data}:/app/pb_data" # PB DB persistence - "./pb_data:/app/pb_data" # PB DB persistence
environment: environment:
PB_ENDPOINT: http://127.0.0.1:8090 # server-side → in-container PB PB_EMAIL: debug@famchamp.dev
PB_EMAIL: ${PB_EMAIL:-} PB_PASSWORD: debug123
PB_PASSWORD: ${PB_PASSWORD:-}
DEBUG_RECORD_ID: ${DEBUG_RECORD_ID:-}
FRONTEND_PORT: "2080"
PROXY_PORT: "3456"
SERVER_IP: 127.0.0.1
PB_PORT: 8090
restart: unless-stopped restart: unless-stopped
+11 -8
View File
@@ -1,7 +1,7 @@
# ── Build stage ────────────────────────────────────────── # ── Build stage ──────────────────────────────────────────
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
ARG PUBLIC_PB_URL=/pb # Browser → PB goes through nginx at /pb (same origin) — fixed for docker.
ENV PUBLIC_PB_URL=$PUBLIC_PB_URL # ENV PUBLIC_PB_URL=/pb
RUN corepack enable RUN corepack enable
WORKDIR /app WORKDIR /app
COPY . . COPY . .
@@ -11,9 +11,9 @@ RUN pnpm --filter proxy build
# ── Runtime stage ──────────────────────────────────────── # ── Runtime stage ────────────────────────────────────────
FROM node:22-alpine FROM node:22-alpine
ARG PUBLIC_PB_URL=/pb # Browser → PB goes through nginx at /pb (same origin). PB_ENDPOINT (server →
ENV PUBLIC_PB_URL=$PUBLIC_PB_URL # PB) is a loopback constant in the app code, not an env var.
ENV PB_ENDPOINT=http://127.0.0.1:8090 # ENV PUBLIC_PB_URL=/pb
RUN corepack enable && apk add --no-cache nginx wget unzip ca-certificates RUN corepack enable && apk add --no-cache nginx wget unzip ca-certificates
WORKDIR /app WORKDIR /app
COPY --from=builder /app/frontend/build ./frontend/build COPY --from=builder /app/frontend/build ./frontend/build
@@ -27,7 +27,10 @@ RUN wget -qO /tmp/pb.zip https://github.com/pocketbase/pocketbase/releases/downl
&& unzip -o /tmp/pb.zip -d /usr/local/bin/ \ && unzip -o /tmp/pb.zip -d /usr/local/bin/ \
&& rm /tmp/pb.zip && rm /tmp/pb.zip
RUN chmod +x /entrypoint.sh /usr/local/bin/pocketbase RUN chmod +x /entrypoint.sh /usr/local/bin/pocketbase
ENV FRONTEND_PORT=2080 # ENV FRONTEND_PORT=2080
ENV PROXY_PORT=3456 # ENV PROXY_PORT=3456
EXPOSE 3001 8090 # Public entrypoint. The host port is chosen at deploy time via compose PORT;
# EXPOSE here is documentation only (nginx listens on 3001). PB (8090) stays
# internal — never published from the image.
EXPOSE 3000 8090
CMD ["/entrypoint.sh"] CMD ["/entrypoint.sh"]
+6 -3
View File
@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
set -e set -e
PB_DATA=${PB_DATA_DIR:-/app/pb_data} # PocketBase data lives on the mounted volume (compose maps ./pb_data → /app/pb_data).
PB_DATA=/app/pb_data
# Seed a platform superuser from env (idempotent — no-op if it already exists). # Seed a platform superuser from env (idempotent — no-op if it already exists).
# If PB_EMAIL/PB_PASSWORD aren't set, skip and rely on manual web setup. # If PB_EMAIL/PB_PASSWORD aren't set, skip and rely on manual web setup.
@@ -24,7 +25,9 @@ for i in $(seq 1 30); do
done done
# Start the app (frontend + proxy). The proxy auto-runs schema migration. # Start the app (frontend + proxy). The proxy auto-runs schema migration.
PORT=${FRONTEND_PORT:-2080} node /app/frontend/build/index.js & # FRONTEND_PORT/PROXY_PORT are set via ENV in the Dockerfile; adapter-node
PROXY_PORT=${PROXY_PORT:-3456} node /app/proxy/dist/index.js & # reads PORT, the proxy reads PROXY_PORT.
node /app/frontend/build/index.js &
node /app/proxy/dist/index.js &
nginx -g 'daemon off;' nginx -g 'daemon off;'
+2 -2
View File
@@ -1,9 +1,9 @@
server { server {
listen 3001; listen 80;
server_name _; server_name _;
location / { location / {
proxy_pass http://127.0.0.1:2080; proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
+4 -1
View File
@@ -29,6 +29,9 @@
"vite": "8.0.16" "vite": "8.0.16"
}, },
"dependencies": { "dependencies": {
"pocketbase": "^0.27.0" "@hiseb/confetti": "^2.0.2",
"chart.js": "^4.4.0",
"pocketbase": "^0.27.0",
"qrcode": "^1.5.4"
} }
} }
+15 -2
View File
@@ -1,6 +1,19 @@
import { defineEnvVars } from '@sveltejs/kit/hooks'; import { defineEnvVars } from '@sveltejs/kit/hooks';
// Default when the env var isn't set, so a missing value never crashes startup.
const withDefault = (value: string) => ({
'~standard': {
version: 1,
vendor: 'famchamp',
validate: (v: unknown) => ({ value: typeof v === 'string' && v ? v : value })
}
} as const);
export const variables = defineEnvVars({ export const variables = defineEnvVars({
DEBUG_RECORD_ID: {}, // SSR → Hono proxy (loopback, proxy runs on the same host as SSR).
PUBLIC_PB_URL: { public: true } PROXY_URL: { public: true, schema: withDefault('http://127.0.0.1:3456') },
SERVER_IP: { public: true, schema: withDefault('192.168.1.225') },
// PB superuser creds (server-only).
PB_EMAIL: { public: false, schema: withDefault('debug@famchamp.dev') },
PB_PASSWORD: { public: false, schema: withDefault('debug123') }
}); });
+3 -7
View File
@@ -1,10 +1,6 @@
const BASE_URL = // Client-only. All /api calls go same-origin (vite proxy in dev, nginx in
typeof window === 'undefined' // prod). Server-side (SSR) calls use PROXY_URL from $app/env/public instead.
? typeof process !== 'undefined' const BASE_URL = '';
? process.env.PROXY_URL ||
`http://${process.env.SERVER_IP || '192.168.1.225'}:${process.env.PROXY_PORT || '3456'}`
: ''
: '';
async function memberFetch<T = unknown>( async function memberFetch<T = unknown>(
method: string, method: string,
+3 -3
View File
@@ -1,7 +1,7 @@
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import { PUBLIC_PB_URL } from '$app/env/public'; import { SERVER_IP } from '$app/env/public';
const PB_ENDPOINT = import.meta.env.PROD ? '/pb' : `http://${SERVER_IP}:8090`;
export const pb = new PocketBase(PUBLIC_PB_URL); export const pb = new PocketBase(PB_ENDPOINT);
pb.autoCancellation(false); pb.autoCancellation(false);
export function initPbFromCookie() { export function initPbFromCookie() {
+4 -6
View File
@@ -1,8 +1,6 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { PROXY_URL } from '$lib/server/env'; import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL;
export function getSession(event: RequestEvent) { export function getSession(event: RequestEvent) {
return event.locals.session; return event.locals.session;
@@ -17,7 +15,7 @@ export function requireAuth(event: RequestEvent) {
} }
export async function signup(email: string, password: string, famName: string, parentName?: string) { export async function signup(email: string, password: string, famName: string, parentName?: string) {
const res = await fetch(`${HONO_URL}/api/admin/signup`, { const res = await fetch(`${PROXY_URL}/api/admin/signup`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, famName, parentName }), body: JSON.stringify({ email, password, famName, parentName }),
@@ -29,7 +27,7 @@ export async function signup(email: string, password: string, famName: string, p
export async function login(email: string, password: string) { export async function login(email: string, password: string) {
console.log(email); console.log(email);
const res = await fetch(`${HONO_URL}/api/admin/login`, { const res = await fetch(`${PROXY_URL}/api/admin/login`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
@@ -40,7 +38,7 @@ export async function login(email: string, password: string) {
} }
export async function joinMember(inviteCode: string, name: string, deviceToken: string) { export async function joinMember(inviteCode: string, name: string, deviceToken: string) {
const res = await fetch(`${HONO_URL}/api/members/join`, { const res = await fetch(`${PROXY_URL}/api/members/join`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inviteCode, name, deviceToken }), body: JSON.stringify({ inviteCode, name, deviceToken }),
+2 -4
View File
@@ -1,7 +1,5 @@
import { PROXY_URL } from '$lib/server/env';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL;
function sessionHeaders(event: RequestEvent): Record<string, string> { function sessionHeaders(event: RequestEvent): Record<string, string> {
const s = event.locals.session; const s = event.locals.session;
@@ -19,7 +17,7 @@ async function request(
body?: unknown, body?: unknown,
headers?: Record<string, string> headers?: Record<string, string>
) { ) {
const res = await fetch(`${HONO_URL}${path}`, { const res = await fetch(`${PROXY_URL}${path}`, {
method, method,
headers: headers || { 'Content-Type': 'application/json' }, headers: headers || { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined body: body ? JSON.stringify(body) : undefined
+3 -1
View File
@@ -1,4 +1,6 @@
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from './env'; import { PB_EMAIL, PB_PASSWORD } from '$app/env/private';
import { SERVER_IP } from '$app/env/public';
export const PB_ENDPOINT = import.meta.env.PROD ? '/pb' : `http://${SERVER_IP}:8090`;
let token: string | null = null; let token: string | null = null;
let tokenExpiry = 0; let tokenExpiry = 0;
+5 -4
View File
@@ -67,7 +67,7 @@ class FamStore {
private initPromise: Promise<void> | null = null; private initPromise: Promise<void> | null = null;
async init(famId: string) { async init(famId: string, initialFam?: Fam) {
if (this.initialized && this.famId === famId) return; if (this.initialized && this.famId === famId) return;
// Wait for any in-flight init to finish first // Wait for any in-flight init to finish first
@@ -82,8 +82,11 @@ class FamStore {
this.initPromise = (async () => { this.initPromise = (async () => {
try { try {
// fams is superadmin-only (non-realtime, one-way writes). It is always
// fetched server-side by the layout load and passed in — never via the
// unauthenticated client PB SDK.
this.fam = initialFam || this.fam || ({} as Fam);
const [ const [
famRes,
membersRes, membersRes,
templatesRes, templatesRes,
assignedRes, assignedRes,
@@ -93,7 +96,6 @@ class FamStore {
rewardsRes, rewardsRes,
seasonsRes seasonsRes
] = await Promise.all([ ] = await Promise.all([
pb.collection('fams').getOne(famId) as Promise<Fam>,
pb.collection('members').getFullList({ filter: `famId = '${famId}'` }) as Promise< pb.collection('members').getFullList({ filter: `famId = '${famId}'` }) as Promise<
Member[] Member[]
>, >,
@@ -119,7 +121,6 @@ class FamStore {
Season[] Season[]
> >
]); ]);
this.fam = famRes;
this.members = membersRes; this.members = membersRes;
this.templates = templatesRes; this.templates = templatesRes;
this.assigned = assignedRes; this.assigned = assignedRes;
+14 -2
View File
@@ -1,4 +1,5 @@
import { PROXY_URL } from '$lib/server/env'; import { PROXY_URL } from '$app/env/public';
import { pbAdmin } from '$lib/server/pb-admin';
const HONO_URL = PROXY_URL; const HONO_URL = PROXY_URL;
@@ -77,5 +78,16 @@ export async function load(event) {
} }
} }
return { session, isParent, role: session?.role || 'child', famId, chat, deviceToken }; return {
session,
isParent,
role: session?.role || 'child',
famId,
chat,
deviceToken,
// fams is superadmin-only (non-realtime). Fetched server-side for both roles.
fam: famId
? await pbAdmin.getOne('fams', famId).catch(() => null)
: null
};
} }
+1 -1
View File
@@ -46,7 +46,7 @@
onMount(() => { onMount(() => {
initPbFromCookie(); initPbFromCookie();
if (page.data.famId) famStore.init(page.data.famId); if (page.data.famId) famStore.init(page.data.famId, page.data.fam);
const chat = page.data.chat; const chat = page.data.chat;
if (chat?.famId && chat?.actor) { if (chat?.famId && chat?.actor) {
chatStore.init({ chatStore.init({
@@ -1,7 +1,6 @@
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono'; import { hono } from '$lib/server/hono';
import { memberApi } from '$lib/client/api'; import { PROXY_URL } from '$app/env/public';
import { PROXY_URL } from '$lib/server/env';
const HONO_URL = PROXY_URL; const HONO_URL = PROXY_URL;
@@ -101,7 +100,11 @@ export async function load(event) {
let chores: any = {}; let chores: any = {};
try { try {
chores = await memberApi.myChores(deviceToken, data.famId); const choresRes = await fetch(`${HONO_URL}/api/members/my-chores`, {
method: 'POST',
headers: { 'x-device-token': deviceToken, 'x-device-famid': data.famId }
});
chores = await choresRes.json();
} catch {} } catch {}
return { return {
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { hono } from '$lib/server/hono'; import { hono } from '$lib/server/hono';
import { PROXY_URL } from '$lib/server/env'; import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL; const HONO_URL = PROXY_URL;
@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { onMount, onDestroy } from 'svelte';
import { pb, initPbFromCookie } from '$lib/pocketbase';
import { famStore } from '$lib/stores/fam.svelte'; import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components'; import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import { COMMON_TIMEZONES } from '../../../../../../timezone.ts'; import { COMMON_TIMEZONES } from '../../../../../../timezone.ts';
@@ -11,8 +9,8 @@
let { data } = $props(); let { data } = $props();
// fam is sensitive (inviteCode, stripeCustomerId, featureFlags) — never in the // fam is sensitive (inviteCode, stripeCustomerId, featureFlags) — never in the
// public famStore stream. This admin-only page subscribes to `fams` with the // public famStore stream. It is superadmin-only, fetched server-side by the
// authenticated PB instance (pb_token cookie), page-local only. // layout load. Writes go through form actions; no live fam subscription.
let fam = $state(data.fam); let fam = $state(data.fam);
let famSlug = $state(page.params.fam); let famSlug = $state(page.params.fam);
@@ -65,32 +63,6 @@
let members = $state(famStore.initialized ? famStore.members : data.members || []); let members = $state(famStore.initialized ? famStore.members : data.members || []);
let deletingSeason = $state<any>(null); let deletingSeason = $state<any>(null);
function syncFamFromRecord(record: any) {
fam = record;
payday = Number(record.payday ?? 1);
paydayTime = record.paydayTime || '18:00';
timezone = record.timezone || 'auto';
}
onMount(async () => {
initPbFromCookie();
if (!fam?.id) return;
try {
await pb.collection('fams').subscribe(fam.id, ({ action, record }) => {
if (action === 'update') syncFamFromRecord(record);
});
} catch (e) {
console.error('fams subscribe failed:', e);
}
});
onDestroy(() => {
if (fam?.id)
pb.collection('fams')
.unsubscribe(fam.id)
.catch(() => {});
});
function copy(url: string) { function copy(url: string) {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
copied = true; copied = true;
+1 -1
View File
@@ -1,6 +1,6 @@
import { pbAdmin } from '$lib/server/pb-admin'; import { pbAdmin } from '$lib/server/pb-admin';
import { redirect, fail } from '@sveltejs/kit'; import { redirect, fail } from '@sveltejs/kit';
import { PB_EMAIL, PB_PASSWORD } from '$lib/server/env'; import { PB_EMAIL, PB_PASSWORD } from '$app/env/private';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageServerLoad = async ({ cookies }) => {
@@ -1,6 +0,0 @@
import { DEBUG_RECORD_ID } from '$app/env/private';
export async function load() {
return {
recordId: DEBUG_RECORD_ID
};
}
-79
View File
@@ -1,79 +0,0 @@
<script lang="ts">
import { pb } from "$lib/pocketbase";
let { data } = $props();
let records = $state(null);
$effect(async () => {
records = await pb.collection('debug').getOne(data.recordId);
return await pb.collection('debug').subscribe(data.recordId, ({ action, record }) => {
records = record
// if (action === 'create') {
// todos = [...todos, record];
// }
// if (action === 'update') {
// todos = todos.map((t) =>
// t.id === record.id ? record : t
// );
// }
// if (action === 'delete') {
// todos = todos.filter((t) =>
// t.id !== record.id
// );
// }
});
});
async function incrementHono() {
try {
const response = await fetch('/api/increment-hono', { method: 'POST' });
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
</script>
<h1>Debug Dashboard</h1>
<div class="counters">
<div class="card">
<h2>Hono</h2>
{#if records}
<p class="value">{records.hono_count}</p>
{/if}
<button onclick={incrementHono}>+1 Hono</button>
</div>
</div>
<style>
h1 {
text-align: center;
margin: 2rem 0;
}
.counters {
display: flex;
gap: 2rem;
justify-content: center;
}
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 2rem;
text-align: center;
min-width: 200px;
}
.value {
font-size: 3rem;
font-weight: bold;
margin: 1rem 0;
}
button {
padding: 0.5rem 1.5rem;
font-size: 1rem;
cursor: pointer;
}
</style>
@@ -1,6 +1,6 @@
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { setDeviceTokenCookie } from '$lib/server/auth'; import { setDeviceTokenCookie } from '$lib/server/auth';
import { PROXY_URL } from '$lib/server/env'; import { PROXY_URL } from '$app/env/public';
const HONO_URL = PROXY_URL; const HONO_URL = PROXY_URL;
+5 -4
View File
@@ -2,7 +2,6 @@ import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { SERVER_IP, FRONTEND_PORT, PROXY_PORT } from '../config.ts';
export default defineConfig(() => { export default defineConfig(() => {
return { return {
@@ -25,11 +24,13 @@ export default defineConfig(() => {
fs: { fs: {
allow: ['.', './node_modules', '../node_modules'] allow: ['.', './node_modules', '../node_modules']
}, },
allowedHosts: [SERVER_IP], // Dev only: allow access via any host/LAN IP without hardcoding it.
port: FRONTEND_PORT, allowedHosts: true,
port: 2080,
proxy: { proxy: {
'/api': { '/api': {
target: `http://${SERVER_IP}:${PROXY_PORT}`, // The vite dev server and Hono proxy run on the same host.
target: `http://127.0.0.1:3456`,
changeOrigin: true changeOrigin: true
} }
} }
+240
View File
@@ -10,9 +10,18 @@ importers:
frontend: frontend:
dependencies: dependencies:
'@hiseb/confetti':
specifier: ^2.0.2
version: 2.2.0
chart.js:
specifier: ^4.4.0
version: 4.5.1
pocketbase: pocketbase:
specifier: ^0.27.0 specifier: ^0.27.0
version: 0.27.0 version: 0.27.0
qrcode:
specifier: ^1.5.4
version: 1.5.4
devDependencies: devDependencies:
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: next specifier: next
@@ -252,6 +261,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@hiseb/confetti@2.2.0':
resolution: {integrity: sha512-iCcTe2AS2Mnj7f2BGPnOetjnX+Qs1jgnKU0GSYyQHFB42psio0EpgxmPXOXf2wcCGBH/W+1G2Ecl4hcbsin1Kg==}
'@hono/node-server@1.19.14': '@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
engines: {node: '>=18.14.1'} engines: {node: '>=18.14.1'}
@@ -274,6 +286,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@napi-rs/wasm-runtime@1.1.5': '@napi-rs/wasm-runtime@1.1.5':
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
peerDependencies: peerDependencies:
@@ -633,6 +648,14 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
aria-query@5.3.1: aria-query@5.3.1:
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -641,18 +664,40 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
cookie@1.1.1: cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
deepmerge@4.3.1: deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -664,6 +709,12 @@ packages:
devalue@5.8.1: devalue@5.8.1:
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
enhanced-resolve@5.21.6: enhanced-resolve@5.21.6:
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -693,11 +744,19 @@ packages:
picomatch: picomatch:
optional: true optional: true
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -705,6 +764,10 @@ packages:
resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-reference@3.0.3: is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
@@ -789,6 +852,10 @@ packages:
locate-character@3.0.0: locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -809,6 +876,22 @@ packages:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -816,6 +899,10 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
pocketbase@0.27.0: pocketbase@0.27.0:
resolution: {integrity: sha512-K5N6d93UP/BNMbMnlZ6BUfy9VPCIvLyqhJFOsNI8OsZwzvKWEAfyD36boi5K4ECIOl5HMlo0TzuaeGdKpMwizQ==} resolution: {integrity: sha512-K5N6d93UP/BNMbMnlZ6BUfy9VPCIvLyqhJFOsNI8OsZwzvKWEAfyD36boi5K4ECIOl5HMlo0TzuaeGdKpMwizQ==}
@@ -890,10 +977,22 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
readdirp@4.1.2: readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
rolldown@1.0.3: rolldown@1.0.3:
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -908,6 +1007,9 @@ packages:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'} engines: {node: '>=6'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
sirv@3.0.2: sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -916,6 +1018,14 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
svelte-check@4.6.0: svelte-check@4.6.0:
resolution: {integrity: sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==} resolution: {integrity: sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
@@ -1015,6 +1125,24 @@ packages:
vite: vite:
optional: true optional: true
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
zimmerframe@1.1.4: zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
@@ -1130,6 +1258,8 @@ snapshots:
'@esbuild/win32-x64@0.28.1': '@esbuild/win32-x64@0.28.1':
optional: true optional: true
'@hiseb/confetti@2.2.0': {}
'@hono/node-server@1.19.14(hono@4.12.27)': '@hono/node-server@1.19.14(hono@4.12.27)':
dependencies: dependencies:
hono: 4.12.27 hono: 4.12.27
@@ -1153,6 +1283,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@kurkle/color@0.3.4': {}
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@emnapi/core': 1.10.0
@@ -1395,24 +1527,54 @@ snapshots:
acorn@8.17.0: {} acorn@8.17.0: {}
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
aria-query@5.3.1: {} aria-query@5.3.1: {}
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
camelcase@5.3.1: {}
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
cookie@1.1.1: {} cookie@1.1.1: {}
decamelize@1.2.0: {}
deepmerge@4.3.1: {} deepmerge@4.3.1: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
devalue@5.8.1: {} devalue@5.8.1: {}
dijkstrajs@1.0.3: {}
emoji-regex@8.0.0: {}
enhanced-resolve@5.21.6: enhanced-resolve@5.21.6:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -1457,13 +1619,22 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
get-caller-file@2.0.5: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
hono@4.12.27: {} hono@4.12.27: {}
is-fullwidth-code-point@3.0.0: {}
is-reference@3.0.3: is-reference@3.0.3:
dependencies: dependencies:
'@types/estree': 1.0.9 '@types/estree': 1.0.9
@@ -1521,6 +1692,10 @@ snapshots:
locate-character@3.0.0: {} locate-character@3.0.0: {}
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -1533,10 +1708,24 @@ snapshots:
obug@2.1.3: {} obug@2.1.3: {}
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-try@2.2.0: {}
path-exists@4.0.0: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.4: {} picomatch@4.0.4: {}
pngjs@5.0.0: {}
pocketbase@0.27.0: {} pocketbase@0.27.0: {}
postcss@8.5.15: postcss@8.5.15:
@@ -1558,8 +1747,18 @@ snapshots:
prettier@3.8.4: {} prettier@3.8.4: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
readdirp@4.1.2: {} readdirp@4.1.2: {}
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
rolldown@1.0.3: rolldown@1.0.3:
dependencies: dependencies:
'@oxc-project/types': 0.133.0 '@oxc-project/types': 0.133.0
@@ -1606,6 +1805,8 @@ snapshots:
dependencies: dependencies:
mri: 1.2.0 mri: 1.2.0
set-blocking@2.0.0: {}
sirv@3.0.2: sirv@3.0.2:
dependencies: dependencies:
'@polka/url': 1.0.0-next.29 '@polka/url': 1.0.0-next.29
@@ -1614,6 +1815,16 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
svelte-check@4.6.0(picomatch@4.0.4)(svelte@5.56.3)(typescript@6.0.3): svelte-check@4.6.0(picomatch@4.0.4)(svelte@5.56.3)(typescript@6.0.3):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
@@ -1692,4 +1903,33 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4) vite: 8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
which-module@2.0.1: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
y18n@4.0.3: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
zimmerframe@1.1.4: {} zimmerframe@1.1.4: {}
+2 -2
View File
@@ -3,10 +3,10 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch --env-file-if-exists=../.env src/index.ts",
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js", "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
"start": "node dist/index.js", "start": "node dist/index.js",
"seed": "tsx scripts/seed.ts" "seed": "tsx --env-file-if-exists=../.env scripts/seed.ts"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.13.0", "@hono/node-server": "^1.13.0",
+3 -5
View File
@@ -1,6 +1,4 @@
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts"; import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "../src/env.ts";
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
interface FieldDef { interface FieldDef {
name: string; name: string;
@@ -121,8 +119,8 @@ async function main() {
ids.fams = await createCollection(token, { ids.fams = await createCollection(token, {
name: "fams", name: "fams",
type: "base", type: "base",
listRule: "", listRule: null,
viewRule: "", viewRule: null,
createRule: null, createRule: null,
updateRule: null, updateRule: null,
deleteRule: null, deleteRule: null,
+14
View File
@@ -0,0 +1,14 @@
// Runtime env for the proxy. Same dev/prod split as the frontend: dev connects
// to the dev machine's PocketBase at SERVER_IP, prod connects to the
// container-internal loopback PB. Ports come from config.ts (single source).
//
// Env loading: dev uses `tsx --env-file=../.env` (see package.json) like vite
// does for the frontend; prod (docker) injects env via compose and has no .env,
// so SERVER_IP is unset → loopback below.
const SERVER_IP = process.env.SERVER_IP;
export const PB_ENDPOINT = SERVER_IP
? `http://${SERVER_IP}:8090`
: `http://127.0.0.1:8090`;
export const PB_EMAIL = process.env.PB_EMAIL || "debug@famchamp.dev";
export const PB_PASSWORD = process.env.PB_PASSWORD || "debug123";
+3 -3
View File
@@ -3,6 +3,7 @@ import { serve } from "@hono/node-server";
import { Hono } from "hono"; import { Hono } from "hono";
import { pb } from "./pb.ts"; import { pb } from "./pb.ts";
import { migrate } from "./migrate.ts"; import { migrate } from "./migrate.ts";
import { PROXY_PORT } from "../../config.ts";
import { import {
weekStart as tzWeekStart, weekStart as tzWeekStart,
addDaysStr, addDaysStr,
@@ -2453,11 +2454,10 @@ app.get("/api/chat/me", async (c) => {
// ── Start server ───────────────────────────────────────── // ── Start server ─────────────────────────────────────────
const port = parseInt(process.env.PROXY_PORT || "3456", 10); const port = parseInt(process.env.PROXY_PORT || PROXY_PORT, 10);
const SERVER_IP = "192.168.1.225";
serve({ fetch: app.fetch, port }, async (info) => { serve({ fetch: app.fetch, port }, async (info) => {
console.log(`Hono proxy listening on ${SERVER_IP}:${info.port}`); console.log(`Hono proxy listening on 0.0.0.0:${info.port}`);
try { try {
await migrate(); await migrate();
} catch (e) { } catch (e) {
+16
View File
@@ -128,6 +128,8 @@ async function ensureSchema(): Promise<void> {
jsonField("seasons"), jsonField("seasons"),
]), ]),
))!; ))!;
// fams is sensitive → superadmin-only (proxy/server reads). Not public.
await updateCollection(famsId, { viewRule: null, listRule: null });
await createCollection( await createCollection(
colDef("bonus_templates", [ colDef("bonus_templates", [
rel("famId", famsId, true), rel("famId", famsId, true),
@@ -280,6 +282,20 @@ export async function migrate(): Promise<void> {
await ensureSchema(); await ensureSchema();
// ── 0. Lock fams to superadmin-only (sensitive; read via proxy/server) ──
{
const famsCol = await getCollection("fams");
if (famsCol) {
const rules = { viewRule: null, listRule: null };
if (famsCol.viewRule !== null || famsCol.listRule !== null) {
console.log("[migrate] Locking fams collection to superadmin-only...");
await updateCollection(famsCol.id, rules);
} else {
console.log(" ↳ fams already superadmin-only");
}
}
}
// ── 1. Create bonus_configs if missing ── // ── 1. Create bonus_configs if missing ──
const existing = await getCollection("bonus_configs"); const existing = await getCollection("bonus_configs");
if (!existing) { if (!existing) {