Phase 3 of the sub-property hierarchy feature.
- New/edit forms grow a "Parent property" select. Edit-side options
exclude the current property and its descendants so the picker
itself can't create a cycle; service-layer assertNoCycle is the
belt-and-braces guard if a malicious form bypasses the dropdown.
- New form accepts ?parent=<id> as a preselect so "Add sub-property"
links from the parent's tab land in a pre-wired form.
- Property detail layout: breadcrumb (Parent › Child) when parent
is set, plus a new "Sub-properties (N)" tab.
- Dedicated Sub-properties tab lists direct children with a
+ New sub-property button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2. Each service grows a `*ForProperties` (plural) variant that
takes a resolved property id array; the existing single-property
functions now delegate to them with `[propertyId]`. The route layer
will compute the descendant set via getDescendantIds when the user
toggles "include sub-properties" — this commit only adds the readers
behind the scenes.
Also extends the checklist_scope enum with 'property' so checklists
can attach directly to a property and the rollup query has a
well-typed scope to filter on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the parent/child rollup feature. Self-FK on properties
with ON DELETE RESTRICT, plus a CHECK that blocks self-reference at
the DB level. Service-layer helpers (getDescendantIds,
getAncestorIds, assertNoCycle) walk the tree via recursive CTEs and
guard against cycles and cross-company parents. softDeleteProperty
now refuses to delete a property with live children.
No UI yet — readers and roll-up routes land in Phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pnpm install --prod` skips devDependencies, so husky is not on
PATH when the prepare lifecycle script runs on the deploy host. Append
`|| true` so the script succeeds in both dev (hooks installed) and
prod (no-op).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/lib/server/env.ts validates DATABASE_URL, SESSION_SECRET,
STORAGE_SIGNING_SECRET, and PUBLIC_BASE_URL with Zod and throws
when missing. SvelteKit's analyse step imports it during build, so
CI needs values that satisfy the schema shape — they never connect
to anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the buildfor_life_budget workflow pair: Gitea runs both
deploy and validate, GitHub mirrors validate only. Differences from
the sibling: pnpm + fnm instead of npm, Node pinned via .node-version,
and the repo is cloned over public HTTPS so no separate deploy key
is needed for git.b4l.co.th. Document required Gitea secrets in
DEPLOYMENT.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin Node 24 via .node-version/.nvmrc and pnpm 9.15.0 via
package.json#packageManager. Regenerate lockfile as pnpm-lock.yaml.
Rewrite README setup + scripts table around pnpm, and add a
production deployment guide covering systemd, nginx, upgrades,
rollback, and backups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- service monthlySeriesForProperty now accepts months: number | 'all';
'all' derives the span from min(incurred_at) for the matching kinds,
capped at 120 months so the SQL stays bounded
- page load reads ?range= query param (default 12; invalid values fall
back silently)
- chart header has a segmented control linking to ?range=6|12|24|all
with active state highlighting; title updates to match
- lib/server/csv-parse.ts: RFC 4180 parser (quoted fields, embedded commas
and quotes, CRLF/LF, BOM), no extra dep
- services/expenses.ts: importExpenses() is transactional + all-or-nothing —
if any row fails Zod-style validation, nothing is inserted
- /properties/[id]/expenses/import: upload page with per-line error list,
sample CSV download at /template.csv, 5 MB cap
- 'Import CSV' button wired on the expenses tab
Without min/max, native <input type="date"> lets you type arbitrarily-many
year digits (YYYYYY-MM-DD). Add min="2000-01-01" max="2099-12-31" to every
date input (and bounded range on the single datetime-local) across expenses,
projects, tasks, assets, decisions, and maintenance.
- expense_kind enum (utilities + maintenance/repair/cleaning/insurance/tax/rent/other)
- property_expenses table with optional link to a property_accounts row
(preserves history via ON DELETE SET NULL)
- services/expenses.ts: CRUD + 12-month monthly series aggregation +
year-to-date summary by kind
- /properties/[id]/expenses tab: inline SVG line chart for electricity +
water last 12 months (no chart library), summary card, add/edit/delete
inline with account linking when kind matches
If a previous run stored the company name with literal quotes
('B4L'), the current run's name-based select missed the row and
the insert collided on the unique slug. Looking up by slug is the
natural idempotency key here: slug is derived deterministically
from name, so a new run and the corrupted row produce the same
slug and therefore resolve to the same company row. If the stored
name differs from the new one, heal it with an UPDATE and log the
rename.
Also tightens the membership lookup to (user_id, company_id)
instead of first-match on user_id so re-running on a user with
multiple companies does the right thing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When npm run create-user is invoked from Windows Git Bash with
single-quoted values (--password 'foo' --name 'Berwn'), the quotes
survive into process.argv and end up stored in the DB. Login fails
silently because the stored hash is for 'foo' but the user types foo.
create-user and diag-user now strip a single set of matching surrounding
quotes from every --flag value. Real values that need literal leading
and trailing quotes can be escaped.
diag-user prints the full user row (email, normalized email, hash
prefix, isActive, memberships, session count) and optionally verifies a
password. Useful whenever a login mystery shows up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without dotenv/config the SvelteKit dev server SSR sees only the
ambient shell env, so the Zod validator rejected all four required
variables with Required. drizzle.config.ts already does this; match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stack matches sibling buildfor_life_* apps: SvelteKit 5 with adapter-node,
Svelte 5 runes, TypeScript, Tailwind v4 with @theme inline tokens,
PostgreSQL via Drizzle ORM, Argon2id sessions via @node-rs/argon2 and
@oslojs/crypto, EasyMDE ready for wiki/decision markdown, Sharp for
thumbnails.
Included in this commit:
- Config: package.json, svelte.config.js, vite.config.ts, tsconfig.json,
drizzle.config.ts, .gitignore, .env.example, .gitattributes, .npmrc
- Tenancy schema: companies, users, company_users, sessions
(10 enums pre-declared for the full domain so downstream migrations
don't re-diff them; decision_scope widened to include asset +
work_package per product decision)
- Auth: password hashing + SHA-256-hashed session cookies,
session lifetime 30d with sliding renewal at T-15d,
login + logout + session refresh in hooks
- Storage: StorageAdapter interface + LocalDiskStorage with HMAC-signed
URLs served by /api/files, S3 drop-in with zero schema change
- UI shell: dark-mode bootstrap in app.html identical to siblings,
sidebar (w-64, h-14 header, amber attention band pattern from repair),
topbar with breadcrumbs, theme toggle with cross-tab sync via
storage event, blue-600 primary, responsive drawer
- Routes: (app) authed group with auto-redirect to /login,
(auth) login group, dashboard placeholder, error page, signed-file API
- Scripts: create-user script for bootstrapping first admin user
- Drizzle: initial migration generated (0000_init.sql)
- Shared agents and skills committed under .claude/; per-user
permissions gitignored
Typecheck: 0 errors / 0 warnings across 555 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>