Commit Graph

23 Commits

Author SHA1 Message Date
grabowski b4108c5a36 feat(maintenance): schema for reminders dedup log
Phase 1 of the maintenance-reminders feature.

- notification_kind: + maintenance_due_soon, maintenance_overdue.
  These are distinct from maintenance_event_recorded (service
  performed) so the bell list can group/filter reminder vs service
  cleanly.
- New maintenance_reminder_kind enum: due_soon | overdue.
- New maintenance_reminders_sent table with UNIQUE(schedule_id,
  kind, due_at). The cron uses INSERT … ON CONFLICT DO NOTHING on
  this composite to make the fire-once-per-window guarantee atomic
  even under concurrent runs. Once the schedule's next_due_at
  advances after a service event, the tuple is fresh and a new
  reminder fires.

No service code yet — Phase 2 wires the readers and orchestrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:11:34 +07:00
grabowski 435bcb981f docs(roadmap): rewrite README roadmap to reflect actual state
Deploy to LXC / deploy (push) Successful in 16s
Validate / validate (push) Successful in 31s
The original table marked Phase 1 as "next" but Phases 1-5a have
all shipped. Replaces the single table with three sections:
Shipped (now includes RBAC, rooms/floors/accounts/expenses, the
sub-property hierarchy, and the fnm/pnpm tooling switch — none of
which were on the original roadmap), 5b-pending (reports,
cross-app APIs to budget/repair), and a Phase 6 quality-of-life
backlog covering maintenance-reminder cron, OIDC handlers,
self-service password reset, permission inheritance, bulk CSV
property import, audit log viewer, and project↔property linking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:51:14 +07:00
grabowski 011e7a2165 chore(graph): refresh graphify after sub-property feature
Deploy to LXC / deploy (push) Successful in 15s
Validate / validate (push) Successful in 30s
Re-extracted 42 changed code files via AST and 3 changed docs
(README, DEPLOYMENT, drizzle/README) via one semantic subagent.
Merged into the existing graph: 453→555 nodes, 486→633 edges,
137 communities.

Top god nodes now reflect the new shape: load() at the center of
every page-server route, buildfor_life_ops as the doc-side anchor,
and Drizzle ORM + Zod as the bridge between expenses and the rest
of the service layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:47:00 +07:00
grabowski c3aaf82642 feat(properties): warn when parenting exceeds depth cap of 5
Deploy to LXC / deploy (push) Successful in 16s
Validate / validate (push) Successful in 29s
Phase 5 polish. Soft cap — properties still save, but a console.warn
fires so journalctl picks up a clear signal when a tree grows
pathologically deep. Triggered on create + on parent reassignment
via updateProperty. Justification for the warning: getDescendantIds
runs an unbounded recursive CTE, and deep hierarchies are also
painful to navigate in the existing list view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:36:30 +07:00
grabowski 90207135c8 feat(properties): list view renders parent/child as a depth-first tree
Deploy to LXC / deploy (push) Successful in 16s
Validate / validate (push) Successful in 30s
The flat list ordered by updatedAt scattered apartments away from
their building. Server-side flatten now does a depth-first walk —
parents render immediately above their children, alphabetical at
each level — and tags each row with its depth. The UI indents the
name column by 1.5rem per level and prefixes children with "└" for
a visible parent/child line.

Orphan rows (parent_id pointing outside the live company set) fall
back to root depth so nothing is silently dropped, even though the
restrict-on-delete FK should prevent that case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:02:46 +07:00
grabowski c61be187e6 feat(properties): roll-up toggle on expenses/assets, new Maintenance + Todos tabs
Deploy to LXC / deploy (push) Successful in 15s
Validate / validate (push) Successful in 31s
Phase 4 — wires the descendant readers from Phase 2 into the UI.

- Expenses tab: ?descendants=1 query param toggles between
  "this property" and "include sub-properties". Affects the chart
  series, the 12-month summary, the recent-expense list, and the
  totals-by-kind chips.
- Assets tab: same toggle, expanded list across the property tree.
- New Maintenance tab on every property: schedules + recent events
  for assets at this property (or descendants), with overdue/active
  status badges. Scheduling itself stays at the asset level.
- New Todos tab: lists checklist instances scoped to this property
  (now possible thanks to the 'property' value added to
  checklist_scope in Phase 2). Open vs completed split.

Toggle only renders when childCount > 0 so leaf properties don't see
chrome they can't use. URL bookmarks without ?descendants stay on
the strict per-property view, preserving existing behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:59:17 +07:00
grabowski 3106286629 feat(properties): parent picker, breadcrumb, sub-properties tab
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>
2026-04-27 12:54:36 +07:00
grabowski 3b34458a99 feat(properties): tree-aware readers for expenses, maintenance, checklists
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>
2026-04-27 12:51:59 +07:00
grabowski 8117253841 feat(properties): add parent_id for sub-property hierarchy
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>
2026-04-27 12:49:04 +07:00
grabowski 76248c3d7f fix(prepare): tolerate missing husky in --prod installs
Deploy to LXC / deploy (push) Successful in 15s
Validate / validate (push) Successful in 27s
`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>
2026-04-27 11:35:03 +07:00
grabowski 3274afb677 ci(validate): stub required env vars so build-time analyse passes
Validate / validate (push) Successful in 29s
Deploy to LXC / deploy (push) Failing after 2m27s
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>
2026-04-27 11:11:47 +07:00
grabowski c43fdc4716 ci(deploy): add gitea LXC deploy + validate workflows
Deploy to LXC / deploy (push) Failing after 3s
Validate / validate (push) Failing after 26s
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>
2026-04-27 11:02:56 +07:00
grabowski 0225b204a2 chore(tooling): switch to fnm + pnpm, add DEPLOYMENT.md
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>
2026-04-24 15:25:15 +07:00
grabowski 0c9a69cfb8 feat(expenses): 6/12/24/All range selector on chart
- 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
2026-04-23 15:59:53 +07:00
grabowski 911898507a feat(expenses): CSV import with per-row validation
- 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
2026-04-23 15:51:26 +07:00
grabowski f8478f5019 fix(ui): cap date inputs to 4-digit years via min/max
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.
2026-04-23 15:45:15 +07:00
grabowski 3417ed6698 feat(properties): expenses tab with electricity+water chart
- 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
2026-04-23 15:32:20 +07:00
grabowski b59904fdae Phases 1-5 + rooms/floors, accounts, custom types, users, notifications
Data model
- Properties, rooms (+optional floors), assets (typed custom fields + Zod
  runtime validator + move history), documents (polymorphic scope)
- Projects -> work packages -> tasks -> subtasks
- Decision events (scoped to project/property/asset/work_package)
- Checklist templates + instances, maintenance schedules (time + usage) with
  auto-materialized checklists on event recording
- Wiki (global + per-project) with revisions + tsvector FTS
- Property accounts (utility/meter numbers by kind)
- Notifications table + per-user channel prefs

Infra
- RBAC guards (requireCompany / requireAdmin)
- Storage abstraction: LocalDiskStorage (HMAC signed URLs) + S3Storage
  behind the same interface, switchable via STORAGE_BACKEND
- CSV export for assets / maintenance / decisions
- QR labels: /api/qr SVG endpoint + printable /assets/[id]/label
- Notifications: in-app + SMTP (own server via nodemailer) + Matrix
  (Client-Server API, per-company room) with opt-in per user
- Company switcher + auto-select first company on login

UI
- Topbar: bell with unread count, theme toggle, name, Sign Out (flat)
- Sidebar: main nav + dedicated Admin section (Asset types, Users, Company)
- Nested-route tabs on property / project / asset detail pages
- Admin UIs for users (invite, role, reset pw, deactivate) and company
  settings (default currency, Matrix room id)
- Custom asset type creation + field-def editor with immutable key/type
  guard and auto-deprecate when removing a field still referenced

Graph
- graphify-out/ committed: GRAPH_REPORT.md, graph.html, graph.json
2026-04-23 15:18:11 +07:00
grabowski ad155d6344 create-user: look up company by slug and heal corrupted name
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>
2026-04-21 16:40:40 +07:00
grabowski b7807e41e0 Strip surrounding quotes from script args; add diag-user
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>
2026-04-21 16:39:16 +07:00
grabowski 98fe341e80 Load .env in env.ts so SvelteKit SSR sees DATABASE_URL etc.
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>
2026-04-21 16:33:50 +07:00
grabowski 95c1f61c88 Add README with setup, npm scripts, layout, roadmap
Covers prerequisites, .env setup, database bootstrap, user creation,
all npm commands, project layout, auth/storage models, phase roadmap,
locked design decisions, sibling-app references, and troubleshooting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:53:10 +07:00
grabowski 0a3aaa5798 Phase 0 scaffold: SvelteKit 5 + Drizzle + auth + storage interface
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>
2026-04-21 15:38:14 +07:00