Compare commits
11 Commits
76248c3d7f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 09e0fdc9ac | |||
| f5e4743120 | |||
| b4108c5a36 | |||
| 435bcb981f | |||
| 011e7a2165 | |||
| c3aaf82642 | |||
| 90207135c8 | |||
| c61be187e6 | |||
| 3106286629 | |||
| 3b34458a99 | |||
| 8117253841 |
@@ -190,6 +190,66 @@ systemctl status buildfor_life_ops
|
|||||||
journalctl -u buildfor_life_ops -f
|
journalctl -u buildfor_life_ops -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Maintenance reminders timer
|
||||||
|
|
||||||
|
The app records `next_due_at` on every time-based maintenance schedule but does not poll itself. A daily systemd timer runs `pnpm run reminders:check`, which scans for schedules entering the 7-day warning window or already overdue and fans out via the existing in-app + email + Matrix notifier. Re-runs are idempotent — `maintenance_reminders_sent` deduplicates per `(schedule, kind, due_at)`.
|
||||||
|
|
||||||
|
`/etc/systemd/system/buildfor_life_ops-reminders.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=buildfor_life_ops maintenance reminder cron
|
||||||
|
After=postgresql.service network.target
|
||||||
|
Wants=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=ops
|
||||||
|
Group=ops
|
||||||
|
WorkingDirectory=/home/ops/buildfor_life_ops
|
||||||
|
EnvironmentFile=/home/ops/buildfor_life_ops/.env
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
ExecStart=/home/ops/.local/share/fnm/aliases/default/bin/pnpm run reminders:check
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/systemd/system/buildfor_life_ops-reminders.timer`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Run buildfor_life_ops maintenance reminders daily
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 06:00:00
|
||||||
|
Persistent=true
|
||||||
|
Unit=buildfor_life_ops-reminders.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now buildfor_life_ops-reminders.timer
|
||||||
|
sudo systemctl list-timers buildfor_life_ops-reminders.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
**First-run protocol** to avoid a deluge of stale alerts on day one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inspect what would fire without notifying.
|
||||||
|
sudo -iu ops bash -lc 'cd ~/buildfor_life_ops && pnpm run reminders:check -- --dry-run'
|
||||||
|
|
||||||
|
# If the count is reasonable, run normally — the timer will pick up subsequent
|
||||||
|
# windows automatically. Or, if you want a clean slate, mark everything
|
||||||
|
# currently-due as already-notified (no fan-out), so day-one alerts only
|
||||||
|
# new breaches:
|
||||||
|
sudo -iu ops bash -lc 'cd ~/buildfor_life_ops && pnpm run reminders:check -- --backfill'
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs end up in `journalctl -u buildfor_life_ops-reminders.service`. Each run prints a single JSON line (`{ ok, scanned, fired, skippedDedup, noRecipients, ... }`) so `journalctl --output=cat | grep '"ok":true' | jq` gives a clean trend view.
|
||||||
|
|
||||||
## 11. Reverse proxy (nginx)
|
## 11. Reverse proxy (nginx)
|
||||||
|
|
||||||
`/etc/nginx/sites-available/buildfor_life_ops`:
|
`/etc/nginx/sites-available/buildfor_life_ops`:
|
||||||
|
|||||||
@@ -161,14 +161,38 @@ static/ public static assets (drop favicon here)
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
| Phase | Scope | State |
|
### Shipped
|
||||||
| --- | --- | --- |
|
|
||||||
| 0 | Scaffold: stack wiring + auth + layout shell + storage interface + tenancy schema + git remote | ✅ shipped |
|
| Phase | Scope |
|
||||||
| 1 | Properties + Assets with typed custom fields + mobility history + asset logs + document upload per scope | next |
|
| --- | --- |
|
||||||
| 2 | Checklist templates + maintenance schedules (time + usage) + maintenance events + usage readings | |
|
| 0 | Scaffold: stack wiring + auth + layout shell + storage interface + tenancy schema + git remote |
|
||||||
| 3 | Projects + WorkPackages → Tasks → Subtasks + **structured decision events** (title, body, alternatives, cost_impact, approved_by, tags) | |
|
| 1 | Properties + Assets with typed custom fields + mobility history + asset logs + document upload per scope |
|
||||||
| 4 | Wiki (global + project + property) with EasyMDE + revisions + FTS | |
|
| 2 | Checklist templates + maintenance schedules (time + usage) + maintenance events + usage readings |
|
||||||
| 5 *(later)* | QR label generation, email/in-app notifications, reports, S3 storage adapter, cross-app APIs | |
|
| 3 | Projects + WorkPackages → Tasks → Subtasks + structured decision events (title, body, alternatives, cost_impact, approved_by, tags) |
|
||||||
|
| 4 | Wiki (global + project + property) with EasyMDE + revisions + FTS |
|
||||||
|
| 5a | QR label generation, in-app + email + Matrix notifications, S3 storage adapter |
|
||||||
|
| — | RBAC: user/company admin, role middleware, last-admin guards |
|
||||||
|
| — | Property structure: rooms + floors, utility account/meter records, expenses with CSV import + electricity/water chart |
|
||||||
|
| — | Sub-property hierarchy: parent_id self-FK, recursive-CTE descendant rollups, "Include sub-properties" toggle on Expenses/Assets/Maintenance/Todos tabs, depth-first nested list view, depth-cap warning at 5 levels |
|
||||||
|
| — | Tooling: switched to fnm + pnpm, Gitea CI deploy workflow with public-HTTPS clone, full `DEPLOYMENT.md` |
|
||||||
|
|
||||||
|
### 5b — pending Phase 5 items
|
||||||
|
|
||||||
|
- **Reports** — cross-cutting outputs (monthly building roll-up, annual asset summary). Per-domain CSV exports already exist; this is the aggregated/scheduled layer
|
||||||
|
- **Cross-app APIs** — wire `buildfor_life_ops` to siblings `buildfor_life_budget` and `buildfor_life_repair` so decisions can link to budget line items and repair tickets
|
||||||
|
|
||||||
|
### 6 — quality-of-life backlog
|
||||||
|
|
||||||
|
Things that came up after the original roadmap was written. Not prioritised — pull whichever the next stakeholder ask depends on.
|
||||||
|
|
||||||
|
- **Maintenance reminders** — cron-style trigger that fires `maintenance_event_recorded` / `task_assigned` notifications when `next_due_at` crosses a threshold. The notification fan-out (app + email + Matrix) is already there; only the scheduler is missing
|
||||||
|
- **OIDC SSO** — env vars + schema are wired (`OIDC_ENABLED`, `OIDC_ISSUER`, etc.), but the route handlers were never written
|
||||||
|
- **Self-service password reset** — currently admin-initiated only via `pnpm run create-user`
|
||||||
|
- **Permission inheritance for sub-properties** — RBAC is per-company today; "manager of Building A" does not imply "manager of Apt 3B"
|
||||||
|
- **Bulk property CSV import** — "create N apartments under this building" in one upload, mirroring the existing expenses CSV importer
|
||||||
|
- **"Move to parent" quick action** — UI sugar over the existing edit form for re-parenting
|
||||||
|
- **Audit log viewer** — `audit_action` enum + audit infrastructure exists, but no UI to browse it
|
||||||
|
- **Project ↔ property linking** — they're disjoint today; many real workflows want "this project is happening at this property"
|
||||||
|
|
||||||
## Key design decisions
|
## Key design decisions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "properties" ADD COLUMN "parent_id" uuid;--> statement-breakpoint
|
||||||
|
ALTER TABLE "properties" ADD CONSTRAINT "properties_parent_id_properties_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."properties"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "properties_by_parent" ON "properties" USING btree ("company_id","parent_id");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- A property cannot be its own parent. Deeper cycle prevention (e.g.
|
||||||
|
-- A→B→A) is enforced in the service layer because Postgres can't express
|
||||||
|
-- recursive CHECK constraints; the assertNoCycle helper in
|
||||||
|
-- src/lib/server/services/properties.ts walks ancestors before save.
|
||||||
|
ALTER TABLE "properties"
|
||||||
|
ADD CONSTRAINT "properties_parent_not_self"
|
||||||
|
CHECK (parent_id IS NULL OR parent_id <> id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."checklist_scope" ADD VALUE 'property';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TYPE "public"."maintenance_reminder_kind" AS ENUM('due_soon', 'overdue');--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."notification_kind" ADD VALUE 'maintenance_due_soon' BEFORE 'generic';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."notification_kind" ADD VALUE 'maintenance_overdue' BEFORE 'generic';--> statement-breakpoint
|
||||||
|
CREATE TABLE "maintenance_reminders_sent" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"schedule_id" uuid NOT NULL,
|
||||||
|
"kind" "maintenance_reminder_kind" NOT NULL,
|
||||||
|
"due_at" timestamp with time zone NOT NULL,
|
||||||
|
"fired_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "maintenance_reminders_sent" ADD CONSTRAINT "maintenance_reminders_sent_schedule_id_maintenance_schedules_id_fk" FOREIGN KEY ("schedule_id") REFERENCES "public"."maintenance_schedules"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mrs_schedule_kind_due_uq" ON "maintenance_reminders_sent" USING btree ("schedule_id","kind","due_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "mrs_by_schedule" ON "maintenance_reminders_sent" USING btree ("schedule_id","kind");
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,34 @@
|
|||||||
"when": 1776932900000,
|
"when": 1776932900000,
|
||||||
"tag": "0015_expenses_updated_at_trigger",
|
"tag": "0015_expenses_updated_at_trigger",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777268853483,
|
||||||
|
"tag": "0016_property_parent_id",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777268853484,
|
||||||
|
"tag": "0017_property_parent_check",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777268985448,
|
||||||
|
"tag": "0018_checklist_scope_property",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777281036233,
|
||||||
|
"tag": "0019_maintenance_reminders",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{"id": "readme_buildfor_life_ops", "label": "buildfor_life_ops", "file_type": "document", "source_file": "README.md", "source_location": "L1-L3", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_buildfor_life_budget", "label": "buildfor_life_budget (sibling)", "file_type": "document", "source_file": "README.md", "source_location": "L4,L178", "source_url": "https://git.b4l.co.th/B4L/buildfor_life_budget", "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_buildfor_life_repair", "label": "buildfor_life_repair (sibling)", "file_type": "document", "source_file": "README.md", "source_location": "L4,L179", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_stack_sveltekit5", "label": "SvelteKit 5 (adapter-node)", "file_type": "document", "source_file": "README.md", "source_location": "L8", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_stack_tailwind_v4", "label": "Tailwind v4 + @theme inline tokens", "file_type": "document", "source_file": "README.md", "source_location": "L9", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_stack_postgres_drizzle", "label": "PostgreSQL 16+ via Drizzle ORM + Zod", "file_type": "document", "source_file": "README.md", "source_location": "L10", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_auth_argon2id", "label": "Argon2id sessions (@node-rs/argon2 + @oslojs/crypto)", "file_type": "document", "source_file": "README.md", "source_location": "L11", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_easymde", "label": "EasyMDE markdown editor", "file_type": "document", "source_file": "README.md", "source_location": "L12", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_sharp", "label": "Sharp image thumbnails", "file_type": "document", "source_file": "README.md", "source_location": "L12", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_storage_adapter", "label": "StorageAdapter interface", "file_type": "document", "source_file": "README.md", "source_location": "L13", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_local_disk_storage", "label": "LocalDiskStorage", "file_type": "document", "source_file": "README.md", "source_location": "L13,L143", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_s3_storage", "label": "S3Storage (future)", "file_type": "document", "source_file": "README.md", "source_location": "L144", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_env_dotenv", "label": ".env configuration", "file_type": "document", "source_file": "README.md", "source_location": "L29-L44", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_create_user_script", "label": "npm run create-user script", "file_type": "document", "source_file": "README.md", "source_location": "L59-L66,L122", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_migrate", "label": "npm run db:migrate", "file_type": "document", "source_file": "README.md", "source_location": "L54,L86", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_generate", "label": "npm run db:generate", "file_type": "document", "source_file": "README.md", "source_location": "L85", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_push", "label": "npm run db:push (dev only)", "file_type": "document", "source_file": "README.md", "source_location": "L87", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_studio", "label": "npm run db:studio (Drizzle Studio)", "file_type": "document", "source_file": "README.md", "source_location": "L88", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_seed", "label": "npm run db:seed", "file_type": "document", "source_file": "README.md", "source_location": "L89", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_validate_script", "label": "npm run validate (check + build)", "file_type": "document", "source_file": "README.md", "source_location": "L84", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_auth_model", "label": "Auth model (sessions + hashed cookies)", "file_type": "document", "source_file": "README.md", "source_location": "L132-L138", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_sliding_renewal", "label": "Sliding session renewal (30d/15d)", "file_type": "document", "source_file": "README.md", "source_location": "L135", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_sha256_cookie_hash", "label": "SHA-256 cookie hashing before DB lookup", "file_type": "document", "source_file": "README.md", "source_location": "L134", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_company_users", "label": "company_users role mapping", "file_type": "document", "source_file": "README.md", "source_location": "L138", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_storage_model", "label": "Storage model (opaque storage_key)", "file_type": "document", "source_file": "README.md", "source_location": "L141-L144", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_hmac_signed_urls", "label": "HMAC-signed short-lived file URLs", "file_type": "document", "source_file": "README.md", "source_location": "L143", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_api_files_route", "label": "/api/files route (signature verification + streaming)", "file_type": "document", "source_file": "README.md", "source_location": "L120,L143", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_layout_app_group", "label": "(app) route group (authed shell)", "file_type": "document", "source_file": "README.md", "source_location": "L113-L116,L137", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_layout_auth_group", "label": "(auth) route group (login shell)", "file_type": "document", "source_file": "README.md", "source_location": "L117-L118", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_hooks_server", "label": "hooks.server.ts (session validation)", "file_type": "document", "source_file": "README.md", "source_location": "L99", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_env_ts", "label": "env.ts (Zod-validated process.env)", "file_type": "document", "source_file": "README.md", "source_location": "L108", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_db_schema_dir", "label": "src/lib/server/db/schema/", "file_type": "document", "source_file": "README.md", "source_location": "L105-L106", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase0", "label": "Phase 0: scaffold (shipped)", "file_type": "document", "source_file": "README.md", "source_location": "L150", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase1", "label": "Phase 1: Properties + Assets", "file_type": "document", "source_file": "README.md", "source_location": "L151", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase2", "label": "Phase 2: Checklists + maintenance", "file_type": "document", "source_file": "README.md", "source_location": "L152", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase3", "label": "Phase 3: Projects + structured decisions", "file_type": "document", "source_file": "README.md", "source_location": "L153", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase4", "label": "Phase 4: Wiki + FTS", "file_type": "document", "source_file": "README.md", "source_location": "L154", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_roadmap_phase5", "label": "Phase 5: QR, notifications, S3", "file_type": "document", "source_file": "README.md", "source_location": "L155", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_uuidv7", "label": "Decision: UUID v7 primary keys", "file_type": "document", "source_file": "README.md", "source_location": "L161", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_timestamptz", "label": "Decision: timestamptz UTC everywhere", "file_type": "document", "source_file": "README.md", "source_location": "L162", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_soft_delete", "label": "Decision: soft delete (deleted_at)", "file_type": "document", "source_file": "README.md", "source_location": "L163", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_money_type", "label": "Decision: numeric(18,4) + char(3) currency", "file_type": "document", "source_file": "README.md", "source_location": "L164", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_jsonb_custom_fields", "label": "Decision: JSONB custom fields + asset_field_defs", "file_type": "document", "source_file": "README.md", "source_location": "L165", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_xor_location", "label": "Decision: XOR asset location (project XOR property)", "file_type": "document", "source_file": "README.md", "source_location": "L166", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_asset_history", "label": "Decision: asset_location_history (movable assets)", "file_type": "document", "source_file": "README.md", "source_location": "L167", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_immutable_keys", "label": "Decision: immutable custom-field keys", "file_type": "document", "source_file": "README.md", "source_location": "L168", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_decision_scope", "label": "Decision: decisions scoped to project/property/asset/work_package", "file_type": "document", "source_file": "README.md", "source_location": "L169", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_currency_default", "label": "Decision: company default currency in settings_json", "file_type": "document", "source_file": "README.md", "source_location": "L170", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_tabs_routes", "label": "Decision: tabs = nested routes (not query-string)", "file_type": "document", "source_file": "README.md", "source_location": "L171", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "readme_decision_theme_key", "label": "Decision: localStorage['theme'] key shared across siblings", "file_type": "document", "source_file": "README.md", "source_location": "L172", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "drizzle_readme_migrations", "label": "Drizzle migrations directory", "file_type": "document", "source_file": "drizzle/README.md", "source_location": "L1-L5", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "drizzle_readme_review_rationale", "label": "Review SQL after generate: enum/index/custom_fields", "file_type": "document", "source_file": "drizzle/README.md", "source_location": "L13-L18", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "drizzle_readme_concurrently_note", "label": "Use CONCURRENTLY on large-table index changes", "file_type": "document", "source_file": "drizzle/README.md", "source_location": "L16", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "drizzle_readme_immutable_key_ref", "label": "Immutable-key policy reference", "file_type": "document", "source_file": "drizzle/README.md", "source_location": "L17-L18", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "apphtml_root", "label": "app.html root document", "file_type": "code", "source_file": "src/app.html", "source_location": "L1-L20", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "apphtml_theme_bootstrap", "label": "Dark-mode bootstrap inline script (localStorage['theme'])", "file_type": "code", "source_file": "src/app.html", "source_location": "L7-L14", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "apphtml_sveltekit_placeholders", "label": "%sveltekit.head% / %sveltekit.body% placeholders", "file_type": "code", "source_file": "src/app.html", "source_location": "L15,L18", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "apphtml_tailwind_body_classes", "label": "Tailwind body classes with dark: variants", "file_type": "code", "source_file": "src/app.html", "source_location": "L17", "source_url": null, "captured_at": null, "author": null, "contributor": null},
|
|
||||||
{"id": "apphtml_preload_hover", "label": "data-sveltekit-preload-data=hover", "file_type": "code", "source_file": "src/app.html", "source_location": "L17", "source_url": null, "captured_at": null, "author": null, "contributor": null}
|
|
||||||
],
|
|
||||||
"edges": [
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_buildfor_life_budget", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L4", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_buildfor_life_repair", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L4", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_stack_sveltekit5", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L8", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_stack_tailwind_v4", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L9", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_stack_postgres_drizzle", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L10", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_auth_argon2id", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L11", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_easymde", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L12", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_sharp", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L12", "weight": 1.0},
|
|
||||||
{"source": "readme_storage_adapter", "target": "readme_local_disk_storage", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L13", "weight": 1.0},
|
|
||||||
{"source": "readme_storage_adapter", "target": "readme_s3_storage", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L144", "weight": 1.0},
|
|
||||||
{"source": "readme_local_disk_storage", "target": "readme_hmac_signed_urls", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L143", "weight": 1.0},
|
|
||||||
{"source": "readme_api_files_route", "target": "readme_hmac_signed_urls", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L120,L143", "weight": 1.0},
|
|
||||||
{"source": "readme_api_files_route", "target": "readme_local_disk_storage", "relation": "calls", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L120", "weight": 1.0},
|
|
||||||
{"source": "readme_storage_model", "target": "readme_storage_adapter", "relation": "rationale_for", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L141-L144", "weight": 1.0},
|
|
||||||
{"source": "readme_auth_model", "target": "readme_sha256_cookie_hash", "relation": "rationale_for", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L134", "weight": 1.0},
|
|
||||||
{"source": "readme_auth_model", "target": "readme_sliding_renewal", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L135", "weight": 1.0},
|
|
||||||
{"source": "readme_auth_model", "target": "readme_company_users", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L138", "weight": 1.0},
|
|
||||||
{"source": "readme_hooks_server", "target": "readme_auth_model", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L99,L135", "weight": 1.0},
|
|
||||||
{"source": "readme_layout_app_group", "target": "readme_auth_model", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L137", "weight": 1.0},
|
|
||||||
{"source": "readme_env_dotenv", "target": "readme_env_ts", "relation": "shares_data_with", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L108,L183", "weight": 1.0},
|
|
||||||
{"source": "readme_create_user_script", "target": "readme_db_schema_dir", "relation": "shares_data_with", "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "README.md", "source_location": "L122", "weight": 1.0},
|
|
||||||
{"source": "readme_db_migrate", "target": "drizzle_readme_migrations", "relation": "calls", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L9", "weight": 1.0},
|
|
||||||
{"source": "readme_db_generate", "target": "drizzle_readme_migrations", "relation": "calls", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L8", "weight": 1.0},
|
|
||||||
{"source": "readme_db_push", "target": "drizzle_readme_migrations", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L10", "weight": 1.0},
|
|
||||||
{"source": "readme_db_studio", "target": "drizzle_readme_migrations", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L11", "weight": 1.0},
|
|
||||||
{"source": "drizzle_readme_review_rationale", "target": "drizzle_readme_migrations", "relation": "rationale_for", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L13-L18", "weight": 1.0},
|
|
||||||
{"source": "drizzle_readme_concurrently_note", "target": "drizzle_readme_review_rationale", "relation": "rationale_for", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L16", "weight": 1.0},
|
|
||||||
{"source": "drizzle_readme_immutable_key_ref", "target": "readme_decision_immutable_keys", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "drizzle/README.md", "source_location": "L17-L18", "weight": 1.0},
|
|
||||||
{"source": "readme_decision_jsonb_custom_fields", "target": "readme_decision_immutable_keys", "relation": "conceptually_related_to", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L165,L168", "weight": 1.0},
|
|
||||||
{"source": "readme_decision_xor_location", "target": "readme_decision_asset_history", "relation": "conceptually_related_to", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L166-L167", "weight": 1.0},
|
|
||||||
{"source": "readme_roadmap_phase5", "target": "readme_s3_storage", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L155", "weight": 1.0},
|
|
||||||
{"source": "readme_roadmap_phase3", "target": "readme_decision_decision_scope", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L153,L169", "weight": 1.0},
|
|
||||||
{"source": "readme_roadmap_phase4", "target": "readme_easymde", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L154,L12", "weight": 1.0},
|
|
||||||
{"source": "readme_roadmap_phase1", "target": "readme_decision_jsonb_custom_fields", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L151,L165", "weight": 1.0},
|
|
||||||
{"source": "apphtml_root", "target": "apphtml_theme_bootstrap", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "src/app.html", "source_location": "L7-L14", "weight": 1.0},
|
|
||||||
{"source": "apphtml_root", "target": "apphtml_sveltekit_placeholders", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "src/app.html", "source_location": "L15,L18", "weight": 1.0},
|
|
||||||
{"source": "apphtml_root", "target": "apphtml_tailwind_body_classes", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "src/app.html", "source_location": "L17", "weight": 1.0},
|
|
||||||
{"source": "apphtml_root", "target": "apphtml_preload_hover", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "src/app.html", "source_location": "L17", "weight": 1.0},
|
|
||||||
{"source": "apphtml_theme_bootstrap", "target": "readme_decision_theme_key", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L172", "weight": 1.0},
|
|
||||||
{"source": "apphtml_tailwind_body_classes", "target": "readme_stack_tailwind_v4", "relation": "implements", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L9,L17", "weight": 1.0},
|
|
||||||
{"source": "apphtml_root", "target": "readme_stack_sveltekit5", "relation": "implements", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "src/app.html", "source_location": "L1-L20", "weight": 1.0},
|
|
||||||
{"source": "readme_decision_theme_key", "target": "apphtml_theme_bootstrap", "relation": "rationale_for", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "README.md", "source_location": "L172", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_buildfor_life_budget", "relation": "semantically_similar_to", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "README.md", "source_location": "L4,L178", "weight": 1.0},
|
|
||||||
{"source": "readme_buildfor_life_ops", "target": "readme_buildfor_life_repair", "relation": "semantically_similar_to", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "README.md", "source_location": "L4,L179", "weight": 1.0},
|
|
||||||
{"source": "readme_local_disk_storage", "target": "readme_s3_storage", "relation": "semantically_similar_to", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "README.md", "source_location": "L13,L144", "weight": 1.0}
|
|
||||||
],
|
|
||||||
"hyperedges": [
|
|
||||||
{"id": "auth_session_flow", "label": "Session auth flow (cookie, hash, hook, gate)", "nodes": ["readme_auth_model", "readme_sha256_cookie_hash", "readme_sliding_renewal", "readme_hooks_server", "readme_layout_app_group"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.95, "source_file": "README.md"},
|
|
||||||
{"id": "storage_abstraction_stack", "label": "Storage abstraction (adapter, local impl, signed URLs, file route)", "nodes": ["readme_storage_adapter", "readme_local_disk_storage", "readme_hmac_signed_urls", "readme_api_files_route", "readme_storage_model"], "relation": "implement", "confidence": "EXTRACTED", "confidence_score": 0.95, "source_file": "README.md"},
|
|
||||||
{"id": "theme_propagation_pattern", "label": "Cross-sibling theme propagation via localStorage", "nodes": ["readme_decision_theme_key", "apphtml_theme_bootstrap", "readme_buildfor_life_budget", "readme_buildfor_life_repair"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "README.md"}
|
|
||||||
],
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0
|
|
||||||
}
|
|
||||||
+249
-205
@@ -1,45 +1,45 @@
|
|||||||
# Graph Report - C:/dev/build_for_life_project (2026-04-23)
|
# Graph Report - . (2026-04-27)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 189 files · ~54,875 words
|
- 47 files · ~0 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- 453 nodes · 486 edges · 131 communities detected
|
- 555 nodes · 633 edges · 137 communities detected
|
||||||
- Extraction: 80% EXTRACTED · 20% INFERRED · 0% AMBIGUOUS · INFERRED: 97 edges (avg confidence: 0.8)
|
- Extraction: 83% EXTRACTED · 17% INFERRED · 0% AMBIGUOUS · INFERRED: 109 edges (avg confidence: 0.8)
|
||||||
- Token cost: 0 input · 0 output
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
## Community Hubs (Navigation)
|
## Community Hubs (Navigation)
|
||||||
- [[_COMMUNITY_Auth & Load Helpers|Auth & Load Helpers]]
|
- [[_COMMUNITY_Page Server Loaders|Page Server Loaders]]
|
||||||
- [[_COMMUNITY_Documents Service|Documents Service]]
|
- [[_COMMUNITY_Stack & Deployment Concepts|Stack & Deployment Concepts]]
|
||||||
- [[_COMMUNITY_Assets Service & CSV|Assets Service & CSV]]
|
- [[_COMMUNITY_Documents & Storage Adapters|Documents & Storage Adapters]]
|
||||||
- [[_COMMUNITY_Email & Markdown|Email & Markdown]]
|
- [[_COMMUNITY_Auth Sessions & Catalog|Auth Sessions & Catalog]]
|
||||||
- [[_COMMUNITY_Property Accounts|Property Accounts]]
|
- [[_COMMUNITY_CSV & API Endpoints|CSV & API Endpoints]]
|
||||||
|
- [[_COMMUNITY_Expenses Service|Expenses Service]]
|
||||||
|
- [[_COMMUNITY_Email & Markdown Rendering|Email & Markdown Rendering]]
|
||||||
- [[_COMMUNITY_Projects Service|Projects Service]]
|
- [[_COMMUNITY_Projects Service|Projects Service]]
|
||||||
- [[_COMMUNITY_App Shell & Theme|App Shell & Theme]]
|
- [[_COMMUNITY_Bootstrap Scripts|Bootstrap Scripts]]
|
||||||
- [[_COMMUNITY_Asset Core|Asset Core]]
|
- [[_COMMUNITY_Maintenance Schedules|Maintenance Schedules]]
|
||||||
- [[_COMMUNITY_Maintenance Core|Maintenance Core]]
|
- [[_COMMUNITY_Assets Service|Assets Service]]
|
||||||
- [[_COMMUNITY_Checklists|Checklists]]
|
- [[_COMMUNITY_App Shell & Bootstrap|App Shell & Bootstrap]]
|
||||||
- [[_COMMUNITY_Admin Scripts|Admin Scripts]]
|
- [[_COMMUNITY_Checklists Service|Checklists Service]]
|
||||||
- [[_COMMUNITY_Asset Types Editor|Asset Types Editor]]
|
- [[_COMMUNITY_Rooms & Floors Service|Rooms & Floors Service]]
|
||||||
- [[_COMMUNITY_Rooms & Floors|Rooms & Floors]]
|
- [[_COMMUNITY_Tasks & Subtasks|Tasks & Subtasks]]
|
||||||
- [[_COMMUNITY_Tasks|Tasks]]
|
|
||||||
- [[_COMMUNITY_User Management|User Management]]
|
|
||||||
- [[_COMMUNITY_DB Schema Helpers|DB Schema Helpers]]
|
|
||||||
- [[_COMMUNITY_Work Packages|Work Packages]]
|
- [[_COMMUNITY_Work Packages|Work Packages]]
|
||||||
- [[_COMMUNITY_Storage Layer|Storage Layer]]
|
- [[_COMMUNITY_Signed URL Storage Layer|Signed URL Storage Layer]]
|
||||||
- [[_COMMUNITY_Migration Workflow|Migration Workflow]]
|
- [[_COMMUNITY_Drizzle Migration Conventions|Drizzle Migration Conventions]]
|
||||||
- [[_COMMUNITY_Session Auth|Session Auth]]
|
- [[_COMMUNITY_Auth Model|Auth Model]]
|
||||||
- [[_COMMUNITY_Companies Service|Companies Service]]
|
- [[_COMMUNITY_Companies Service|Companies Service]]
|
||||||
- [[_COMMUNITY_Form Utilities|Form Utilities]]
|
- [[_COMMUNITY_JSONB Custom Fields Policy|JSONB Custom Fields Policy]]
|
||||||
- [[_COMMUNITY_Custom Fields Design|Custom Fields Design]]
|
- [[_COMMUNITY_Cluster 21|Cluster 21]]
|
||||||
- [[_COMMUNITY_Field Types|Field Types]]
|
- [[_COMMUNITY_Cluster 22|Cluster 22]]
|
||||||
- [[_COMMUNITY_Form Helper|Form Helper]]
|
- [[_COMMUNITY_Cluster 23|Cluster 23]]
|
||||||
- [[_COMMUNITY_Env Config|Env Config]]
|
- [[_COMMUNITY_Cluster 24|Cluster 24]]
|
||||||
- [[_COMMUNITY_Seed Script|Seed Script]]
|
- [[_COMMUNITY_Cluster 25|Cluster 25]]
|
||||||
- [[_COMMUNITY_Asset Location Design|Asset Location Design]]
|
- [[_COMMUNITY_Cluster 26|Cluster 26]]
|
||||||
- [[_COMMUNITY_Decision Design|Decision Design]]
|
- [[_COMMUNITY_Cluster 27|Cluster 27]]
|
||||||
- [[_COMMUNITY_Drizzle Config|Drizzle Config]]
|
- [[_COMMUNITY_Cluster 28|Cluster 28]]
|
||||||
|
- [[_COMMUNITY_Cluster 29|Cluster 29]]
|
||||||
- [[_COMMUNITY_Cluster 30|Cluster 30]]
|
- [[_COMMUNITY_Cluster 30|Cluster 30]]
|
||||||
- [[_COMMUNITY_Cluster 31|Cluster 31]]
|
- [[_COMMUNITY_Cluster 31|Cluster 31]]
|
||||||
- [[_COMMUNITY_Cluster 32|Cluster 32]]
|
- [[_COMMUNITY_Cluster 32|Cluster 32]]
|
||||||
@@ -141,30 +141,36 @@
|
|||||||
- [[_COMMUNITY_Cluster 128|Cluster 128]]
|
- [[_COMMUNITY_Cluster 128|Cluster 128]]
|
||||||
- [[_COMMUNITY_Cluster 129|Cluster 129]]
|
- [[_COMMUNITY_Cluster 129|Cluster 129]]
|
||||||
- [[_COMMUNITY_Cluster 130|Cluster 130]]
|
- [[_COMMUNITY_Cluster 130|Cluster 130]]
|
||||||
|
- [[_COMMUNITY_Cluster 131|Cluster 131]]
|
||||||
|
- [[_COMMUNITY_Cluster 132|Cluster 132]]
|
||||||
|
- [[_COMMUNITY_Cluster 133|Cluster 133]]
|
||||||
|
- [[_COMMUNITY_Cluster 134|Cluster 134]]
|
||||||
|
- [[_COMMUNITY_Cluster 135|Cluster 135]]
|
||||||
|
- [[_COMMUNITY_Cluster 136|Cluster 136]]
|
||||||
|
|
||||||
## God Nodes (most connected - your core abstractions)
|
## God Nodes (most connected - your core abstractions)
|
||||||
1. `load()` - 79 edges
|
1. `load()` - 95 edges
|
||||||
2. `GET()` - 20 edges
|
2. `buildfor_life_ops` - 26 edges
|
||||||
3. `LocalDiskStorage` - 10 edges
|
3. `GET()` - 23 edges
|
||||||
4. `load()` - 9 edges
|
4. `Drizzle ORM + Zod` - 14 edges
|
||||||
5. `S3Storage` - 8 edges
|
5. `LocalDiskStorage` - 10 edges
|
||||||
6. `buildfor_life_ops` - 8 edges
|
6. `load()` - 9 edges
|
||||||
7. `fanOutExternal()` - 7 edges
|
7. `S3Storage` - 8 edges
|
||||||
8. `getTaskWithSubtasks()` - 7 edges
|
8. `buildfor_life_ops` - 8 edges
|
||||||
9. `handle()` - 6 edges
|
9. `Drizzle migrations (drizzle/)` - 8 edges
|
||||||
10. `uploadDocument()` - 6 edges
|
10. `fanOutExternal()` - 7 edges
|
||||||
|
|
||||||
## Surprising Connections (you probably didn't know these)
|
## Surprising Connections (you probably didn't know these)
|
||||||
- `load()` --calls--> `renderMarkdown()` [INFERRED]
|
- `load()` --calls--> `renderMarkdown()` [INFERRED]
|
||||||
src\routes\(auth)\login\+page.server.ts → src\lib\server\markdown.ts
|
src\routes\(app)\properties\[id]\todos\+page.server.ts → src\lib\server\markdown.ts
|
||||||
- `load()` --calls--> `listTemplates()` [INFERRED]
|
- `load()` --calls--> `listTemplates()` [INFERRED]
|
||||||
src\routes\(auth)\login\+page.server.ts → src\lib\server\services\checklists.ts
|
src\routes\(app)\properties\[id]\todos\+page.server.ts → src\lib\server\services\checklists.ts
|
||||||
- `load()` --calls--> `getCompany()` [INFERRED]
|
- `load()` --calls--> `getCompany()` [INFERRED]
|
||||||
src\routes\(auth)\login\+page.server.ts → src\lib\server\services\companies.ts
|
src\routes\(app)\properties\[id]\todos\+page.server.ts → src\lib\server\services\companies.ts
|
||||||
- `load()` --calls--> `listDocumentsForScope()` [INFERRED]
|
- `load()` --calls--> `listDocumentsForScope()` [INFERRED]
|
||||||
src\routes\(auth)\login\+page.server.ts → src\lib\server\services\documents.ts
|
src\routes\(app)\properties\[id]\todos\+page.server.ts → src\lib\server\services\documents.ts
|
||||||
- `load()` --calls--> `countOverdueForCompany()` [INFERRED]
|
- `load()` --calls--> `countOverdueForCompany()` [INFERRED]
|
||||||
src\routes\(auth)\login\+page.server.ts → src\lib\server\services\maintenance.ts
|
src\routes\(app)\properties\[id]\todos\+page.server.ts → src\lib\server\services\maintenance.ts
|
||||||
|
|
||||||
## Hyperedges (group relationships)
|
## Hyperedges (group relationships)
|
||||||
- **Session auth flow (cookie, hash, hook, gate)** — readme_auth_model, readme_sha256_cookie_hash, readme_sliding_renewal, readme_hooks_server, readme_layout_app_group [EXTRACTED 0.95]
|
- **Session auth flow (cookie, hash, hook, gate)** — readme_auth_model, readme_sha256_cookie_hash, readme_sliding_renewal, readme_hooks_server, readme_layout_app_group [EXTRACTED 0.95]
|
||||||
@@ -173,123 +179,123 @@
|
|||||||
|
|
||||||
## Communities
|
## Communities
|
||||||
|
|
||||||
### Community 0 - "Auth & Load Helpers"
|
### Community 0 - "Page Server Loaders"
|
||||||
Cohesion: 0.04
|
Cohesion: 0.04
|
||||||
Nodes (12): requireAdmin(), requireCompany(), load(), parseSettings(), getPageWithCurrentRevision(), getRevision(), listPagesForScope(), listRevisions() (+4 more)
|
Nodes (15): requireAdmin(), requireCompany(), e2n(), flattenTree(), load(), parseRange(), parseSettings(), getPageWithCurrentRevision() (+7 more)
|
||||||
|
|
||||||
### Community 1 - "Documents Service"
|
### Community 1 - "Stack & Deployment Concepts"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (59): Argon2id sessions (@node-rs/argon2), asset_location_history (movable assets), XOR location: asset at project OR property (CHECK), Blob storage snapshot/rsync backup, 10M upload size cap (BODY_SIZE_LIMIT + client_max_body_size), buildfor_life_budget (sibling), buildfor_life_ops, buildfor_life_repair (sibling) (+51 more)
|
||||||
|
|
||||||
|
### Community 2 - "Documents & Storage Adapters"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.07
|
||||||
Nodes (13): assertScope(), deleteDocument(), getDocument(), listDocumentsForScope(), signedUrlForDocument(), uploadDocument(), getStorage(), LocalDiskStorage (+5 more)
|
Nodes (13): assertScope(), deleteDocument(), getDocument(), listDocumentsForScope(), signedUrlForDocument(), uploadDocument(), getStorage(), LocalDiskStorage (+5 more)
|
||||||
|
|
||||||
### Community 2 - "Assets Service & CSV"
|
### Community 3 - "Auth Sessions & Catalog"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.1
|
||||||
Nodes (13): listAssets(), csvResponse(), toCsv(), gatherCustomFieldsFromForm(), createDecision(), decisionScopeLink(), listDecisionsForScope(), clamp() (+5 more)
|
Nodes (20): assertProperty(), createAccount(), deleteAccount(), listAccounts(), addFieldDef(), createCompanyAssetType(), deleteCompanyAssetType(), loadEditableType() (+12 more)
|
||||||
|
|
||||||
### Community 3 - "Email & Markdown"
|
### Community 4 - "CSV & API Endpoints"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (12): csvResponse(), toCsv(), gatherCustomFieldsFromForm(), createDecision(), decisionScopeLink(), listDecisionsForScope(), clamp(), GET() (+4 more)
|
||||||
|
|
||||||
|
### Community 5 - "Expenses Service"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (13): Drizzle ORM + Zod, assertAccountInProperty(), assertProperty(), createExpense(), importExpenses(), listExpensesForProperties(), listExpensesForProperty(), monthlySeriesForProperties() (+5 more)
|
||||||
|
|
||||||
|
### Community 6 - "Email & Markdown Rendering"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (15): getTransport(), isEmailConfigured(), sendEmail(), escapeHtml(), html(), renderMarkdown(), buildBodies(), isMatrixConfigured() (+7 more)
|
Nodes (15): getTransport(), isEmailConfigured(), sendEmail(), escapeHtml(), html(), renderMarkdown(), buildBodies(), isMatrixConfigured() (+7 more)
|
||||||
|
|
||||||
### Community 4 - "Property Accounts"
|
### Community 7 - "Projects Service"
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (12): assertProperty(), createAccount(), deleteAccount(), listAccounts(), handle(), deleteFloor(), handleLogout(), createSession() (+4 more)
|
|
||||||
|
|
||||||
### Community 5 - "Projects Service"
|
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
Nodes (6): load(), unreadCountForUser(), getProject(), listProjects(), getProperty(), listProperties()
|
Nodes (13): load(), unreadCountForUser(), getProject(), listProjects(), assertNoCycle(), assertParentInCompany(), createProperty(), getAncestorIds() (+5 more)
|
||||||
|
|
||||||
### Community 6 - "App Shell & Theme"
|
### Community 8 - "Bootstrap Scripts"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (17): main(), readArg(), slugify(), stripSurroundingQuotes(), main(), readArg(), stripSurroundingQuotes(), normalizeEmail() (+9 more)
|
||||||
|
|
||||||
|
### Community 9 - "Maintenance Schedules"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (15): addInterval(), assertAsset(), countOverdueForCompany(), createSchedule(), deleteSchedule(), getSchedule(), listDueAndOverdue(), listEventsForAsset() (+7 more)
|
||||||
|
|
||||||
|
### Community 10 - "Assets Service"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (11): assertContainer(), createAsset(), listAssets(), loadTypeWithFields(), moveAsset(), updateAsset(), validateCustomFields(), buildCustomFieldsSchema() (+3 more)
|
||||||
|
|
||||||
|
### Community 11 - "App Shell & Bootstrap"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
Nodes (16): data-sveltekit-preload-data=hover, app.html root document, %sveltekit.head% / %sveltekit.body% placeholders, Tailwind body classes with dark: variants, Dark-mode bootstrap inline script (localStorage['theme']), Argon2id sessions (@node-rs/argon2 + @oslojs/crypto), buildfor_life_budget (sibling), buildfor_life_ops (+8 more)
|
Nodes (16): data-sveltekit-preload-data=hover, app.html root document, %sveltekit.head% / %sveltekit.body% placeholders, Tailwind body classes with dark: variants, Dark-mode bootstrap inline script (localStorage['theme']), Argon2id sessions (@node-rs/argon2 + @oslojs/crypto), buildfor_life_budget (sibling), buildfor_life_ops (+8 more)
|
||||||
|
|
||||||
### Community 7 - "Asset Core"
|
### Community 12 - "Checklists Service"
|
||||||
Cohesion: 0.22
|
|
||||||
Nodes (10): assertContainer(), createAsset(), loadTypeWithFields(), moveAsset(), updateAsset(), validateCustomFields(), buildCustomFieldsSchema(), getCachedCustomFieldsSchema() (+2 more)
|
|
||||||
|
|
||||||
### Community 8 - "Maintenance Core"
|
|
||||||
Cohesion: 0.22
|
|
||||||
Nodes (13): addInterval(), assertAsset(), countOverdueForCompany(), createSchedule(), deleteSchedule(), getSchedule(), listDueAndOverdue(), listEventsForAsset() (+5 more)
|
|
||||||
|
|
||||||
### Community 9 - "Checklists"
|
|
||||||
Cohesion: 0.19
|
Cohesion: 0.19
|
||||||
Nodes (7): addTemplateItem(), deleteTemplate(), getInstance(), getTemplate(), listTemplates(), removeTemplateItem(), setItemDone()
|
Nodes (7): addTemplateItem(), getInstance(), getTemplate(), listInstancesForProperties(), listTemplates(), removeTemplateItem(), setItemDone()
|
||||||
|
|
||||||
### Community 10 - "Admin Scripts"
|
### Community 13 - "Rooms & Floors Service"
|
||||||
Cohesion: 0.24
|
Cohesion: 0.29
|
||||||
Nodes (9): main(), readArg(), slugify(), stripSurroundingQuotes(), main(), readArg(), stripSurroundingQuotes(), normalizeEmail() (+1 more)
|
Nodes (9): assertProperty(), createFloor(), createRoom(), deleteFloor(), getRoom(), listFloors(), listRoomsWithCounts(), softDeleteRoom() (+1 more)
|
||||||
|
|
||||||
### Community 11 - "Asset Types Editor"
|
### Community 14 - "Tasks & Subtasks"
|
||||||
Cohesion: 0.31
|
|
||||||
Nodes (8): addFieldDef(), createCompanyAssetType(), deleteCompanyAssetType(), loadEditableType(), normalizeFieldKey(), removeFieldDef(), slugifyTypeSlug(), updateCompanyAssetType()
|
|
||||||
|
|
||||||
### Community 12 - "Rooms & Floors"
|
|
||||||
Cohesion: 0.33
|
|
||||||
Nodes (8): assertProperty(), createFloor(), createRoom(), getRoom(), listFloors(), listRoomsWithCounts(), softDeleteRoom(), updateRoom()
|
|
||||||
|
|
||||||
### Community 13 - "Tasks"
|
|
||||||
Cohesion: 0.36
|
Cohesion: 0.36
|
||||||
Nodes (9): addSubtask(), assertWorkPackage(), createTask(), getTaskWithSubtasks(), listTasksForWorkPackage(), removeSubtask(), softDeleteTask(), toggleSubtask() (+1 more)
|
Nodes (9): addSubtask(), assertWorkPackage(), createTask(), getTaskWithSubtasks(), listTasksForWorkPackage(), removeSubtask(), softDeleteTask(), toggleSubtask() (+1 more)
|
||||||
|
|
||||||
### Community 14 - "User Management"
|
### Community 15 - "Work Packages"
|
||||||
Cohesion: 0.38
|
|
||||||
Nodes (8): assertMembership(), countAdmins(), listCompanyUsers(), removeUserFromCompany(), resetUserPassword(), setUserActive(), setUserRoleInCompany(), updateDisplayName()
|
|
||||||
|
|
||||||
### Community 15 - "DB Schema Helpers"
|
|
||||||
Cohesion: 0.29
|
|
||||||
Nodes (0):
|
|
||||||
|
|
||||||
### Community 16 - "Work Packages"
|
|
||||||
Cohesion: 0.48
|
Cohesion: 0.48
|
||||||
Nodes (6): assertProject(), createWorkPackage(), getWorkPackage(), listWorkPackagesForProject(), softDeleteWorkPackage(), updateWorkPackage()
|
Nodes (6): assertProject(), createWorkPackage(), getWorkPackage(), listWorkPackagesForProject(), softDeleteWorkPackage(), updateWorkPackage()
|
||||||
|
|
||||||
### Community 17 - "Storage Layer"
|
### Community 16 - "Signed URL Storage Layer"
|
||||||
Cohesion: 0.38
|
Cohesion: 0.38
|
||||||
Nodes (7): /api/files route (signature verification + streaming), HMAC-signed short-lived file URLs, LocalDiskStorage, Phase 5: QR, notifications, S3, S3Storage (future), StorageAdapter interface, Storage model (opaque storage_key)
|
Nodes (7): /api/files route (signature verification + streaming), HMAC-signed short-lived file URLs, LocalDiskStorage, Phase 5: QR, notifications, S3, S3Storage (future), StorageAdapter interface, Storage model (opaque storage_key)
|
||||||
|
|
||||||
### Community 18 - "Migration Workflow"
|
### Community 17 - "Drizzle Migration Conventions"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
Nodes (7): Use CONCURRENTLY on large-table index changes, Drizzle migrations directory, Review SQL after generate: enum/index/custom_fields, npm run db:generate, npm run db:migrate, npm run db:push (dev only), npm run db:studio (Drizzle Studio)
|
Nodes (7): Use CONCURRENTLY on large-table index changes, Drizzle migrations directory, Review SQL after generate: enum/index/custom_fields, npm run db:generate, npm run db:migrate, npm run db:push (dev only), npm run db:studio (Drizzle Studio)
|
||||||
|
|
||||||
### Community 19 - "Session Auth"
|
### Community 18 - "Auth Model"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
Nodes (6): Auth model (sessions + hashed cookies), company_users role mapping, hooks.server.ts (session validation), (app) route group (authed shell), SHA-256 cookie hashing before DB lookup, Sliding session renewal (30d/15d)
|
Nodes (6): Auth model (sessions + hashed cookies), company_users role mapping, hooks.server.ts (session validation), (app) route group (authed shell), SHA-256 cookie hashing before DB lookup, Sliding session renewal (30d/15d)
|
||||||
|
|
||||||
### Community 20 - "Companies Service"
|
### Community 19 - "Companies Service"
|
||||||
Cohesion: 0.6
|
Cohesion: 0.6
|
||||||
Nodes (4): createCompanyWithAdmin(), getCompany(), slugify(), updateCompany()
|
Nodes (4): createCompanyWithAdmin(), getCompany(), slugify(), updateCompany()
|
||||||
|
|
||||||
### Community 21 - "Form Utilities"
|
### Community 20 - "JSONB Custom Fields Policy"
|
||||||
Cohesion: 0.4
|
|
||||||
Nodes (1): e2n()
|
|
||||||
|
|
||||||
### Community 22 - "Custom Fields Design"
|
|
||||||
Cohesion: 0.5
|
Cohesion: 0.5
|
||||||
Nodes (4): Immutable-key policy reference, Decision: immutable custom-field keys, Decision: JSONB custom fields + asset_field_defs, Phase 1: Properties + Assets
|
Nodes (4): Immutable-key policy reference, Decision: immutable custom-field keys, Decision: JSONB custom fields + asset_field_defs, Phase 1: Properties + Assets
|
||||||
|
|
||||||
### Community 23 - "Field Types"
|
### Community 21 - "Cluster 21"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): parseCsv(), parseCsvDict()
|
||||||
|
|
||||||
|
### Community 22 - "Cluster 22"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (0):
|
Nodes (0):
|
||||||
|
|
||||||
### Community 24 - "Form Helper"
|
### Community 23 - "Cluster 23"
|
||||||
Cohesion: 1.0
|
|
||||||
Nodes (0):
|
|
||||||
|
|
||||||
### Community 25 - "Env Config"
|
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (2): .env configuration, env.ts (Zod-validated process.env)
|
Nodes (2): .env configuration, env.ts (Zod-validated process.env)
|
||||||
|
|
||||||
### Community 26 - "Seed Script"
|
### Community 24 - "Cluster 24"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (2): npm run create-user script, src/lib/server/db/schema/
|
Nodes (2): npm run create-user script, src/lib/server/db/schema/
|
||||||
|
|
||||||
### Community 27 - "Asset Location Design"
|
### Community 25 - "Cluster 25"
|
||||||
Cohesion: 1.0
|
|
||||||
Nodes (2): Decision: asset_location_history (movable assets), Decision: XOR asset location (project XOR property)
|
|
||||||
|
|
||||||
### Community 28 - "Decision Design"
|
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (2): Decision: decisions scoped to project/property/asset/work_package, Phase 3: Projects + structured decisions
|
Nodes (2): Decision: decisions scoped to project/property/asset/work_package, Phase 3: Projects + structured decisions
|
||||||
|
|
||||||
### Community 29 - "Drizzle Config"
|
### Community 26 - "Cluster 26"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Decision: asset_location_history (movable assets), Decision: XOR asset location (project XOR property)
|
||||||
|
|
||||||
|
### Community 27 - "Cluster 27"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 28 - "Cluster 28"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 29 - "Cluster 29"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (0):
|
Nodes (0):
|
||||||
|
|
||||||
@@ -647,146 +653,172 @@ Nodes (0):
|
|||||||
|
|
||||||
### Community 118 - "Cluster 118"
|
### Community 118 - "Cluster 118"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (0):
|
Nodes (1): npm run db:seed
|
||||||
|
|
||||||
### Community 119 - "Cluster 119"
|
### Community 119 - "Cluster 119"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (0):
|
Nodes (1): npm run validate (check + build)
|
||||||
|
|
||||||
### Community 120 - "Cluster 120"
|
### Community 120 - "Cluster 120"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): npm run db:seed
|
Nodes (1): (auth) route group (login shell)
|
||||||
|
|
||||||
### Community 121 - "Cluster 121"
|
### Community 121 - "Cluster 121"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): npm run validate (check + build)
|
Nodes (1): Phase 0: scaffold (shipped)
|
||||||
|
|
||||||
### Community 122 - "Cluster 122"
|
### Community 122 - "Cluster 122"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): (auth) route group (login shell)
|
Nodes (1): Phase 2: Checklists + maintenance
|
||||||
|
|
||||||
### Community 123 - "Cluster 123"
|
### Community 123 - "Cluster 123"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Phase 0: scaffold (shipped)
|
Nodes (1): Decision: UUID v7 primary keys
|
||||||
|
|
||||||
### Community 124 - "Cluster 124"
|
### Community 124 - "Cluster 124"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Phase 2: Checklists + maintenance
|
Nodes (1): Decision: timestamptz UTC everywhere
|
||||||
|
|
||||||
### Community 125 - "Cluster 125"
|
### Community 125 - "Cluster 125"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: UUID v7 primary keys
|
Nodes (1): Decision: soft delete (deleted_at)
|
||||||
|
|
||||||
### Community 126 - "Cluster 126"
|
### Community 126 - "Cluster 126"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: timestamptz UTC everywhere
|
Nodes (1): Decision: numeric(18,4) + char(3) currency
|
||||||
|
|
||||||
### Community 127 - "Cluster 127"
|
### Community 127 - "Cluster 127"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: soft delete (deleted_at)
|
Nodes (1): Decision: company default currency in settings_json
|
||||||
|
|
||||||
### Community 128 - "Cluster 128"
|
### Community 128 - "Cluster 128"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: numeric(18,4) + char(3) currency
|
Nodes (1): Decision: tabs = nested routes (not query-string)
|
||||||
|
|
||||||
### Community 129 - "Cluster 129"
|
### Community 129 - "Cluster 129"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: company default currency in settings_json
|
Nodes (0):
|
||||||
|
|
||||||
### Community 130 - "Cluster 130"
|
### Community 130 - "Cluster 130"
|
||||||
Cohesion: 1.0
|
Cohesion: 1.0
|
||||||
Nodes (1): Decision: tabs = nested routes (not query-string)
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 131 - "Cluster 131"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 132 - "Cluster 132"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 133 - "Cluster 133"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 134 - "Cluster 134"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 135 - "Cluster 135"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 136 - "Cluster 136"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
## Knowledge Gaps
|
## Knowledge Gaps
|
||||||
- **42 isolated node(s):** `buildfor_life_budget (sibling)`, `buildfor_life_repair (sibling)`, `PostgreSQL 16+ via Drizzle ORM + Zod`, `Argon2id sessions (@node-rs/argon2 + @oslojs/crypto)`, `Sharp image thumbnails` (+37 more)
|
- **73 isolated node(s):** `buildfor_life_budget (sibling)`, `buildfor_life_repair (sibling)`, `PostgreSQL 16+ via Drizzle ORM + Zod`, `Argon2id sessions (@node-rs/argon2 + @oslojs/crypto)`, `Sharp image thumbnails` (+68 more)
|
||||||
These have ≤1 connection - possible missing edges or undocumented components.
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
- **Thin community `Field Types`** (2 nodes): `needsEnumValues()`, `field-types.ts`
|
- **Thin community `Cluster 22`** (2 nodes): `needsEnumValues()`, `field-types.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Form Helper`** (2 nodes): `emptyToNull()`, `+page.server.ts`
|
- **Thin community `Cluster 23`** (2 nodes): `.env configuration`, `env.ts (Zod-validated process.env)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Env Config`** (2 nodes): `.env configuration`, `env.ts (Zod-validated process.env)`
|
- **Thin community `Cluster 24`** (2 nodes): `npm run create-user script`, `src/lib/server/db/schema/`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Seed Script`** (2 nodes): `npm run create-user script`, `src/lib/server/db/schema/`
|
- **Thin community `Cluster 25`** (2 nodes): `Decision: decisions scoped to project/property/asset/work_package`, `Phase 3: Projects + structured decisions`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Asset Location Design`** (2 nodes): `Decision: asset_location_history (movable assets)`, `Decision: XOR asset location (project XOR property)`
|
- **Thin community `Cluster 26`** (2 nodes): `Decision: asset_location_history (movable assets)`, `Decision: XOR asset location (project XOR property)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Decision Design`** (2 nodes): `Decision: decisions scoped to project/property/asset/work_package`, `Phase 3: Projects + structured decisions`
|
- **Thin community `Cluster 27`** (1 nodes): `drizzle.config.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Drizzle Config`** (1 nodes): `drizzle.config.ts`
|
- **Thin community `Cluster 28`** (1 nodes): `svelte.config.js`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 30`** (1 nodes): `svelte.config.js`
|
- **Thin community `Cluster 29`** (1 nodes): `vite.config.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 31`** (1 nodes): `vite.config.ts`
|
- **Thin community `Cluster 30`** (1 nodes): `app.d.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 32`** (1 nodes): `app.d.ts`
|
- **Thin community `Cluster 31`** (1 nodes): `accounts.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 33`** (1 nodes): `accounts.ts`
|
- **Thin community `Cluster 32`** (1 nodes): `notifications.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 34`** (1 nodes): `notifications.ts`
|
- **Thin community `Cluster 33`** (1 nodes): `roles.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 35`** (1 nodes): `roles.ts`
|
- **Thin community `Cluster 34`** (1 nodes): `CustomFieldsForm.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 36`** (1 nodes): `CustomFieldsForm.svelte`
|
- **Thin community `Cluster 35`** (1 nodes): `Sidebar.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 37`** (1 nodes): `Sidebar.svelte`
|
- **Thin community `Cluster 36`** (1 nodes): `TabNav.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 38`** (1 nodes): `TabNav.svelte`
|
- **Thin community `Cluster 37`** (1 nodes): `ThemeToggle.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 39`** (1 nodes): `ThemeToggle.svelte`
|
- **Thin community `Cluster 38`** (1 nodes): `TopBar.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 40`** (1 nodes): `TopBar.svelte`
|
- **Thin community `Cluster 39`** (1 nodes): `env.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 41`** (1 nodes): `env.ts`
|
- **Thin community `Cluster 40`** (1 nodes): `types.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 42`** (1 nodes): `types.ts`
|
- **Thin community `Cluster 41`** (1 nodes): `client.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 43`** (1 nodes): `client.ts`
|
- **Thin community `Cluster 42`** (1 nodes): `accounts.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 44`** (1 nodes): `accounts.ts`
|
- **Thin community `Cluster 43`** (1 nodes): `assets.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 45`** (1 nodes): `assets.ts`
|
- **Thin community `Cluster 44`** (1 nodes): `checklists.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 46`** (1 nodes): `checklists.ts`
|
- **Thin community `Cluster 45`** (1 nodes): `decisions.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 47`** (1 nodes): `decisions.ts`
|
- **Thin community `Cluster 46`** (1 nodes): `documents.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 48`** (1 nodes): `documents.ts`
|
- **Thin community `Cluster 47`** (1 nodes): `index.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 49`** (1 nodes): `index.ts`
|
- **Thin community `Cluster 48`** (1 nodes): `maintenance.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 50`** (1 nodes): `maintenance.ts`
|
- **Thin community `Cluster 49`** (1 nodes): `notifications.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 51`** (1 nodes): `notifications.ts`
|
- **Thin community `Cluster 50`** (1 nodes): `projects.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 52`** (1 nodes): `projects.ts`
|
- **Thin community `Cluster 51`** (1 nodes): `properties.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 53`** (1 nodes): `properties.ts`
|
- **Thin community `Cluster 52`** (1 nodes): `rooms.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 54`** (1 nodes): `rooms.ts`
|
- **Thin community `Cluster 53`** (1 nodes): `tenancy.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 55`** (1 nodes): `tenancy.ts`
|
- **Thin community `Cluster 54`** (1 nodes): `wiki.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 56`** (1 nodes): `wiki.ts`
|
- **Thin community `Cluster 55`** (1 nodes): `+error.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 57`** (1 nodes): `+error.svelte`
|
- **Thin community `Cluster 56`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 58`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 57`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 59`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 58`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 60`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 59`** (1 nodes): `+page.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 60`** (1 nodes): `+page.server.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 61`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 61`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 62`** (1 nodes): `+page.server.ts`
|
- **Thin community `Cluster 62`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 63`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 63`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 64`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 64`** (1 nodes): `+page.server.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 65`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 65`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 66`** (1 nodes): `+page.server.ts`
|
- **Thin community `Cluster 66`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 67`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 67`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -794,11 +826,11 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 69`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 69`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 70`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 70`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 71`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 71`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 72`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 72`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 73`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 73`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -824,11 +856,11 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 84`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 84`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 85`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 85`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 86`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 86`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 87`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 87`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 88`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 88`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -836,11 +868,11 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 90`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 90`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 91`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 91`** (1 nodes): `+page.server.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 92`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 92`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 93`** (1 nodes): `+page.server.ts`
|
- **Thin community `Cluster 93`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 94`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 94`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -858,11 +890,11 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 101`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 101`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 102`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 102`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 103`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 103`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 104`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 104`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 105`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 105`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -874,11 +906,11 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 109`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 109`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 110`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 110`** (1 nodes): `+page.server.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 111`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 111`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 112`** (1 nodes): `+page.server.ts`
|
- **Thin community `Cluster 112`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 113`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 113`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
@@ -886,51 +918,63 @@ Nodes (1): Decision: tabs = nested routes (not query-string)
|
|||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 115`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 115`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 116`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 116`** (1 nodes): `+layout.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 117`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 117`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 118`** (1 nodes): `+layout.svelte`
|
- **Thin community `Cluster 118`** (1 nodes): `npm run db:seed`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 119`** (1 nodes): `+page.svelte`
|
- **Thin community `Cluster 119`** (1 nodes): `npm run validate (check + build)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 120`** (1 nodes): `npm run db:seed`
|
- **Thin community `Cluster 120`** (1 nodes): `(auth) route group (login shell)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 121`** (1 nodes): `npm run validate (check + build)`
|
- **Thin community `Cluster 121`** (1 nodes): `Phase 0: scaffold (shipped)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 122`** (1 nodes): `(auth) route group (login shell)`
|
- **Thin community `Cluster 122`** (1 nodes): `Phase 2: Checklists + maintenance`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 123`** (1 nodes): `Phase 0: scaffold (shipped)`
|
- **Thin community `Cluster 123`** (1 nodes): `Decision: UUID v7 primary keys`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 124`** (1 nodes): `Phase 2: Checklists + maintenance`
|
- **Thin community `Cluster 124`** (1 nodes): `Decision: timestamptz UTC everywhere`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 125`** (1 nodes): `Decision: UUID v7 primary keys`
|
- **Thin community `Cluster 125`** (1 nodes): `Decision: soft delete (deleted_at)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 126`** (1 nodes): `Decision: timestamptz UTC everywhere`
|
- **Thin community `Cluster 126`** (1 nodes): `Decision: numeric(18,4) + char(3) currency`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 127`** (1 nodes): `Decision: soft delete (deleted_at)`
|
- **Thin community `Cluster 127`** (1 nodes): `Decision: company default currency in settings_json`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 128`** (1 nodes): `Decision: numeric(18,4) + char(3) currency`
|
- **Thin community `Cluster 128`** (1 nodes): `Decision: tabs = nested routes (not query-string)`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 129`** (1 nodes): `Decision: company default currency in settings_json`
|
- **Thin community `Cluster 129`** (1 nodes): `expenses.ts`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
- **Thin community `Cluster 130`** (1 nodes): `Decision: tabs = nested routes (not query-string)`
|
- **Thin community `Cluster 130`** (1 nodes): `ExpenseChart.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 131`** (1 nodes): `expenses.ts`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 132`** (1 nodes): `+page.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 133`** (1 nodes): `+page.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 134`** (1 nodes): `+page.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 135`** (1 nodes): `+page.svelte`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Cluster 136`** (1 nodes): `+page.svelte`
|
||||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
|
||||||
## Suggested Questions
|
## Suggested Questions
|
||||||
_Questions this graph is uniquely positioned to answer:_
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
- **Why does `load()` connect `Auth & Load Helpers` to `Documents Service`, `Assets Service & CSV`, `Email & Markdown`, `Property Accounts`, `Projects Service`, `Asset Core`, `Maintenance Core`, `Checklists`, `Rooms & Floors`, `Tasks`, `User Management`, `Work Packages`, `Companies Service`, `Form Utilities`?**
|
- **Why does `load()` connect `Page Server Loaders` to `Documents & Storage Adapters`, `Auth Sessions & Catalog`, `CSV & API Endpoints`, `Expenses Service`, `Email & Markdown Rendering`, `Projects Service`, `Bootstrap Scripts`, `Maintenance Schedules`, `Assets Service`, `Checklists Service`, `Rooms & Floors Service`, `Tasks & Subtasks`, `Work Packages`, `Companies Service`?**
|
||||||
_High betweenness centrality (0.298) - this node is a cross-community bridge._
|
_High betweenness centrality (0.308) - this node is a cross-community bridge._
|
||||||
- **Why does `GET()` connect `Assets Service & CSV` to `Auth & Load Helpers`, `Documents Service`, `Property Accounts`, `Asset Core`, `Maintenance Core`?**
|
- **Why does `Drizzle ORM + Zod` connect `Expenses Service` to `Page Server Loaders`, `Stack & Deployment Concepts`, `Projects Service`, `Bootstrap Scripts`, `Maintenance Schedules`, `Assets Service`, `Checklists Service`?**
|
||||||
_High betweenness centrality (0.120) - this node is a cross-community bridge._
|
_High betweenness centrality (0.173) - this node is a cross-community bridge._
|
||||||
- **Why does `listCompanyUsers()` connect `User Management` to `Auth & Load Helpers`?**
|
- **Why does `buildfor_life_ops` connect `Stack & Deployment Concepts` to `Expenses Service`?**
|
||||||
_High betweenness centrality (0.040) - this node is a cross-community bridge._
|
_High betweenness centrality (0.137) - this node is a cross-community bridge._
|
||||||
- **Are the 34 inferred relationships involving `load()` (e.g. with `countOverdueForCompany()` and `listDueAndOverdue()`) actually correct?**
|
- **Are the 41 inferred relationships involving `load()` (e.g. with `renderMarkdown()` and `requireCompany()`) actually correct?**
|
||||||
_`load()` has 34 INFERRED edges - model-reasoned connections that need verification._
|
_`load()` has 41 INFERRED edges - model-reasoned connections that need verification._
|
||||||
- **Are the 13 inferred relationships involving `GET()` (e.g. with `syncFieldDefs()` and `handle()`) actually correct?**
|
- **Are the 15 inferred relationships involving `GET()` (e.g. with `syncFieldDefs()` and `handle()`) actually correct?**
|
||||||
_`GET()` has 13 INFERRED edges - model-reasoned connections that need verification._
|
_`GET()` has 15 INFERRED edges - model-reasoned connections that need verification._
|
||||||
- **Are the 5 inferred relationships involving `load()` (e.g. with `setActiveCompany()` and `unreadCountForUser()`) actually correct?**
|
|
||||||
_`load()` has 5 INFERRED edges - model-reasoned connections that need verification._
|
|
||||||
- **What connects `buildfor_life_budget (sibling)`, `buildfor_life_repair (sibling)`, `PostgreSQL 16+ via Drizzle ORM + Zod` to the rest of the system?**
|
- **What connects `buildfor_life_budget (sibling)`, `buildfor_life_repair (sibling)`, `PostgreSQL 16+ via Drizzle ORM + Zod` to the rest of the system?**
|
||||||
_42 weakly-connected nodes found - possible documentation gaps or missing edges._
|
_73 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
|
- **Should `Page Server Loaders` be split into smaller, more focused modules?**
|
||||||
|
_Cohesion score 0.04 - nodes in this community are weakly interconnected._
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_assets_id_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\assets\\[id]\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_assets_id_maintenance_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\assets\\[id]\\maintenance\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_assets_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L6"}], "edges": [{"source": "src_routes_app_properties_id_assets_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_assets_page_server_ts", "target": "assets", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_assets_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_assets_page_server_ts", "target": "src_routes_app_properties_id_assets_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_id_assets_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L7"}, {"caller_nid": "page_server_load", "callee": "get", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L8"}, {"caller_nid": "page_server_load", "callee": "getDescendantIds", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L10"}, {"caller_nid": "page_server_load", "callee": "listAssets", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts", "source_location": "L12"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_projects_id_decisions_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\projects\\[id]\\decisions\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_e2n", "label": "e2n()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L26"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L28"}], "edges": [{"source": "src_routes_app_properties_id_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "drizzle_orm", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "zod", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "client", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L5", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L6", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "src_routes_app_properties_id_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L11", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "page_server_e2n", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L26", "weight": 1.0}, {"source": "src_routes_app_properties_id_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L28", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L29"}, {"caller_nid": "page_server_load", "callee": "getDescendantIds", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L34"}, {"caller_nid": "page_server_load", "callee": "orderBy", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L35"}, {"caller_nid": "page_server_load", "callee": "where", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L35"}, {"caller_nid": "page_server_load", "callee": "from", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L35"}, {"caller_nid": "page_server_load", "callee": "select", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L35"}, {"caller_nid": "page_server_load", "callee": "and", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L39"}, {"caller_nid": "page_server_load", "callee": "eq", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L40"}, {"caller_nid": "page_server_load", "callee": "isNull", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L41"}, {"caller_nid": "page_server_load", "callee": "ne", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L42"}, {"caller_nid": "page_server_load", "callee": "notInArray", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.server.ts", "source_location": "L43"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_expenses_import_template_csv_server_ts", "label": "+server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L1"}, {"id": "server_get", "label": "GET()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L7"}], "edges": [{"source": "src_routes_app_properties_id_expenses_import_template_csv_server_ts", "target": "guards", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_template_csv_server_ts", "target": "csv", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_template_csv_server_ts", "target": "src_routes_app_properties_id_expenses_import_template_csv_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_template_csv_server_ts", "target": "server_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L7", "weight": 1.0}], "raw_calls": [{"caller_nid": "server_get", "callee": "requireCompany", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L8"}, {"caller_nid": "server_get", "callee": "slice", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L9"}, {"caller_nid": "server_get", "callee": "toISOString", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L9"}, {"caller_nid": "server_get", "callee": "toCsv", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L10"}, {"caller_nid": "server_get", "callee": "csvResponse", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts", "source_location": "L47"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_assets_new_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\assets\\new\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_server_db_schema_expenses_ts", "label": "expenses.ts", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L1"}], "edges": [{"source": "src_lib_server_db_schema_expenses_ts", "target": "pg_core", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_lib_server_db_schema_expenses_ts", "target": "src_lib_server_db_schema_properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_lib_server_db_schema_expenses_ts", "target": "src_lib_server_db_schema_accounts", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_lib_server_db_schema_expenses_ts", "target": "src_lib_server_db_schema_tenancy", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_lib_server_db_schema_expenses_ts", "target": "src_lib_server_db_schema_shared", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\expenses.ts", "source_location": "L5", "weight": 1.0}], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_todos_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L6"}], "edges": [{"source": "src_routes_app_properties_id_todos_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_todos_page_server_ts", "target": "checklists", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_todos_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_todos_page_server_ts", "target": "src_routes_app_properties_id_todos_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_id_todos_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L7"}, {"caller_nid": "page_server_load", "callee": "get", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L8"}, {"caller_nid": "page_server_load", "callee": "getDescendantIds", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L10"}, {"caller_nid": "page_server_load", "callee": "listInstancesForProperties", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts", "source_location": "L12"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_server_db_schema_shared_ts", "label": "_shared.ts", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L1"}, {"id": "shared_pk", "label": "pk()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98"}, {"id": "shared_fk", "label": "fk()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L99"}, {"id": "shared_createdat", "label": "createdAt()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L100"}, {"id": "shared_updatedat", "label": "updatedAt()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L102"}, {"id": "shared_deletedat", "label": "deletedAt()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L104"}, {"id": "shared_slugcol", "label": "slugCol()", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L105"}], "edges": [{"source": "src_lib_server_db_schema_shared_ts", "target": "drizzle_orm", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "pg_core", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_pk", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_fk", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L99", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_createdat", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L100", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_updatedat", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L102", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_deletedat", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L104", "weight": 1.0}, {"source": "src_lib_server_db_schema_shared_ts", "target": "shared_slugcol", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L105", "weight": 1.0}], "raw_calls": [{"caller_nid": "shared_pk", "callee": "default", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98"}, {"caller_nid": "shared_pk", "callee": "primaryKey", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98"}, {"caller_nid": "shared_pk", "callee": "uuid", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98"}, {"caller_nid": "shared_pk", "callee": "sql", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L98"}, {"caller_nid": "shared_fk", "callee": "uuid", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L99"}, {"caller_nid": "shared_createdat", "callee": "defaultNow", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L101"}, {"caller_nid": "shared_createdat", "callee": "notNull", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L101"}, {"caller_nid": "shared_createdat", "callee": "timestamp", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L101"}, {"caller_nid": "shared_updatedat", "callee": "defaultNow", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L103"}, {"caller_nid": "shared_updatedat", "callee": "notNull", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L103"}, {"caller_nid": "shared_updatedat", "callee": "timestamp", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L103"}, {"caller_nid": "shared_deletedat", "callee": "timestamp", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L104"}, {"caller_nid": "shared_slugcol", "callee": "notNull", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L105"}, {"caller_nid": "shared_slugcol", "callee": "varchar", "source_file": "src\\lib\\server\\db\\schema\\_shared.ts", "source_location": "L105"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_projects_id_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\projects\\[id]\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_server_db_schema_properties_ts", "label": "properties.ts", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\properties.ts", "source_location": "L1"}], "edges": [{"source": "src_lib_server_db_schema_properties_ts", "target": "pg_core", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\properties.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_lib_server_db_schema_properties_ts", "target": "pg_core", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\properties.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_lib_server_db_schema_properties_ts", "target": "src_lib_server_db_schema_tenancy", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\properties.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_lib_server_db_schema_properties_ts", "target": "src_lib_server_db_schema_shared", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\db\\schema\\properties.ts", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_maintenance_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L9"}], "edges": [{"source": "src_routes_app_properties_id_maintenance_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_maintenance_page_server_ts", "target": "maintenance", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_maintenance_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L6", "weight": 1.0}, {"source": "src_routes_app_properties_id_maintenance_page_server_ts", "target": "src_routes_app_properties_id_maintenance_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L7", "weight": 1.0}, {"source": "src_routes_app_properties_id_maintenance_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L10"}, {"caller_nid": "page_server_load", "callee": "get", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L11"}, {"caller_nid": "page_server_load", "callee": "getDescendantIds", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L13"}, {"caller_nid": "page_server_load", "callee": "all", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L15"}, {"caller_nid": "page_server_load", "callee": "listSchedulesForProperties", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L16"}, {"caller_nid": "page_server_load", "callee": "listEventsForProperties", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts", "source_location": "L17"}]}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_assets_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\assets\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_new_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_emptytonull", "label": "emptyToNull()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L22"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L26"}], "edges": [{"source": "src_routes_app_properties_new_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "drizzle_orm", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "zod", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "client", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L5", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L6", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "src_routes_app_properties_new_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L7", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "page_server_emptytonull", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L22", "weight": 1.0}, {"source": "src_routes_app_properties_new_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L26", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L27"}, {"caller_nid": "page_server_load", "callee": "orderBy", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L28"}, {"caller_nid": "page_server_load", "callee": "where", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L28"}, {"caller_nid": "page_server_load", "callee": "from", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L28"}, {"caller_nid": "page_server_load", "callee": "select", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L28"}, {"caller_nid": "page_server_load", "callee": "and", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L31"}, {"caller_nid": "page_server_load", "callee": "eq", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L31"}, {"caller_nid": "page_server_load", "callee": "isNull", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L31"}, {"caller_nid": "page_server_load", "callee": "get", "source_file": "src\\routes\\(app)\\properties\\new\\+page.server.ts", "source_location": "L33"}]}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_expenses_ts", "label": "expenses.ts", "file_type": "code", "source_file": "src\\lib\\expenses.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_server_csv_parse_ts", "label": "csv-parse.ts", "file_type": "code", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L1"}, {"id": "csv_parse_parsecsv", "label": "parseCsv()", "file_type": "code", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L17"}, {"id": "csv_parse_parsecsvdict", "label": "parseCsvDict()", "file_type": "code", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L88"}], "edges": [{"source": "src_lib_server_csv_parse_ts", "target": "csv_parse_parsecsv", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L17", "weight": 1.0}, {"source": "src_lib_server_csv_parse_ts", "target": "csv_parse_parsecsvdict", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L88", "weight": 1.0}, {"source": "csv_parse_parsecsvdict", "target": "csv_parse_parsecsv", "relation": "calls", "confidence": "EXTRACTED", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L89", "weight": 1.0}], "raw_calls": [{"caller_nid": "csv_parse_parsecsv", "callee": "charCodeAt", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L19"}, {"caller_nid": "csv_parse_parsecsv", "callee": "slice", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L19"}, {"caller_nid": "csv_parse_parsecsv", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L56"}, {"caller_nid": "csv_parse_parsecsv", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L62"}, {"caller_nid": "csv_parse_parsecsv", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L68"}, {"caller_nid": "csv_parse_parsecsv", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L77"}, {"caller_nid": "csv_parse_parsecsv", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L78"}, {"caller_nid": "csv_parse_parsecsvdict", "callee": "map", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L91"}, {"caller_nid": "csv_parse_parsecsvdict", "callee": "push", "source_file": "src\\lib\\server\\csv-parse.ts", "source_location": "L102"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_expenses_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_sub_properties_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L7"}], "edges": [{"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "drizzle_orm", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "client", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "src_routes_app_properties_id_sub_properties_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L5", "weight": 1.0}, {"source": "src_routes_app_properties_id_sub_properties_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L7", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L8"}, {"caller_nid": "page_server_load", "callee": "orderBy", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L9"}, {"caller_nid": "page_server_load", "callee": "where", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L9"}, {"caller_nid": "page_server_load", "callee": "from", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L9"}, {"caller_nid": "page_server_load", "callee": "select", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L9"}, {"caller_nid": "page_server_load", "callee": "and", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L19"}, {"caller_nid": "page_server_load", "callee": "eq", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L20"}, {"caller_nid": "page_server_load", "callee": "eq", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L21"}, {"caller_nid": "page_server_load", "callee": "isNull", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L22"}, {"caller_nid": "page_server_load", "callee": "asc", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts", "source_location": "L25"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_server_db_schema_index_ts", "label": "index.ts", "file_type": "code", "source_file": "src\\lib\\server\\db\\schema\\index.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_projects_id_work_wpid_taskid_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\[taskId]\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_projects_new_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\projects\\new\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_maintenance_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_new_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\new\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_lib_components_expensechart_svelte", "label": "ExpenseChart.svelte", "file_type": "code", "source_file": "src\\lib\\components\\ExpenseChart.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_expenses_import_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_layout_svelte", "label": "+layout.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\+layout.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_flattentree", "label": "flattenTree()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L15"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L49"}], "edges": [{"source": "src_routes_app_properties_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_page_server_ts", "target": "properties", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_page_server_ts", "target": "src_routes_app_properties_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_page_server_ts", "target": "page_server_flattentree", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L15", "weight": 1.0}, {"source": "src_routes_app_properties_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L49", "weight": 1.0}, {"source": "page_server_load", "target": "page_server_flattentree", "relation": "calls", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L52", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_flattentree", "callee": "get", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L19"}, {"caller_nid": "page_server_flattentree", "callee": "push", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L20"}, {"caller_nid": "page_server_flattentree", "callee": "set", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L21"}, {"caller_nid": "page_server_flattentree", "callee": "values", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L23"}, {"caller_nid": "page_server_flattentree", "callee": "sort", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L24"}, {"caller_nid": "page_server_flattentree", "callee": "map", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L28"}, {"caller_nid": "page_server_flattentree", "callee": "walk", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L36"}, {"caller_nid": "page_server_flattentree", "callee": "map", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L39"}, {"caller_nid": "page_server_flattentree", "callee": "has", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L41"}, {"caller_nid": "page_server_flattentree", "callee": "has", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L41"}, {"caller_nid": "page_server_flattentree", "callee": "push", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L42"}, {"caller_nid": "page_server_load", "callee": "error", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L50"}, {"caller_nid": "page_server_load", "callee": "listProperties", "source_file": "src\\routes\\(app)\\properties\\+page.server.ts", "source_location": "L51"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_sub_properties_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_todos_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\todos\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_properties_id_expenses_import_page_server_ts", "label": "+page.server.ts", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L1"}, {"id": "page_server_parsesettings", "label": "parseSettings()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L15"}, {"id": "page_server_load", "label": "load()", "file_type": "code", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L24"}], "edges": [{"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "kit", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L1", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "drizzle_orm", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L2", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "guards", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L3", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "client", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L4", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "tenancy", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L5", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "csv_parse", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L6", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "expenses", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L7", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "src_routes_app_properties_id_expenses_import_types", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L8", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "page_server_parsesettings", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L15", "weight": 1.0}, {"source": "src_routes_app_properties_id_expenses_import_page_server_ts", "target": "page_server_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L24", "weight": 1.0}, {"source": "page_server_load", "target": "page_server_parsesettings", "relation": "calls", "confidence": "EXTRACTED", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L32", "weight": 1.0}], "raw_calls": [{"caller_nid": "page_server_parsesettings", "callee": "parse", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L18"}, {"caller_nid": "page_server_load", "callee": "requireCompany", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L25"}, {"caller_nid": "page_server_load", "callee": "limit", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L26"}, {"caller_nid": "page_server_load", "callee": "where", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L26"}, {"caller_nid": "page_server_load", "callee": "from", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L26"}, {"caller_nid": "page_server_load", "callee": "select", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L26"}, {"caller_nid": "page_server_load", "callee": "eq", "source_file": "src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts", "source_location": "L29"}]}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "src_routes_app_projects_id_work_wpid_page_svelte", "label": "+page.svelte", "file_type": "code", "source_file": "src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\+page.svelte", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
@@ -5,6 +5,12 @@
|
|||||||
"input_tokens": 0,
|
"input_tokens": 0,
|
||||||
"output_tokens": 0,
|
"output_tokens": 0,
|
||||||
"files": 189
|
"files": 189
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-04-27T08:46:41.997113+00:00",
|
||||||
|
"input_tokens": 0,
|
||||||
|
"output_tokens": 0,
|
||||||
|
"files": 47
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_input_tokens": 0,
|
"total_input_tokens": 0,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+5000
-2074
File diff suppressed because it is too large
Load Diff
+47
-28
@@ -2,20 +2,23 @@
|
|||||||
"drizzle.config.ts": 1776759714.513955,
|
"drizzle.config.ts": 1776759714.513955,
|
||||||
"svelte.config.js": 1776759709.852201,
|
"svelte.config.js": 1776759709.852201,
|
||||||
"vite.config.ts": 1776759710.9294431,
|
"vite.config.ts": 1776759710.9294431,
|
||||||
"scripts\\create-user.ts": 1776764431.9558957,
|
"scripts\\create-user.ts": 1777018112.65223,
|
||||||
"scripts\\diag-user.ts": 1776764326.9098525,
|
"scripts\\diag-user.ts": 1776764326.9098525,
|
||||||
"scripts\\seed\\system-asset-types.ts": 1776912939.322366,
|
"scripts\\seed\\system-asset-types.ts": 1776912939.322366,
|
||||||
"src\\app.d.ts": 1776759729.3791924,
|
"src\\app.d.ts": 1776759729.3791924,
|
||||||
"src\\hooks.server.ts": 1776759818.2697544,
|
"src\\hooks.server.ts": 1776759818.2697544,
|
||||||
"src\\lib\\accounts.ts": 1776920290.934606,
|
"src\\lib\\accounts.ts": 1776920290.934606,
|
||||||
|
"src\\lib\\expenses.ts": 1776932953.1379492,
|
||||||
"src\\lib\\field-types.ts": 1776920825.6922204,
|
"src\\lib\\field-types.ts": 1776920825.6922204,
|
||||||
"src\\lib\\notifications.ts": 1776931229.9228654,
|
"src\\lib\\notifications.ts": 1776931229.9228654,
|
||||||
"src\\lib\\roles.ts": 1776926943.3422728,
|
"src\\lib\\roles.ts": 1776926943.3422728,
|
||||||
"src\\lib\\components\\CustomFieldsForm.svelte": 1776913252.4056394,
|
"src\\lib\\components\\CustomFieldsForm.svelte": 1776913252.4056394,
|
||||||
|
"src\\lib\\components\\ExpenseChart.svelte": 1776933040.9983582,
|
||||||
"src\\lib\\components\\Sidebar.svelte": 1776927136.3547218,
|
"src\\lib\\components\\Sidebar.svelte": 1776927136.3547218,
|
||||||
"src\\lib\\components\\TabNav.svelte": 1776913159.6860654,
|
"src\\lib\\components\\TabNav.svelte": 1776913159.6860654,
|
||||||
"src\\lib\\components\\ThemeToggle.svelte": 1776759845.5468612,
|
"src\\lib\\components\\ThemeToggle.svelte": 1776759845.5468612,
|
||||||
"src\\lib\\components\\TopBar.svelte": 1776931220.6103387,
|
"src\\lib\\components\\TopBar.svelte": 1776931220.6103387,
|
||||||
|
"src\\lib\\server\\csv-parse.ts": 1776934140.7087996,
|
||||||
"src\\lib\\server\\csv.ts": 1776917156.6442757,
|
"src\\lib\\server\\csv.ts": 1776917156.6442757,
|
||||||
"src\\lib\\server\\custom-fields-form.ts": 1776913256.9887655,
|
"src\\lib\\server\\custom-fields-form.ts": 1776913256.9887655,
|
||||||
"src\\lib\\server\\env.ts": 1776931062.8713133,
|
"src\\lib\\server\\env.ts": 1776931062.8713133,
|
||||||
@@ -29,28 +32,30 @@
|
|||||||
"src\\lib\\server\\db\\schema\\checklists.ts": 1776913879.7155764,
|
"src\\lib\\server\\db\\schema\\checklists.ts": 1776913879.7155764,
|
||||||
"src\\lib\\server\\db\\schema\\decisions.ts": 1776915253.4674976,
|
"src\\lib\\server\\db\\schema\\decisions.ts": 1776915253.4674976,
|
||||||
"src\\lib\\server\\db\\schema\\documents.ts": 1776912778.9190943,
|
"src\\lib\\server\\db\\schema\\documents.ts": 1776912778.9190943,
|
||||||
"src\\lib\\server\\db\\schema\\index.ts": 1776930971.0629141,
|
"src\\lib\\server\\db\\schema\\expenses.ts": 1776932803.6511443,
|
||||||
|
"src\\lib\\server\\db\\schema\\index.ts": 1776932816.3969572,
|
||||||
"src\\lib\\server\\db\\schema\\maintenance.ts": 1776913892.3164668,
|
"src\\lib\\server\\db\\schema\\maintenance.ts": 1776913892.3164668,
|
||||||
"src\\lib\\server\\db\\schema\\notifications.ts": 1776930892.1852467,
|
"src\\lib\\server\\db\\schema\\notifications.ts": 1776930892.1852467,
|
||||||
"src\\lib\\server\\db\\schema\\projects.ts": 1776915246.2953787,
|
"src\\lib\\server\\db\\schema\\projects.ts": 1776915246.2953787,
|
||||||
"src\\lib\\server\\db\\schema\\properties.ts": 1776912743.139987,
|
"src\\lib\\server\\db\\schema\\properties.ts": 1777268843.2618184,
|
||||||
"src\\lib\\server\\db\\schema\\rooms.ts": 1776918599.0392416,
|
"src\\lib\\server\\db\\schema\\rooms.ts": 1776918599.0392416,
|
||||||
"src\\lib\\server\\db\\schema\\tenancy.ts": 1776930946.3660817,
|
"src\\lib\\server\\db\\schema\\tenancy.ts": 1776930946.3660817,
|
||||||
"src\\lib\\server\\db\\schema\\wiki.ts": 1776916193.8209262,
|
"src\\lib\\server\\db\\schema\\wiki.ts": 1776916193.8209262,
|
||||||
"src\\lib\\server\\db\\schema\\_shared.ts": 1776930905.6758077,
|
"src\\lib\\server\\db\\schema\\_shared.ts": 1777268980.6436405,
|
||||||
"src\\lib\\server\\notifications\\email.ts": 1776931016.4405794,
|
"src\\lib\\server\\notifications\\email.ts": 1776931016.4405794,
|
||||||
"src\\lib\\server\\notifications\\matrix.ts": 1776931029.1186867,
|
"src\\lib\\server\\notifications\\matrix.ts": 1776931029.1186867,
|
||||||
"src\\lib\\server\\services\\accounts.ts": 1776920323.303991,
|
"src\\lib\\server\\services\\accounts.ts": 1776920323.303991,
|
||||||
"src\\lib\\server\\services\\asset-types.ts": 1776920794.8900447,
|
"src\\lib\\server\\services\\asset-types.ts": 1776920794.8900447,
|
||||||
"src\\lib\\server\\services\\assets.ts": 1776918741.5526845,
|
"src\\lib\\server\\services\\assets.ts": 1777269430.577926,
|
||||||
"src\\lib\\server\\services\\checklists.ts": 1776914015.1864648,
|
"src\\lib\\server\\services\\checklists.ts": 1777269092.1071846,
|
||||||
"src\\lib\\server\\services\\companies.ts": 1776926919.6478693,
|
"src\\lib\\server\\services\\companies.ts": 1776926919.6478693,
|
||||||
"src\\lib\\server\\services\\decisions.ts": 1776931161.1573675,
|
"src\\lib\\server\\services\\decisions.ts": 1776931161.1573675,
|
||||||
"src\\lib\\server\\services\\documents.ts": 1776913042.152006,
|
"src\\lib\\server\\services\\documents.ts": 1776913042.152006,
|
||||||
"src\\lib\\server\\services\\maintenance.ts": 1776914056.7123244,
|
"src\\lib\\server\\services\\expenses.ts": 1777269052.5917537,
|
||||||
|
"src\\lib\\server\\services\\maintenance.ts": 1777269075.0133095,
|
||||||
"src\\lib\\server\\services\\notifications.ts": 1776931111.3630683,
|
"src\\lib\\server\\services\\notifications.ts": 1776931111.3630683,
|
||||||
"src\\lib\\server\\services\\projects.ts": 1776915354.1029918,
|
"src\\lib\\server\\services\\projects.ts": 1776915354.1029918,
|
||||||
"src\\lib\\server\\services\\properties.ts": 1776913017.5585654,
|
"src\\lib\\server\\services\\properties.ts": 1777278959.9801009,
|
||||||
"src\\lib\\server\\services\\rooms.ts": 1776918691.0627687,
|
"src\\lib\\server\\services\\rooms.ts": 1776918691.0627687,
|
||||||
"src\\lib\\server\\services\\tasks.ts": 1776931148.6006575,
|
"src\\lib\\server\\services\\tasks.ts": 1776931148.6006575,
|
||||||
"src\\lib\\server\\services\\users.ts": 1776926913.1441553,
|
"src\\lib\\server\\services\\users.ts": 1776926913.1441553,
|
||||||
@@ -86,11 +91,11 @@
|
|||||||
"src\\routes\\(app)\\assets\\+page.svelte": 1776917178.3600802,
|
"src\\routes\\(app)\\assets\\+page.svelte": 1776917178.3600802,
|
||||||
"src\\routes\\(app)\\assets\\export.csv\\+server.ts": 1776917162.3927114,
|
"src\\routes\\(app)\\assets\\export.csv\\+server.ts": 1776917162.3927114,
|
||||||
"src\\routes\\(app)\\assets\\new\\+page.server.ts": 1776918891.8736053,
|
"src\\routes\\(app)\\assets\\new\\+page.server.ts": 1776918891.8736053,
|
||||||
"src\\routes\\(app)\\assets\\new\\+page.svelte": 1776918929.98384,
|
"src\\routes\\(app)\\assets\\new\\+page.svelte": 1776933848.5311816,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\+layout.server.ts": 1776918845.937803,
|
"src\\routes\\(app)\\assets\\[id]\\+layout.server.ts": 1776918845.937803,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\+layout.svelte": 1776918861.8613749,
|
"src\\routes\\(app)\\assets\\[id]\\+layout.svelte": 1776918861.8613749,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\+page.server.ts": 1776918966.9773984,
|
"src\\routes\\(app)\\assets\\[id]\\+page.server.ts": 1776918966.9773984,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\+page.svelte": 1776918977.7803142,
|
"src\\routes\\(app)\\assets\\[id]\\+page.svelte": 1776933888.6220212,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\documents\\+page.server.ts": 1776913388.3625875,
|
"src\\routes\\(app)\\assets\\[id]\\documents\\+page.server.ts": 1776913388.3625875,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\documents\\+page.svelte": 1776913399.9952705,
|
"src\\routes\\(app)\\assets\\[id]\\documents\\+page.svelte": 1776913399.9952705,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\history\\+page.server.ts": 1776913363.7884815,
|
"src\\routes\\(app)\\assets\\[id]\\history\\+page.server.ts": 1776913363.7884815,
|
||||||
@@ -100,7 +105,7 @@
|
|||||||
"src\\routes\\(app)\\assets\\[id]\\logs\\+page.server.ts": 1776913374.1819277,
|
"src\\routes\\(app)\\assets\\[id]\\logs\\+page.server.ts": 1776913374.1819277,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\logs\\+page.svelte": 1776913381.2650573,
|
"src\\routes\\(app)\\assets\\[id]\\logs\\+page.svelte": 1776913381.2650573,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\maintenance\\+page.server.ts": 1776918070.6355166,
|
"src\\routes\\(app)\\assets\\[id]\\maintenance\\+page.server.ts": 1776918070.6355166,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\maintenance\\+page.svelte": 1776914405.4364533,
|
"src\\routes\\(app)\\assets\\[id]\\maintenance\\+page.svelte": 1776933855.15931,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\maintenance\\events\\[eventId]\\+page.server.ts": 1776914228.1214633,
|
"src\\routes\\(app)\\assets\\[id]\\maintenance\\events\\[eventId]\\+page.server.ts": 1776914228.1214633,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\maintenance\\events\\[eventId]\\+page.svelte": 1776914244.2005274,
|
"src\\routes\\(app)\\assets\\[id]\\maintenance\\events\\[eventId]\\+page.svelte": 1776914244.2005274,
|
||||||
"src\\routes\\(app)\\assets\\[id]\\move\\+page.server.ts": 1776919009.9313874,
|
"src\\routes\\(app)\\assets\\[id]\\move\\+page.server.ts": 1776919009.9313874,
|
||||||
@@ -117,15 +122,15 @@
|
|||||||
"src\\routes\\(app)\\projects\\+page.server.ts": 1776915423.3344169,
|
"src\\routes\\(app)\\projects\\+page.server.ts": 1776915423.3344169,
|
||||||
"src\\routes\\(app)\\projects\\+page.svelte": 1776915434.5388634,
|
"src\\routes\\(app)\\projects\\+page.svelte": 1776915434.5388634,
|
||||||
"src\\routes\\(app)\\projects\\new\\+page.server.ts": 1776918064.9852977,
|
"src\\routes\\(app)\\projects\\new\\+page.server.ts": 1776918064.9852977,
|
||||||
"src\\routes\\(app)\\projects\\new\\+page.svelte": 1776915457.2821925,
|
"src\\routes\\(app)\\projects\\new\\+page.svelte": 1776933886.386777,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\+layout.server.ts": 1776915464.0169995,
|
"src\\routes\\(app)\\projects\\[id]\\+layout.server.ts": 1776915464.0169995,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\+layout.svelte": 1776916623.3352191,
|
"src\\routes\\(app)\\projects\\[id]\\+layout.svelte": 1776916623.3352191,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\+page.server.ts": 1776915476.949084,
|
"src\\routes\\(app)\\projects\\[id]\\+page.server.ts": 1776915476.949084,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\+page.svelte": 1776915496.4709525,
|
"src\\routes\\(app)\\projects\\[id]\\+page.svelte": 1776933887.4594047,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\assets\\+page.server.ts": 1776915627.8771396,
|
"src\\routes\\(app)\\projects\\[id]\\assets\\+page.server.ts": 1776915627.8771396,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\assets\\+page.svelte": 1776915636.780885,
|
"src\\routes\\(app)\\projects\\[id]\\assets\\+page.svelte": 1776915636.780885,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\decisions\\+page.server.ts": 1776915599.6791656,
|
"src\\routes\\(app)\\projects\\[id]\\decisions\\+page.server.ts": 1776915599.6791656,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\decisions\\+page.svelte": 1776917192.7825646,
|
"src\\routes\\(app)\\projects\\[id]\\decisions\\+page.svelte": 1776933891.6662703,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\decisions\\export.csv\\+server.ts": 1776917170.5648644,
|
"src\\routes\\(app)\\projects\\[id]\\decisions\\export.csv\\+server.ts": 1776917170.5648644,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\documents\\+page.server.ts": 1776915643.2300684,
|
"src\\routes\\(app)\\projects\\[id]\\documents\\+page.server.ts": 1776915643.2300684,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\documents\\+page.svelte": 1776915654.9358807,
|
"src\\routes\\(app)\\projects\\[id]\\documents\\+page.svelte": 1776915654.9358807,
|
||||||
@@ -144,25 +149,36 @@
|
|||||||
"src\\routes\\(app)\\projects\\[id]\\work\\+page.server.ts": 1776915505.82269,
|
"src\\routes\\(app)\\projects\\[id]\\work\\+page.server.ts": 1776915505.82269,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\work\\+page.svelte": 1776915517.9951062,
|
"src\\routes\\(app)\\projects\\[id]\\work\\+page.svelte": 1776915517.9951062,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\+page.server.ts": 1776915525.877059,
|
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\+page.server.ts": 1776915525.877059,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\+page.svelte": 1776919721.2087197,
|
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\+page.svelte": 1776933889.7585225,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\[taskId]\\+page.server.ts": 1776915557.2259672,
|
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\[taskId]\\+page.server.ts": 1776915557.2259672,
|
||||||
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\[taskId]\\+page.svelte": 1776919694.229038,
|
"src\\routes\\(app)\\projects\\[id]\\work\\[wpId]\\[taskId]\\+page.svelte": 1776933890.5722253,
|
||||||
"src\\routes\\(app)\\properties\\+page.server.ts": 1776913103.3089087,
|
"src\\routes\\(app)\\properties\\+page.server.ts": 1777276929.1625655,
|
||||||
"src\\routes\\(app)\\properties\\+page.svelte": 1776913114.269045,
|
"src\\routes\\(app)\\properties\\+page.svelte": 1777276938.2817636,
|
||||||
"src\\routes\\(app)\\properties\\new\\+page.server.ts": 1776913120.8220265,
|
"src\\routes\\(app)\\properties\\new\\+page.server.ts": 1777269213.3164072,
|
||||||
"src\\routes\\(app)\\properties\\new\\+page.svelte": 1776913139.3366928,
|
"src\\routes\\(app)\\properties\\new\\+page.svelte": 1777269235.010823,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\+layout.server.ts": 1776913161.8158467,
|
"src\\routes\\(app)\\properties\\[id]\\+layout.server.ts": 1777269158.567569,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\+layout.svelte": 1776919919.0561438,
|
"src\\routes\\(app)\\properties\\[id]\\+layout.svelte": 1777269527.4078188,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\+page.server.ts": 1776913174.1071742,
|
"src\\routes\\(app)\\properties\\[id]\\+page.server.ts": 1777269189.052704,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\+page.svelte": 1776913195.5002894,
|
"src\\routes\\(app)\\properties\\[id]\\+page.svelte": 1777269198.207656,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\accounts\\+page.server.ts": 1776919929.527811,
|
"src\\routes\\(app)\\properties\\[id]\\accounts\\+page.server.ts": 1776919929.527811,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\accounts\\+page.svelte": 1776920324.8604333,
|
"src\\routes\\(app)\\properties\\[id]\\accounts\\+page.svelte": 1776920324.8604333,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts": 1776913196.9792116,
|
"src\\routes\\(app)\\properties\\[id]\\assets\\+page.server.ts": 1777269436.916948,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\assets\\+page.svelte": 1776919046.6825135,
|
"src\\routes\\(app)\\properties\\[id]\\assets\\+page.svelte": 1777269466.5592797,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\documents\\+page.server.ts": 1776913212.7782526,
|
"src\\routes\\(app)\\properties\\[id]\\documents\\+page.server.ts": 1776913212.7782526,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\documents\\+page.svelte": 1776913224.460486,
|
"src\\routes\\(app)\\properties\\[id]\\documents\\+page.svelte": 1776913224.460486,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\expenses\\+page.server.ts": 1777269312.9716668,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\expenses\\+page.svelte": 1777269370.880504,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.server.ts": 1776934262.3544624,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\expenses\\import\\+page.svelte": 1776934221.4350948,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\expenses\\import\\template.csv\\+server.ts": 1776934226.9037814,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.server.ts": 1777269483.5094693,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\maintenance\\+page.svelte": 1777269504.9545746,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\rooms\\+page.server.ts": 1776918787.5071964,
|
"src\\routes\\(app)\\properties\\[id]\\rooms\\+page.server.ts": 1776918787.5071964,
|
||||||
"src\\routes\\(app)\\properties\\[id]\\rooms\\+page.svelte": 1776919689.9999452,
|
"src\\routes\\(app)\\properties\\[id]\\rooms\\+page.svelte": 1776919689.9999452,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.server.ts": 1777269241.9622283,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\sub-properties\\+page.svelte": 1777269248.938543,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\todos\\+page.server.ts": 1777269507.4355745,
|
||||||
|
"src\\routes\\(app)\\properties\\[id]\\todos\\+page.svelte": 1777269519.2786589,
|
||||||
"src\\routes\\(app)\\settings\\notifications\\+page.server.ts": 1776931253.4051654,
|
"src\\routes\\(app)\\settings\\notifications\\+page.server.ts": 1776931253.4051654,
|
||||||
"src\\routes\\(app)\\settings\\notifications\\+page.svelte": 1776931270.4663363,
|
"src\\routes\\(app)\\settings\\notifications\\+page.svelte": 1776931270.4663363,
|
||||||
"src\\routes\\(app)\\wiki\\+page.server.ts": 1776916429.1431959,
|
"src\\routes\\(app)\\wiki\\+page.server.ts": 1776916429.1431959,
|
||||||
@@ -185,7 +201,10 @@
|
|||||||
"src\\routes\\api\\qr\\+server.ts": 1776917032.1335907,
|
"src\\routes\\api\\qr\\+server.ts": 1776917032.1335907,
|
||||||
"src\\routes\\logout\\+server.ts": 1776760388.366003,
|
"src\\routes\\logout\\+server.ts": 1776760388.366003,
|
||||||
"src\\routes\\switch-company\\+server.ts": 1776914979.4505768,
|
"src\\routes\\switch-company\\+server.ts": 1776914979.4505768,
|
||||||
"README.md": 1776761445.7901409,
|
"DEPLOYMENT.md": 1777262556.380254,
|
||||||
"drizzle\\README.md": 1776759950.7471619,
|
"README.md": 1777018156.956704,
|
||||||
|
"drizzle\\README.md": 1777018117.6299796,
|
||||||
|
"graphify-out\\graph.html": 1777279589.8841953,
|
||||||
|
"graphify-out\\GRAPH_REPORT.md": 1777279513.4607954,
|
||||||
"src\\app.html": 1776759722.8929892
|
"src\\app.html": 1776759722.8929892
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:seed": "tsx scripts/seed/system-asset-types.ts",
|
"db:seed": "tsx scripts/seed/system-asset-types.ts",
|
||||||
"create-user": "tsx scripts/create-user.ts",
|
"create-user": "tsx scripts/create-user.ts",
|
||||||
|
"reminders:check": "tsx scripts/maintenance-reminders.ts",
|
||||||
"prepare": "husky || true"
|
"prepare": "husky || true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { pool } from '../src/lib/server/db/client';
|
||||||
|
import {
|
||||||
|
runRemindersOnce,
|
||||||
|
type RunResult
|
||||||
|
} from '../src/lib/server/services/maintenance-reminders';
|
||||||
|
|
||||||
|
function stripSurroundingQuotes(s: string | undefined): string | undefined {
|
||||||
|
if (!s || s.length < 2) return s;
|
||||||
|
const first = s[0];
|
||||||
|
const last = s[s.length - 1];
|
||||||
|
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readArg(flag: string, fallback?: string): string | undefined {
|
||||||
|
const i = process.argv.indexOf(flag);
|
||||||
|
return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFlag(flag: string): boolean {
|
||||||
|
return process.argv.includes(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const soonRaw = readArg('--soon-days', '7') ?? '7';
|
||||||
|
const soonDays = Number.parseInt(soonRaw, 10);
|
||||||
|
if (!Number.isFinite(soonDays) || soonDays < 0 || soonDays > 365) {
|
||||||
|
console.error('--soon-days must be an integer in [0, 365]');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const companyId = readArg('--company');
|
||||||
|
const dryRun = hasFlag('--dry-run');
|
||||||
|
const backfill = hasFlag('--backfill');
|
||||||
|
|
||||||
|
if (dryRun && backfill) {
|
||||||
|
console.error('--dry-run and --backfill are mutually exclusive');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = new Date();
|
||||||
|
let result: RunResult;
|
||||||
|
try {
|
||||||
|
result = await runRemindersOnce({ companyId, soonDays, dryRun, backfill });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(JSON.stringify({ ok: false, error: (e as Error).message }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
ok: true,
|
||||||
|
startedAt: startedAt.toISOString(),
|
||||||
|
durationMs: Date.now() - startedAt.getTime(),
|
||||||
|
soonDays,
|
||||||
|
companyId: companyId ?? null,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
// Single-line JSON so journald/grep handle it cleanly.
|
||||||
|
console.log(JSON.stringify(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(JSON.stringify({ ok: false, error: (e as Error).message }));
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await pool.end().catch(() => {});
|
||||||
|
});
|
||||||
@@ -33,7 +33,8 @@ export const checklistScopeEnum = pgEnum('checklist_scope', [
|
|||||||
'task',
|
'task',
|
||||||
'subtask',
|
'subtask',
|
||||||
'maintenance_event',
|
'maintenance_event',
|
||||||
'ad_hoc'
|
'ad_hoc',
|
||||||
|
'property'
|
||||||
]);
|
]);
|
||||||
export const docScopeEnum = pgEnum('doc_scope', [
|
export const docScopeEnum = pgEnum('doc_scope', [
|
||||||
'project',
|
'project',
|
||||||
@@ -75,8 +76,15 @@ export const notificationKindEnum = pgEnum('notification_kind', [
|
|||||||
'asset_moved',
|
'asset_moved',
|
||||||
'decision_created',
|
'decision_created',
|
||||||
'maintenance_event_recorded',
|
'maintenance_event_recorded',
|
||||||
|
'maintenance_due_soon',
|
||||||
|
'maintenance_overdue',
|
||||||
'generic'
|
'generic'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const maintenanceReminderKindEnum = pgEnum('maintenance_reminder_kind', [
|
||||||
|
'due_soon',
|
||||||
|
'overdue'
|
||||||
|
]);
|
||||||
export const expenseKindEnum = pgEnum('expense_kind', [
|
export const expenseKindEnum = pgEnum('expense_kind', [
|
||||||
'water',
|
'water',
|
||||||
'electricity',
|
'electricity',
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { pgTable, varchar, text, integer, numeric, timestamp, boolean, index } from 'drizzle-orm/pg-core';
|
import { pgTable, varchar, text, integer, numeric, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './tenancy';
|
import { users } from './tenancy';
|
||||||
import { assets } from './assets';
|
import { assets } from './assets';
|
||||||
import { checklistTemplates, checklistInstances } from './checklists';
|
import { checklistTemplates, checklistInstances } from './checklists';
|
||||||
import { scheduleKindEnum, intervalUnitEnum, pk, fk, createdAt, updatedAt } from './_shared';
|
import {
|
||||||
|
scheduleKindEnum,
|
||||||
|
intervalUnitEnum,
|
||||||
|
maintenanceReminderKindEnum,
|
||||||
|
pk,
|
||||||
|
fk,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
|
} from './_shared';
|
||||||
|
|
||||||
export const maintenanceSchedules = pgTable(
|
export const maintenanceSchedules = pgTable(
|
||||||
'maintenance_schedules',
|
'maintenance_schedules',
|
||||||
@@ -80,7 +88,32 @@ export const maintenanceEvents = pgTable(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dedup log for the reminder cron. We insert (schedule_id, kind, due_at)
|
||||||
|
// before firing notify(); the unique index makes the insert atomic, so
|
||||||
|
// concurrent or repeated runs cannot double-fire for the same window.
|
||||||
|
// After service is recorded, schedules.next_due_at advances → new tuple →
|
||||||
|
// reminder fires fresh. Rows are kept indefinitely for audit; a separate
|
||||||
|
// cleanup task can prune > 90 days old.
|
||||||
|
export const maintenanceRemindersSent = pgTable(
|
||||||
|
'maintenance_reminders_sent',
|
||||||
|
{
|
||||||
|
id: pk(),
|
||||||
|
scheduleId: fk('schedule_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => maintenanceSchedules.id, { onDelete: 'cascade' }),
|
||||||
|
kind: maintenanceReminderKindEnum('kind').notNull(),
|
||||||
|
dueAt: timestamp('due_at', { withTimezone: true }).notNull(),
|
||||||
|
firedAt: timestamp('fired_at', { withTimezone: true }).notNull().defaultNow()
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
// The dedup key. INSERT … ON CONFLICT DO NOTHING relies on this.
|
||||||
|
dedupUq: uniqueIndex('mrs_schedule_kind_due_uq').on(t.scheduleId, t.kind, t.dueAt),
|
||||||
|
bySchedule: index('mrs_by_schedule').on(t.scheduleId, t.kind)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export type MaintenanceSchedule = typeof maintenanceSchedules.$inferSelect;
|
export type MaintenanceSchedule = typeof maintenanceSchedules.$inferSelect;
|
||||||
export type NewMaintenanceSchedule = typeof maintenanceSchedules.$inferInsert;
|
export type NewMaintenanceSchedule = typeof maintenanceSchedules.$inferInsert;
|
||||||
export type UsageReading = typeof usageReadings.$inferSelect;
|
export type UsageReading = typeof usageReadings.$inferSelect;
|
||||||
export type MaintenanceEvent = typeof maintenanceEvents.$inferSelect;
|
export type MaintenanceEvent = typeof maintenanceEvents.$inferSelect;
|
||||||
|
export type MaintenanceReminderSent = typeof maintenanceRemindersSent.$inferSelect;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
|
||||||
import { pgTable, varchar, text, numeric, index } from 'drizzle-orm/pg-core';
|
import { pgTable, varchar, text, numeric, index } from 'drizzle-orm/pg-core';
|
||||||
import { companies, users } from './tenancy';
|
import { companies, users } from './tenancy';
|
||||||
import { pk, fk, createdAt, updatedAt, deletedAt } from './_shared';
|
import { pk, fk, createdAt, updatedAt, deletedAt } from './_shared';
|
||||||
@@ -9,6 +10,13 @@ export const properties = pgTable(
|
|||||||
companyId: fk('company_id')
|
companyId: fk('company_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => companies.id, { onDelete: 'cascade' }),
|
.references(() => companies.id, { onDelete: 'cascade' }),
|
||||||
|
// NULL = root. References another property in the same company. Use cases:
|
||||||
|
// a building owns its apartments, a campus owns its sub-buildings.
|
||||||
|
// Cross-company references and cycles are blocked at the service layer
|
||||||
|
// (see assertNoCycle / setParent in services/properties.ts).
|
||||||
|
parentId: fk('parent_id').references((): AnyPgColumn => properties.id, {
|
||||||
|
onDelete: 'restrict'
|
||||||
|
}),
|
||||||
name: varchar('name', { length: 255 }).notNull(),
|
name: varchar('name', { length: 255 }).notNull(),
|
||||||
kind: varchar('kind', { length: 64 }), // warehouse, office, datacenter, ...
|
kind: varchar('kind', { length: 64 }), // warehouse, office, datacenter, ...
|
||||||
addressLine1: varchar('address_line1', { length: 255 }),
|
addressLine1: varchar('address_line1', { length: 255 }),
|
||||||
@@ -26,7 +34,8 @@ export const properties = pgTable(
|
|||||||
deletedAt: deletedAt()
|
deletedAt: deletedAt()
|
||||||
},
|
},
|
||||||
(t) => ({
|
(t) => ({
|
||||||
byCompany: index('properties_by_company').on(t.companyId)
|
byCompany: index('properties_by_company').on(t.companyId),
|
||||||
|
byParent: index('properties_by_parent').on(t.companyId, t.parentId)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, asc, desc, eq, isNull, or, sql } from 'drizzle-orm';
|
import { and, asc, desc, eq, inArray, isNull, or, sql } from 'drizzle-orm';
|
||||||
import { db } from '$lib/server/db/client';
|
import { db } from '$lib/server/db/client';
|
||||||
import {
|
import {
|
||||||
assets,
|
assets,
|
||||||
@@ -292,7 +292,10 @@ export async function appendAssetLog(
|
|||||||
export interface AssetListOptions {
|
export interface AssetListOptions {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
typeSlug?: string;
|
typeSlug?: string;
|
||||||
|
/** Restrict to a single property. Mutually exclusive with `propertyIds`. */
|
||||||
propertyId?: string;
|
propertyId?: string;
|
||||||
|
/** Restrict to assets at any of these properties (e.g. a property tree). */
|
||||||
|
propertyIds?: string[];
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
q?: string;
|
q?: string;
|
||||||
@@ -302,7 +305,11 @@ export interface AssetListOptions {
|
|||||||
|
|
||||||
export async function listAssets(opts: AssetListOptions) {
|
export async function listAssets(opts: AssetListOptions) {
|
||||||
const where = [eq(assets.companyId, opts.companyId), isNull(assets.deletedAt)];
|
const where = [eq(assets.companyId, opts.companyId), isNull(assets.deletedAt)];
|
||||||
if (opts.propertyId) where.push(eq(assets.currentPropertyId, opts.propertyId));
|
if (opts.propertyIds && opts.propertyIds.length > 0) {
|
||||||
|
where.push(inArray(assets.currentPropertyId, opts.propertyIds));
|
||||||
|
} else if (opts.propertyId) {
|
||||||
|
where.push(eq(assets.currentPropertyId, opts.propertyId));
|
||||||
|
}
|
||||||
if (opts.projectId) where.push(eq(assets.currentProjectId, opts.projectId));
|
if (opts.projectId) where.push(eq(assets.currentProjectId, opts.projectId));
|
||||||
if (opts.roomId) where.push(eq(assets.currentRoomId, opts.roomId));
|
if (opts.roomId) where.push(eq(assets.currentRoomId, opts.roomId));
|
||||||
if (opts.typeSlug) {
|
if (opts.typeSlug) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, asc, desc, eq, isNull, sql } from 'drizzle-orm';
|
import { and, asc, desc, eq, inArray, isNull, sql } from 'drizzle-orm';
|
||||||
import { db } from '$lib/server/db/client';
|
import { db } from '$lib/server/db/client';
|
||||||
import {
|
import {
|
||||||
checklistInstances,
|
checklistInstances,
|
||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
type ChecklistTemplateItem
|
type ChecklistTemplateItem
|
||||||
} from '$lib/server/db/schema/checklists';
|
} from '$lib/server/db/schema/checklists';
|
||||||
|
|
||||||
export type ChecklistScope = 'task' | 'subtask' | 'maintenance_event' | 'ad_hoc';
|
export type ChecklistScope =
|
||||||
|
| 'task'
|
||||||
|
| 'subtask'
|
||||||
|
| 'maintenance_event'
|
||||||
|
| 'ad_hoc'
|
||||||
|
| 'property';
|
||||||
|
|
||||||
// --- templates ---------------------------------------------------------------
|
// --- templates ---------------------------------------------------------------
|
||||||
|
|
||||||
@@ -252,3 +257,25 @@ export async function listInstancesForScope(
|
|||||||
)
|
)
|
||||||
.orderBy(desc(checklistInstances.createdAt));
|
.orderBy(desc(checklistInstances.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklist instances scoped to any of the given property ids. Used by the
|
||||||
|
* property roll-up tab when "include sub-properties" is on.
|
||||||
|
*/
|
||||||
|
export async function listInstancesForProperties(
|
||||||
|
companyId: string,
|
||||||
|
propertyIds: string[]
|
||||||
|
) {
|
||||||
|
if (propertyIds.length === 0) return [];
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(checklistInstances)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(checklistInstances.companyId, companyId),
|
||||||
|
eq(checklistInstances.scopeType, 'property'),
|
||||||
|
inArray(checklistInstances.scopeId, propertyIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(checklistInstances.createdAt));
|
||||||
|
}
|
||||||
|
|||||||
@@ -165,7 +165,21 @@ export async function listExpensesForProperty(
|
|||||||
opts: { kinds?: ExpenseKind[]; limit?: number } = {}
|
opts: { kinds?: ExpenseKind[]; limit?: number } = {}
|
||||||
): Promise<PropertyExpense[]> {
|
): Promise<PropertyExpense[]> {
|
||||||
await assertProperty(companyId, propertyId);
|
await assertProperty(companyId, propertyId);
|
||||||
const where = [eq(propertyExpenses.propertyId, propertyId)];
|
return listExpensesForProperties([propertyId], opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree-aware list. Caller resolves descendants via getDescendantIds (in
|
||||||
|
* services/properties) and passes the full id array. No per-property auth
|
||||||
|
* check — caller is responsible for ensuring every id belongs to the active
|
||||||
|
* company. Empty array returns [].
|
||||||
|
*/
|
||||||
|
export async function listExpensesForProperties(
|
||||||
|
propertyIds: string[],
|
||||||
|
opts: { kinds?: ExpenseKind[]; limit?: number } = {}
|
||||||
|
): Promise<PropertyExpense[]> {
|
||||||
|
if (propertyIds.length === 0) return [];
|
||||||
|
const where = [inArray(propertyExpenses.propertyId, propertyIds)];
|
||||||
if (opts.kinds && opts.kinds.length > 0) {
|
if (opts.kinds && opts.kinds.length > 0) {
|
||||||
where.push(inArray(propertyExpenses.kind, opts.kinds));
|
where.push(inArray(propertyExpenses.kind, opts.kinds));
|
||||||
}
|
}
|
||||||
@@ -199,7 +213,18 @@ export async function monthlySeriesForProperty(
|
|||||||
months: ChartRange = 12
|
months: ChartRange = 12
|
||||||
): Promise<MonthlySeriesPoint[]> {
|
): Promise<MonthlySeriesPoint[]> {
|
||||||
await assertProperty(companyId, propertyId);
|
await assertProperty(companyId, propertyId);
|
||||||
if (kinds.length === 0) return [];
|
return monthlySeriesForProperties([propertyId], kinds, months);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree-aware monthly series. Caller passes the resolved id array.
|
||||||
|
*/
|
||||||
|
export async function monthlySeriesForProperties(
|
||||||
|
propertyIds: string[],
|
||||||
|
kinds: ExpenseKind[],
|
||||||
|
months: ChartRange = 12
|
||||||
|
): Promise<MonthlySeriesPoint[]> {
|
||||||
|
if (propertyIds.length === 0 || kinds.length === 0) return [];
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const thisMonthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
const thisMonthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||||
@@ -214,7 +239,7 @@ export async function monthlySeriesForProperty(
|
|||||||
.from(propertyExpenses)
|
.from(propertyExpenses)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(propertyExpenses.propertyId, propertyId),
|
inArray(propertyExpenses.propertyId, propertyIds),
|
||||||
inArray(propertyExpenses.kind, kinds)
|
inArray(propertyExpenses.kind, kinds)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -241,7 +266,7 @@ export async function monthlySeriesForProperty(
|
|||||||
.from(propertyExpenses)
|
.from(propertyExpenses)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(propertyExpenses.propertyId, propertyId),
|
inArray(propertyExpenses.propertyId, propertyIds),
|
||||||
inArray(propertyExpenses.kind, kinds),
|
inArray(propertyExpenses.kind, kinds),
|
||||||
gte(propertyExpenses.incurredAt, start),
|
gte(propertyExpenses.incurredAt, start),
|
||||||
lt(propertyExpenses.incurredAt, new Date(Date.UTC(thisMonthStart.getUTCFullYear(), thisMonthStart.getUTCMonth() + 1, 1)))
|
lt(propertyExpenses.incurredAt, new Date(Date.UTC(thisMonthStart.getUTCFullYear(), thisMonthStart.getUTCMonth() + 1, 1)))
|
||||||
@@ -279,6 +304,25 @@ export async function summaryForProperty(
|
|||||||
currency: string | null;
|
currency: string | null;
|
||||||
}> {
|
}> {
|
||||||
await assertProperty(companyId, propertyId);
|
await assertProperty(companyId, propertyId);
|
||||||
|
return summaryForProperties([propertyId], days);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree-aware summary. Caller passes resolved id array. When children carry
|
||||||
|
* different currencies, `currency` is null — the UI groups by currency in
|
||||||
|
* that case rather than auto-converting.
|
||||||
|
*/
|
||||||
|
export async function summaryForProperties(
|
||||||
|
propertyIds: string[],
|
||||||
|
days = 365
|
||||||
|
): Promise<{
|
||||||
|
byKind: Partial<Record<ExpenseKind, number>>;
|
||||||
|
grandTotal: number;
|
||||||
|
currency: string | null;
|
||||||
|
}> {
|
||||||
|
if (propertyIds.length === 0) {
|
||||||
|
return { byKind: {}, grandTotal: 0, currency: null };
|
||||||
|
}
|
||||||
const since = new Date(Date.now() - days * 86_400_000);
|
const since = new Date(Date.now() - days * 86_400_000);
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -289,7 +333,7 @@ export async function summaryForProperty(
|
|||||||
.from(propertyExpenses)
|
.from(propertyExpenses)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(propertyExpenses.propertyId, propertyId),
|
inArray(propertyExpenses.propertyId, propertyIds),
|
||||||
gte(propertyExpenses.incurredAt, since)
|
gte(propertyExpenses.incurredAt, since)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { and, eq, inArray, isNotNull, lte } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db/client';
|
||||||
|
import { assets } from '$lib/server/db/schema/assets';
|
||||||
|
import { properties } from '$lib/server/db/schema/properties';
|
||||||
|
import {
|
||||||
|
maintenanceRemindersSent,
|
||||||
|
maintenanceSchedules
|
||||||
|
} from '$lib/server/db/schema/maintenance';
|
||||||
|
import { companyUsers } from '$lib/server/db/schema/tenancy';
|
||||||
|
import { notify } from './notifications';
|
||||||
|
|
||||||
|
export type ReminderKind = 'due_soon' | 'overdue';
|
||||||
|
|
||||||
|
export interface DueSchedule {
|
||||||
|
scheduleId: string;
|
||||||
|
scheduleName: string;
|
||||||
|
nextDueAt: Date;
|
||||||
|
kind: ReminderKind;
|
||||||
|
assetId: string;
|
||||||
|
assetName: string;
|
||||||
|
companyId: string;
|
||||||
|
propertyId: string | null;
|
||||||
|
propertyName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time-based active schedules whose next_due_at is overdue or within the
|
||||||
|
* `soonDays` window. Joined to asset → property so the notification body can
|
||||||
|
* cite the location. Usage-based schedules don't have next_due_at and are
|
||||||
|
* intentionally excluded — they need a different trigger (usage-reading
|
||||||
|
* crossover) which isn't part of this iteration.
|
||||||
|
*/
|
||||||
|
export async function findDueSchedules(opts: {
|
||||||
|
companyId?: string;
|
||||||
|
soonDays: number;
|
||||||
|
now?: Date;
|
||||||
|
}): Promise<DueSchedule[]> {
|
||||||
|
const now = opts.now ?? new Date();
|
||||||
|
const horizon = new Date(now.getTime() + opts.soonDays * 86_400_000);
|
||||||
|
|
||||||
|
const where = [
|
||||||
|
eq(maintenanceSchedules.active, true),
|
||||||
|
eq(maintenanceSchedules.kind, 'time'),
|
||||||
|
isNotNull(maintenanceSchedules.nextDueAt),
|
||||||
|
lte(maintenanceSchedules.nextDueAt, horizon)
|
||||||
|
];
|
||||||
|
if (opts.companyId) where.push(eq(assets.companyId, opts.companyId));
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
scheduleId: maintenanceSchedules.id,
|
||||||
|
scheduleName: maintenanceSchedules.name,
|
||||||
|
nextDueAt: maintenanceSchedules.nextDueAt,
|
||||||
|
assetId: maintenanceSchedules.assetId,
|
||||||
|
assetName: assets.name,
|
||||||
|
companyId: assets.companyId,
|
||||||
|
propertyId: assets.currentPropertyId,
|
||||||
|
propertyName: properties.name
|
||||||
|
})
|
||||||
|
.from(maintenanceSchedules)
|
||||||
|
.innerJoin(assets, eq(assets.id, maintenanceSchedules.assetId))
|
||||||
|
.leftJoin(properties, eq(properties.id, assets.currentPropertyId))
|
||||||
|
.where(and(...where));
|
||||||
|
|
||||||
|
return rows
|
||||||
|
.filter((r): r is typeof r & { nextDueAt: Date } => r.nextDueAt !== null)
|
||||||
|
.map((r) => ({
|
||||||
|
scheduleId: r.scheduleId,
|
||||||
|
scheduleName: r.scheduleName,
|
||||||
|
nextDueAt: r.nextDueAt,
|
||||||
|
kind: r.nextDueAt < now ? ('overdue' as const) : ('due_soon' as const),
|
||||||
|
assetId: r.assetId,
|
||||||
|
assetName: r.assetName,
|
||||||
|
companyId: r.companyId,
|
||||||
|
propertyId: r.propertyId,
|
||||||
|
propertyName: r.propertyName
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** admin + manager user_ids for a company. */
|
||||||
|
async function recipientsFor(companyId: string): Promise<string[]> {
|
||||||
|
const rows = await db
|
||||||
|
.select({ userId: companyUsers.userId })
|
||||||
|
.from(companyUsers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(companyUsers.companyId, companyId),
|
||||||
|
inArray(companyUsers.role, ['admin', 'manager'])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Array.from(new Set(rows.map((r) => r.userId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to record that a reminder is being fired. Returns true if this is the
|
||||||
|
* first time for (schedule, kind, due_at), false if it was already logged.
|
||||||
|
* The unique index makes this atomic across concurrent runs.
|
||||||
|
*/
|
||||||
|
async function tryRecordSent(
|
||||||
|
scheduleId: string,
|
||||||
|
kind: ReminderKind,
|
||||||
|
dueAt: Date
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await db
|
||||||
|
.insert(maintenanceRemindersSent)
|
||||||
|
.values({ scheduleId, kind, dueAt })
|
||||||
|
.onConflictDoNothing({
|
||||||
|
target: [
|
||||||
|
maintenanceRemindersSent.scheduleId,
|
||||||
|
maintenanceRemindersSent.kind,
|
||||||
|
maintenanceRemindersSent.dueAt
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.returning({ id: maintenanceRemindersSent.id });
|
||||||
|
return result.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBody(s: DueSchedule, now: Date): string {
|
||||||
|
const due = s.nextDueAt;
|
||||||
|
const days = Math.round((due.getTime() - now.getTime()) / 86_400_000);
|
||||||
|
const where = s.propertyName ? ` at ${s.propertyName}` : '';
|
||||||
|
if (s.kind === 'overdue') {
|
||||||
|
const overdueDays = Math.max(0, -days);
|
||||||
|
return `${s.scheduleName} on ${s.assetName}${where} is overdue by ${overdueDays} day${overdueDays === 1 ? '' : 's'} (was due ${due.toISOString().slice(0, 10)}).`;
|
||||||
|
}
|
||||||
|
return `${s.scheduleName} on ${s.assetName}${where} is due in ${days} day${days === 1 ? '' : 's'} (${due.toISOString().slice(0, 10)}).`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunOpts {
|
||||||
|
companyId?: string;
|
||||||
|
soonDays: number;
|
||||||
|
now?: Date;
|
||||||
|
dryRun?: boolean;
|
||||||
|
/**
|
||||||
|
* On first deploy, mark every currently-due/overdue schedule as already
|
||||||
|
* notified so day-one isn't a deluge of stale alerts. Returns count in
|
||||||
|
* `backfilled`. Existing dedup rows are not touched (still ON CONFLICT
|
||||||
|
* DO NOTHING).
|
||||||
|
*/
|
||||||
|
backfill?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunResult {
|
||||||
|
scanned: number;
|
||||||
|
fired: number;
|
||||||
|
skippedDedup: number;
|
||||||
|
noRecipients: number;
|
||||||
|
backfilled: number;
|
||||||
|
dryRun: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runRemindersOnce(opts: RunOpts): Promise<RunResult> {
|
||||||
|
const now = opts.now ?? new Date();
|
||||||
|
const due = await findDueSchedules({
|
||||||
|
companyId: opts.companyId,
|
||||||
|
soonDays: opts.soonDays,
|
||||||
|
now
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: RunResult = {
|
||||||
|
scanned: due.length,
|
||||||
|
fired: 0,
|
||||||
|
skippedDedup: 0,
|
||||||
|
noRecipients: 0,
|
||||||
|
backfilled: 0,
|
||||||
|
dryRun: !!opts.dryRun
|
||||||
|
};
|
||||||
|
|
||||||
|
// Backfill: log everything as already-sent without notifying. Useful on
|
||||||
|
// first deploy. Doesn't depend on dryRun because backfill is the entire
|
||||||
|
// effect when set.
|
||||||
|
if (opts.backfill) {
|
||||||
|
for (const s of due) {
|
||||||
|
const inserted = await tryRecordSent(s.scheduleId, s.kind, s.nextDueAt);
|
||||||
|
if (inserted) result.backfilled += 1;
|
||||||
|
else result.skippedDedup += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache recipients per company within this run.
|
||||||
|
const recipientsCache = new Map<string, string[]>();
|
||||||
|
|
||||||
|
for (const s of due) {
|
||||||
|
let userIds = recipientsCache.get(s.companyId);
|
||||||
|
if (!userIds) {
|
||||||
|
userIds = await recipientsFor(s.companyId);
|
||||||
|
recipientsCache.set(s.companyId, userIds);
|
||||||
|
}
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
result.noRecipients += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
result.fired += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically claim this (schedule, kind, due_at). Only proceed to notify
|
||||||
|
// if we won the insert.
|
||||||
|
const claimed = await tryRecordSent(s.scheduleId, s.kind, s.nextDueAt);
|
||||||
|
if (!claimed) {
|
||||||
|
result.skippedDedup += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
s.kind === 'overdue'
|
||||||
|
? `Maintenance overdue: ${s.scheduleName}`
|
||||||
|
: `Maintenance due soon: ${s.scheduleName}`;
|
||||||
|
const notificationKind =
|
||||||
|
s.kind === 'overdue' ? 'maintenance_overdue' : 'maintenance_due_soon';
|
||||||
|
await notify({
|
||||||
|
companyId: s.companyId,
|
||||||
|
userIds,
|
||||||
|
kind: notificationKind,
|
||||||
|
title,
|
||||||
|
body: buildBody(s, now),
|
||||||
|
link: `/assets/${s.assetId}/maintenance`
|
||||||
|
});
|
||||||
|
result.fired += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addDays, addHours, addMonths, addYears } from 'date-fns';
|
import { addDays, addHours, addMonths, addYears } from 'date-fns';
|
||||||
import { and, asc, desc, eq, isNotNull, lte, sql } from 'drizzle-orm';
|
import { and, asc, desc, eq, inArray, isNotNull, lte, sql } from 'drizzle-orm';
|
||||||
import { db } from '$lib/server/db/client';
|
import { db } from '$lib/server/db/client';
|
||||||
import { assets } from '$lib/server/db/schema/assets';
|
import { assets } from '$lib/server/db/schema/assets';
|
||||||
import {
|
import {
|
||||||
@@ -349,6 +349,77 @@ export async function listDueAndOverdue(opts: OverdueOpts) {
|
|||||||
.limit(opts.limit ?? 50);
|
.limit(opts.limit ?? 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- property-tree readers --------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintenance schedules for assets currently located at any of the given
|
||||||
|
* properties. Caller passes a property id array (e.g. expanded via
|
||||||
|
* getDescendantIds).
|
||||||
|
*/
|
||||||
|
export async function listSchedulesForProperties(companyId: string, propertyIds: string[]) {
|
||||||
|
if (propertyIds.length === 0) return [];
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: maintenanceSchedules.id,
|
||||||
|
name: maintenanceSchedules.name,
|
||||||
|
kind: maintenanceSchedules.kind,
|
||||||
|
active: maintenanceSchedules.active,
|
||||||
|
intervalValue: maintenanceSchedules.intervalValue,
|
||||||
|
intervalUnit: maintenanceSchedules.intervalUnit,
|
||||||
|
lastServicedAt: maintenanceSchedules.lastServicedAt,
|
||||||
|
nextDueAt: maintenanceSchedules.nextDueAt,
|
||||||
|
nextDueUsage: maintenanceSchedules.nextDueUsage,
|
||||||
|
assetId: maintenanceSchedules.assetId,
|
||||||
|
assetName: assets.name,
|
||||||
|
propertyId: assets.currentPropertyId
|
||||||
|
})
|
||||||
|
.from(maintenanceSchedules)
|
||||||
|
.innerJoin(assets, eq(assets.id, maintenanceSchedules.assetId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(assets.companyId, companyId),
|
||||||
|
inArray(assets.currentPropertyId, propertyIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(maintenanceSchedules.active), asc(maintenanceSchedules.nextDueAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintenance events for assets at any of the given properties. Recent first.
|
||||||
|
*/
|
||||||
|
export async function listEventsForProperties(
|
||||||
|
companyId: string,
|
||||||
|
propertyIds: string[],
|
||||||
|
limit = 200
|
||||||
|
) {
|
||||||
|
if (propertyIds.length === 0) return [];
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: maintenanceEvents.id,
|
||||||
|
scheduleId: maintenanceEvents.scheduleId,
|
||||||
|
scheduleName: maintenanceSchedules.name,
|
||||||
|
performedAt: maintenanceEvents.performedAt,
|
||||||
|
performedBy: maintenanceEvents.performedBy,
|
||||||
|
notes: maintenanceEvents.notes,
|
||||||
|
usageReading: maintenanceEvents.usageReading,
|
||||||
|
checklistInstanceId: maintenanceEvents.checklistInstanceId,
|
||||||
|
assetId: maintenanceEvents.assetId,
|
||||||
|
assetName: assets.name,
|
||||||
|
propertyId: assets.currentPropertyId
|
||||||
|
})
|
||||||
|
.from(maintenanceEvents)
|
||||||
|
.innerJoin(assets, eq(assets.id, maintenanceEvents.assetId))
|
||||||
|
.leftJoin(maintenanceSchedules, eq(maintenanceSchedules.id, maintenanceEvents.scheduleId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(assets.companyId, companyId),
|
||||||
|
inArray(assets.currentPropertyId, propertyIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(maintenanceEvents.performedAt))
|
||||||
|
.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
export async function countOverdueForCompany(companyId: string): Promise<number> {
|
export async function countOverdueForCompany(companyId: string): Promise<number> {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({ n: sql<number>`count(*)::int` })
|
.select({ n: sql<number>`count(*)::int` })
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type NotificationKind =
|
|||||||
| 'asset_moved'
|
| 'asset_moved'
|
||||||
| 'decision_created'
|
| 'decision_created'
|
||||||
| 'maintenance_event_recorded'
|
| 'maintenance_event_recorded'
|
||||||
|
| 'maintenance_due_soon'
|
||||||
|
| 'maintenance_overdue'
|
||||||
| 'generic';
|
| 'generic';
|
||||||
|
|
||||||
export interface NotifyInput {
|
export interface NotifyInput {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface PropertyCreateInput {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
name: string;
|
name: string;
|
||||||
kind?: string | null;
|
kind?: string | null;
|
||||||
|
parentId?: string | null;
|
||||||
addressLine1?: string | null;
|
addressLine1?: string | null;
|
||||||
addressLine2?: string | null;
|
addressLine2?: string | null;
|
||||||
city?: string | null;
|
city?: string | null;
|
||||||
@@ -16,9 +17,51 @@ export interface PropertyCreateInput {
|
|||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soft cap. Properties past this depth still save, but get logged so a sudden
|
||||||
|
// spike in journal entries draws attention to a tree growing pathologically.
|
||||||
|
// Exists because getDescendantIds is an unbounded recursive CTE and deep
|
||||||
|
// hierarchies are also painful to navigate in the UI.
|
||||||
|
const MAX_RECOMMENDED_DEPTH = 5;
|
||||||
|
|
||||||
|
async function warnIfDeep(
|
||||||
|
companyId: string,
|
||||||
|
propertyId: string,
|
||||||
|
candidateParentId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const parentAncestors = await getAncestorIds(companyId, candidateParentId);
|
||||||
|
const newChildDepth = parentAncestors.length + 1;
|
||||||
|
if (newChildDepth > MAX_RECOMMENDED_DEPTH) {
|
||||||
|
console.warn(
|
||||||
|
`[properties] depth cap exceeded: parenting ${propertyId} under ${candidateParentId} ` +
|
||||||
|
`places it at depth ${newChildDepth} (recommended max: ${MAX_RECOMMENDED_DEPTH}). ` +
|
||||||
|
`Allowed but flagged.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertParentInCompany(companyId: string, parentId: string): Promise<void> {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ id: properties.id })
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.id, parentId),
|
||||||
|
eq(properties.companyId, companyId),
|
||||||
|
isNull(properties.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
if (!row) throw new Error('parent property not found in this company');
|
||||||
|
}
|
||||||
|
|
||||||
export async function createProperty(input: PropertyCreateInput): Promise<{ id: string }> {
|
export async function createProperty(input: PropertyCreateInput): Promise<{ id: string }> {
|
||||||
|
if (input.parentId) {
|
||||||
|
await assertParentInCompany(input.companyId, input.parentId);
|
||||||
|
await warnIfDeep(input.companyId, '<new>', input.parentId);
|
||||||
|
}
|
||||||
const values: NewProperty = {
|
const values: NewProperty = {
|
||||||
companyId: input.companyId,
|
companyId: input.companyId,
|
||||||
|
parentId: input.parentId ?? null,
|
||||||
name: input.name.trim(),
|
name: input.name.trim(),
|
||||||
kind: input.kind ?? null,
|
kind: input.kind ?? null,
|
||||||
addressLine1: input.addressLine1 ?? null,
|
addressLine1: input.addressLine1 ?? null,
|
||||||
@@ -62,11 +105,18 @@ export async function updateProperty(
|
|||||||
id: string,
|
id: string,
|
||||||
patch: Partial<PropertyCreateInput>
|
patch: Partial<PropertyCreateInput>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (patch.parentId !== undefined && patch.parentId !== null) {
|
||||||
|
if (patch.parentId === id) throw new Error('a property cannot be its own parent');
|
||||||
|
await assertParentInCompany(companyId, patch.parentId);
|
||||||
|
await assertNoCycle(companyId, id, patch.parentId);
|
||||||
|
await warnIfDeep(companyId, id, patch.parentId);
|
||||||
|
}
|
||||||
await db
|
await db
|
||||||
.update(properties)
|
.update(properties)
|
||||||
.set({
|
.set({
|
||||||
...(patch.name !== undefined && { name: patch.name.trim() }),
|
...(patch.name !== undefined && { name: patch.name.trim() }),
|
||||||
...(patch.kind !== undefined && { kind: patch.kind ?? null }),
|
...(patch.kind !== undefined && { kind: patch.kind ?? null }),
|
||||||
|
...(patch.parentId !== undefined && { parentId: patch.parentId ?? null }),
|
||||||
...(patch.addressLine1 !== undefined && { addressLine1: patch.addressLine1 ?? null }),
|
...(patch.addressLine1 !== undefined && { addressLine1: patch.addressLine1 ?? null }),
|
||||||
...(patch.addressLine2 !== undefined && { addressLine2: patch.addressLine2 ?? null }),
|
...(patch.addressLine2 !== undefined && { addressLine2: patch.addressLine2 ?? null }),
|
||||||
...(patch.city !== undefined && { city: patch.city ?? null }),
|
...(patch.city !== undefined && { city: patch.city ?? null }),
|
||||||
@@ -81,8 +131,96 @@ export async function updateProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function softDeleteProperty(companyId: string, id: string): Promise<void> {
|
export async function softDeleteProperty(companyId: string, id: string): Promise<void> {
|
||||||
|
// Block delete when live children exist — orphaning to root would silently
|
||||||
|
// disconnect roll-up reports. Caller should detach children first if that's
|
||||||
|
// the intent.
|
||||||
|
const [child] = await db
|
||||||
|
.select({ id: properties.id })
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.parentId, id),
|
||||||
|
eq(properties.companyId, companyId),
|
||||||
|
isNull(properties.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
if (child) throw new Error('detach or delete sub-properties first');
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(properties)
|
.update(properties)
|
||||||
.set({ deletedAt: sql`now()` })
|
.set({ deletedAt: sql`now()` })
|
||||||
.where(and(eq(properties.id, id), eq(properties.companyId, companyId)));
|
.where(and(eq(properties.id, id), eq(properties.companyId, companyId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Hierarchy helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [propertyId, ...all descendants] using a recursive CTE. The result
|
||||||
|
* always includes the root id so callers can use it directly with
|
||||||
|
* `WHERE property_id = ANY($ids)` for roll-up queries.
|
||||||
|
*
|
||||||
|
* Bounded by `deleted_at IS NULL` and `company_id = $companyId` to avoid
|
||||||
|
* leaking cross-company rows even if a service caller goofs.
|
||||||
|
*/
|
||||||
|
export async function getDescendantIds(
|
||||||
|
companyId: string,
|
||||||
|
propertyId: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const rows = await db.execute<{ id: string }>(sql`
|
||||||
|
WITH RECURSIVE tree AS (
|
||||||
|
SELECT id FROM properties
|
||||||
|
WHERE id = ${propertyId}
|
||||||
|
AND company_id = ${companyId}
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id FROM properties p
|
||||||
|
INNER JOIN tree t ON p.parent_id = t.id
|
||||||
|
WHERE p.company_id = ${companyId}
|
||||||
|
AND p.deleted_at IS NULL
|
||||||
|
)
|
||||||
|
SELECT id FROM tree
|
||||||
|
`);
|
||||||
|
return rows.rows.map((r) => r.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk parent_id from `propertyId` up to the root. Used by assertNoCycle
|
||||||
|
* to detect attempts at moving a property under one of its own descendants.
|
||||||
|
*/
|
||||||
|
export async function getAncestorIds(
|
||||||
|
companyId: string,
|
||||||
|
propertyId: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const rows = await db.execute<{ id: string }>(sql`
|
||||||
|
WITH RECURSIVE chain AS (
|
||||||
|
SELECT id, parent_id FROM properties
|
||||||
|
WHERE id = ${propertyId} AND company_id = ${companyId}
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id, p.parent_id FROM properties p
|
||||||
|
INNER JOIN chain c ON p.id = c.parent_id
|
||||||
|
WHERE p.company_id = ${companyId}
|
||||||
|
)
|
||||||
|
SELECT id FROM chain WHERE id <> ${propertyId}
|
||||||
|
`);
|
||||||
|
return rows.rows.map((r) => r.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws if assigning `candidateParentId` as the parent of `propertyId` would
|
||||||
|
* create a cycle (i.e. candidateParentId is propertyId itself or one of its
|
||||||
|
* descendants).
|
||||||
|
*/
|
||||||
|
export async function assertNoCycle(
|
||||||
|
companyId: string,
|
||||||
|
propertyId: string,
|
||||||
|
candidateParentId: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (candidateParentId === propertyId) {
|
||||||
|
throw new Error('a property cannot be its own parent');
|
||||||
|
}
|
||||||
|
const descendants = await getDescendantIds(companyId, propertyId);
|
||||||
|
if (descendants.includes(candidateParentId)) {
|
||||||
|
throw new Error('cannot set parent: that property is already a descendant');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,53 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { listProperties } from '$lib/server/services/properties';
|
import { listProperties } from '$lib/server/services/properties';
|
||||||
|
import type { Property } from '$lib/server/db/schema/properties';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export type PropertyRow = Property & { depth: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder the flat company-scoped property list into a depth-first traversal
|
||||||
|
* so parents render immediately above their children. Within each level we
|
||||||
|
* sort by name. Orphan rows (parent_id points outside the visible set —
|
||||||
|
* shouldn't happen with the current restrict-on-delete policy, but defended
|
||||||
|
* here so the UI never silently drops a row) are appended at the end as roots.
|
||||||
|
*/
|
||||||
|
function flattenTree(rows: Property[]): PropertyRow[] {
|
||||||
|
const byParent = new Map<string | null, Property[]>();
|
||||||
|
for (const r of rows) {
|
||||||
|
const key = r.parentId;
|
||||||
|
const list = byParent.get(key);
|
||||||
|
if (list) list.push(r);
|
||||||
|
else byParent.set(key, [r]);
|
||||||
|
}
|
||||||
|
for (const list of byParent.values()) {
|
||||||
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: PropertyRow[] = [];
|
||||||
|
const visible = new Set(rows.map((r) => r.id));
|
||||||
|
function walk(parentId: string | null, depth: number): void {
|
||||||
|
const list = byParent.get(parentId) ?? [];
|
||||||
|
for (const r of list) {
|
||||||
|
out.push({ ...r, depth });
|
||||||
|
walk(r.id, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(null, 0);
|
||||||
|
|
||||||
|
if (out.length < rows.length) {
|
||||||
|
const seen = new Set(out.map((r) => r.id));
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!seen.has(r.id) && (!r.parentId || !visible.has(r.parentId))) {
|
||||||
|
out.push({ ...r, depth: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
if (!locals.company) throw error(400, 'No active company. Pick one from the sidebar.');
|
if (!locals.company) throw error(400, 'No active company. Pick one from the sidebar.');
|
||||||
const rows = await listProperties(locals.company.id);
|
const flat = await listProperties(locals.company.id);
|
||||||
return { properties: rows };
|
return { properties: flattenTree(flat) };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,9 @@
|
|||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{#each data.properties as p}
|
{#each data.properties as p}
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||||
<td class="px-4 py-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
<td class="py-2 pr-4 text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
style:padding-left="{1 + p.depth * 1.5}rem">
|
||||||
|
{#if p.depth > 0}<span class="mr-1 select-none text-gray-400 dark:text-gray-500">└</span>{/if}
|
||||||
<a href="/properties/{p.id}" class="hover:text-primary-600 dark:hover:text-primary-400">{p.name}</a>
|
<a href="/properties/{p.id}" class="hover:text-primary-600 dark:hover:text-primary-400">{p.name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{p.kind ?? '—'}</td>
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{p.kind ?? '—'}</td>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { and, eq, isNull, sql } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db/client';
|
||||||
|
import { properties } from '$lib/server/db/schema/properties';
|
||||||
import { getProperty } from '$lib/server/services/properties';
|
import { getProperty } from '$lib/server/services/properties';
|
||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
@@ -6,5 +9,40 @@ export const load: LayoutServerLoad = async ({ locals, params }) => {
|
|||||||
if (!locals.company) throw error(400, 'No active company');
|
if (!locals.company) throw error(400, 'No active company');
|
||||||
const property = await getProperty(locals.company.id, params.id);
|
const property = await getProperty(locals.company.id, params.id);
|
||||||
if (!property) throw error(404, 'Property not found');
|
if (!property) throw error(404, 'Property not found');
|
||||||
return { property };
|
|
||||||
|
// Fetch parent (for the breadcrumb) and the child count (for the tab badge)
|
||||||
|
// in a single round trip each — both are tiny single-row queries.
|
||||||
|
const [parent, childCountRow] = await Promise.all([
|
||||||
|
property.parentId
|
||||||
|
? db
|
||||||
|
.select({ id: properties.id, name: properties.name })
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.id, property.parentId),
|
||||||
|
eq(properties.companyId, locals.company.id),
|
||||||
|
isNull(properties.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.then((r) => r[0] ?? null)
|
||||||
|
: Promise.resolve(null),
|
||||||
|
db
|
||||||
|
.select({ n: sql<number>`count(*)::int` })
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.parentId, property.id),
|
||||||
|
eq(properties.companyId, locals.company.id),
|
||||||
|
isNull(properties.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then((r) => r[0])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
property,
|
||||||
|
parent,
|
||||||
|
childCount: childCountRow?.n ?? 0
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,16 @@
|
|||||||
|
|
||||||
const tabs = $derived([
|
const tabs = $derived([
|
||||||
{ href: `/properties/${data.property.id}`, label: 'Overview' },
|
{ href: `/properties/${data.property.id}`, label: 'Overview' },
|
||||||
|
{
|
||||||
|
href: `/properties/${data.property.id}/sub-properties`,
|
||||||
|
label: data.childCount > 0 ? `Sub-properties (${data.childCount})` : 'Sub-properties'
|
||||||
|
},
|
||||||
{ href: `/properties/${data.property.id}/rooms`, label: 'Rooms' },
|
{ href: `/properties/${data.property.id}/rooms`, label: 'Rooms' },
|
||||||
{ href: `/properties/${data.property.id}/assets`, label: 'Assets' },
|
{ href: `/properties/${data.property.id}/assets`, label: 'Assets' },
|
||||||
{ href: `/properties/${data.property.id}/accounts`, label: 'Accounts' },
|
{ href: `/properties/${data.property.id}/accounts`, label: 'Accounts' },
|
||||||
{ href: `/properties/${data.property.id}/expenses`, label: 'Expenses' },
|
{ href: `/properties/${data.property.id}/expenses`, label: 'Expenses' },
|
||||||
|
{ href: `/properties/${data.property.id}/maintenance`, label: 'Maintenance' },
|
||||||
|
{ href: `/properties/${data.property.id}/todos`, label: 'Todos' },
|
||||||
{ href: `/properties/${data.property.id}/documents`, label: 'Documents' }
|
{ href: `/properties/${data.property.id}/documents`, label: 'Documents' }
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
@@ -18,9 +24,19 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
{#if data.parent}
|
||||||
|
<nav aria-label="Breadcrumb" class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<a href="/properties/{data.parent.id}" class="hover:text-gray-700 dark:hover:text-gray-200">
|
||||||
|
{data.parent.name}
|
||||||
|
</a>
|
||||||
|
<span class="mx-1.5">›</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">{data.property.name}</span>
|
||||||
|
</nav>
|
||||||
|
{:else}
|
||||||
<div class="text-xs uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
<div class="text-xs uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{data.property.kind ?? 'Property'}
|
{data.property.kind ?? 'Property'}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<h1 class="truncate text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
<h1 class="truncate text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{data.property.name}
|
{data.property.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { error, fail } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { and, eq, isNull, ne, notInArray } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { updateProperty, softDeleteProperty } from '$lib/server/services/properties';
|
import { db } from '$lib/server/db/client';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { properties } from '$lib/server/db/schema/properties';
|
||||||
import type { Actions } from './$types';
|
import {
|
||||||
|
getDescendantIds,
|
||||||
|
softDeleteProperty,
|
||||||
|
updateProperty
|
||||||
|
} from '$lib/server/services/properties';
|
||||||
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
|
||||||
const PatchSchema = z.object({
|
const PatchSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(255),
|
name: z.string().trim().min(1).max(255),
|
||||||
kind: z.string().trim().max(64).optional().or(z.literal('')),
|
kind: z.string().trim().max(64).optional().or(z.literal('')),
|
||||||
|
parentId: z.string().uuid().optional().or(z.literal('')),
|
||||||
addressLine1: z.string().trim().max(255).optional().or(z.literal('')),
|
addressLine1: z.string().trim().max(255).optional().or(z.literal('')),
|
||||||
addressLine2: z.string().trim().max(255).optional().or(z.literal('')),
|
addressLine2: z.string().trim().max(255).optional().or(z.literal('')),
|
||||||
city: z.string().trim().max(128).optional().or(z.literal('')),
|
city: z.string().trim().max(128).optional().or(z.literal('')),
|
||||||
@@ -18,6 +25,28 @@ const PatchSchema = z.object({
|
|||||||
|
|
||||||
const e2n = (s: string | undefined) => (!s ? null : s);
|
const e2n = (s: string | undefined) => (!s ? null : s);
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||||
|
if (!locals.company) throw error(400, 'No active company');
|
||||||
|
// Eligible parents = all live properties in the company minus this one
|
||||||
|
// and its descendants. Excluding descendants is what blocks the user from
|
||||||
|
// creating a cycle through the picker; the service layer's assertNoCycle
|
||||||
|
// is the belt-and-braces guard.
|
||||||
|
const exclude = await getDescendantIds(locals.company.id, params.id);
|
||||||
|
const eligibleParents = await db
|
||||||
|
.select({ id: properties.id, name: properties.name })
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.companyId, locals.company.id),
|
||||||
|
isNull(properties.deletedAt),
|
||||||
|
ne(properties.id, params.id),
|
||||||
|
exclude.length > 0 ? notInArray(properties.id, exclude) : undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(properties.name);
|
||||||
|
return { eligibleParents };
|
||||||
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
save: async ({ request, locals, params }) => {
|
save: async ({ request, locals, params }) => {
|
||||||
if (!locals.company) throw error(401);
|
if (!locals.company) throw error(401);
|
||||||
@@ -28,9 +57,11 @@ export const actions: Actions = {
|
|||||||
return fail(400, { error: parsed.error.errors[0]?.message ?? 'Invalid input' });
|
return fail(400, { error: parsed.error.errors[0]?.message ?? 'Invalid input' });
|
||||||
}
|
}
|
||||||
const v = parsed.data;
|
const v = parsed.data;
|
||||||
|
try {
|
||||||
await updateProperty(locals.company.id, params.id, {
|
await updateProperty(locals.company.id, params.id, {
|
||||||
name: v.name,
|
name: v.name,
|
||||||
kind: e2n(v.kind),
|
kind: e2n(v.kind),
|
||||||
|
parentId: v.parentId ? v.parentId : null,
|
||||||
addressLine1: e2n(v.addressLine1),
|
addressLine1: e2n(v.addressLine1),
|
||||||
addressLine2: e2n(v.addressLine2),
|
addressLine2: e2n(v.addressLine2),
|
||||||
city: e2n(v.city),
|
city: e2n(v.city),
|
||||||
@@ -39,11 +70,18 @@ export const actions: Actions = {
|
|||||||
countryCode: e2n(v.countryCode),
|
countryCode: e2n(v.countryCode),
|
||||||
notes: e2n(v.notes)
|
notes: e2n(v.notes)
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return fail(400, { error: (e as Error).message });
|
||||||
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
delete: async ({ locals, params }) => {
|
delete: async ({ locals, params }) => {
|
||||||
if (!locals.company) throw error(401);
|
if (!locals.company) throw error(401);
|
||||||
|
try {
|
||||||
await softDeleteProperty(locals.company.id, params.id);
|
await softDeleteProperty(locals.company.id, params.id);
|
||||||
|
} catch (e) {
|
||||||
|
return fail(400, { error: (e as Error).message });
|
||||||
|
}
|
||||||
throw redirect(303, '/properties');
|
throw redirect(303, '/properties');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,19 @@
|
|||||||
<input id="kind" name="kind" value={p.kind ?? ''}
|
<input id="kind" name="kind" value={p.kind ?? ''}
|
||||||
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" />
|
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="parentId" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Parent property</label>
|
||||||
|
<select id="parentId" name="parentId"
|
||||||
|
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100">
|
||||||
|
<option value="">— Top level (no parent) —</option>
|
||||||
|
{#each data.eligibleParents as opt (opt.id)}
|
||||||
|
<option value={opt.id} selected={opt.id === p.parentId}>{opt.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Sub-properties (e.g. apartments inside a building) inherit roll-up totals from their parent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="countryCode" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country (ISO 2)</label>
|
<label for="countryCode" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country (ISO 2)</label>
|
||||||
<input id="countryCode" name="countryCode" maxlength="2" value={p.countryCode ?? ''}
|
<input id="countryCode" name="countryCode" maxlength="2" value={p.countryCode ?? ''}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { listAssets } from '$lib/server/services/assets';
|
import { listAssets } from '$lib/server/services/assets';
|
||||||
|
import { getDescendantIds } from '$lib/server/services/properties';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
||||||
if (!locals.company) throw error(401);
|
if (!locals.company) throw error(401);
|
||||||
const rows = await listAssets({ companyId: locals.company.id, propertyId: params.id });
|
const includeDescendants = url.searchParams.get('descendants') === '1';
|
||||||
return { assets: rows };
|
const propertyIds = includeDescendants
|
||||||
|
? await getDescendantIds(locals.company.id, params.id)
|
||||||
|
: [params.id];
|
||||||
|
const rows = await listAssets({ companyId: locals.company.id, propertyIds });
|
||||||
|
return { assets: rows, includeDescendants, descendantCount: propertyIds.length };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
let { data }: { data: PageData } = $props();
|
import type { LayoutData } from '../$types';
|
||||||
|
let { data }: { data: PageData & LayoutData } = $props();
|
||||||
const propId = $derived(data.property.id);
|
const propId = $derived(data.property.id);
|
||||||
|
|
||||||
// Group assets by their room (including an "Unassigned" bucket).
|
// Group assets by their room (including an "Unassigned" bucket).
|
||||||
@@ -31,8 +32,37 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
{#if data.childCount > 0}
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="text-gray-700 dark:text-gray-300">
|
||||||
|
{#if data.includeDescendants}
|
||||||
|
<span class="font-medium">{data.property.name}</span> + sub-properties
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.descendantCount} total)</span>
|
||||||
|
{:else}
|
||||||
|
<span class="font-medium">{data.property.name}</span>
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.childCount} sub-properties hidden)</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div role="tablist" class="inline-flex rounded-md border border-gray-200 bg-gray-50 p-0.5 text-xs font-medium dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
{#each [{ v: '', l: 'This property' }, { v: '1', l: 'Include sub-properties' }] as opt}
|
||||||
|
{@const active = (data.includeDescendants ? '1' : '') === opt.v}
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
href={opt.v ? '?descendants=1' : '?'}
|
||||||
|
aria-selected={active}
|
||||||
|
class="rounded px-2 py-1 transition {active
|
||||||
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}"
|
||||||
|
>
|
||||||
|
{opt.l}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{data.assets.length} asset{data.assets.length === 1 ? '' : 's'} at this property.</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{data.assets.length} asset{data.assets.length === 1 ? '' : 's'}{data.includeDescendants ? ' across the property tree' : ' at this property'}.</p>
|
||||||
<a href="/assets/new?property={propId}" class="rounded-md bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700">+ Add asset</a>
|
<a href="/assets/new?property={propId}" class="rounded-md bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700">+ Add asset</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { companies } from '$lib/server/db/schema/tenancy';
|
|||||||
import {
|
import {
|
||||||
createExpense,
|
createExpense,
|
||||||
deleteExpense,
|
deleteExpense,
|
||||||
listExpensesForProperty,
|
listExpensesForProperties,
|
||||||
monthlySeriesForProperty,
|
monthlySeriesForProperties,
|
||||||
summaryForProperty,
|
summaryForProperties,
|
||||||
updateExpense,
|
updateExpense,
|
||||||
type ChartRange,
|
type ChartRange,
|
||||||
type ExpenseKind
|
type ExpenseKind
|
||||||
} from '$lib/server/services/expenses';
|
} from '$lib/server/services/expenses';
|
||||||
|
import { getDescendantIds } from '$lib/server/services/properties';
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
|
||||||
const KINDS = [
|
const KINDS = [
|
||||||
@@ -70,8 +71,17 @@ function parseRange(raw: string | null): ChartRange {
|
|||||||
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
||||||
const { company } = requireCompany(locals);
|
const { company } = requireCompany(locals);
|
||||||
const range = parseRange(url.searchParams.get('range'));
|
const range = parseRange(url.searchParams.get('range'));
|
||||||
|
const includeDescendants = url.searchParams.get('descendants') === '1';
|
||||||
|
|
||||||
|
// Single-property reads still go through `[params.id]`; tree reads expand via
|
||||||
|
// the recursive CTE in services/properties. The query already validates the
|
||||||
|
// company scope on every descendant.
|
||||||
|
const propertyIds = includeDescendants
|
||||||
|
? await getDescendantIds(company.id, params.id)
|
||||||
|
: [params.id];
|
||||||
|
|
||||||
const [expenses, accounts, series, summary, [companyRow]] = await Promise.all([
|
const [expenses, accounts, series, summary, [companyRow]] = await Promise.all([
|
||||||
listExpensesForProperty(company.id, params.id),
|
listExpensesForProperties(propertyIds),
|
||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
id: propertyAccounts.id,
|
id: propertyAccounts.id,
|
||||||
@@ -82,8 +92,8 @@ export const load: PageServerLoad = async ({ locals, params, url }) => {
|
|||||||
})
|
})
|
||||||
.from(propertyAccounts)
|
.from(propertyAccounts)
|
||||||
.where(eq(propertyAccounts.propertyId, params.id)),
|
.where(eq(propertyAccounts.propertyId, params.id)),
|
||||||
monthlySeriesForProperty(company.id, params.id, ['electricity', 'water'], range),
|
monthlySeriesForProperties(propertyIds, ['electricity', 'water'], range),
|
||||||
summaryForProperty(company.id, params.id, 365),
|
summaryForProperties(propertyIds, 365),
|
||||||
db.select({ settings: companies.settings }).from(companies).where(eq(companies.id, company.id)).limit(1)
|
db.select({ settings: companies.settings }).from(companies).where(eq(companies.id, company.id)).limit(1)
|
||||||
]);
|
]);
|
||||||
const defaultCurrency = parseSettings(companyRow?.settings ?? null).default_currency ?? 'USD';
|
const defaultCurrency = parseSettings(companyRow?.settings ?? null).default_currency ?? 'USD';
|
||||||
@@ -93,7 +103,9 @@ export const load: PageServerLoad = async ({ locals, params, url }) => {
|
|||||||
chartSeries: series,
|
chartSeries: series,
|
||||||
chartRange: range,
|
chartRange: range,
|
||||||
summary,
|
summary,
|
||||||
defaultCurrency
|
defaultCurrency,
|
||||||
|
includeDescendants,
|
||||||
|
descendantCount: propertyIds.length
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,23 @@
|
|||||||
import ExpenseChart from '$lib/components/ExpenseChart.svelte';
|
import ExpenseChart from '$lib/components/ExpenseChart.svelte';
|
||||||
import { EXPENSE_KINDS, EXPENSE_KIND_LABEL, type ExpenseKind } from '$lib/expenses';
|
import { EXPENSE_KINDS, EXPENSE_KIND_LABEL, type ExpenseKind } from '$lib/expenses';
|
||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
|
import type { LayoutData } from '../$types';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData & LayoutData; form: ActionData } = $props();
|
||||||
|
|
||||||
|
function withParams(extra: Record<string, string | undefined>): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const base = {
|
||||||
|
range: String(data.chartRange),
|
||||||
|
descendants: data.includeDescendants ? '1' : ''
|
||||||
|
} as Record<string, string>;
|
||||||
|
const merged = { ...base, ...extra };
|
||||||
|
for (const [k, v] of Object.entries(merged)) {
|
||||||
|
if (v) params.set(k, v);
|
||||||
|
}
|
||||||
|
const qs = params.toString();
|
||||||
|
return qs ? `?${qs}` : '?';
|
||||||
|
}
|
||||||
|
|
||||||
let showForm = $state(false);
|
let showForm = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
@@ -39,6 +54,35 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
{#if data.childCount > 0}
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="text-gray-700 dark:text-gray-300">
|
||||||
|
{#if data.includeDescendants}
|
||||||
|
<span class="font-medium">{data.property.name}</span> + sub-properties
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.descendantCount} total)</span>
|
||||||
|
{:else}
|
||||||
|
<span class="font-medium">{data.property.name}</span>
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.childCount} sub-properties hidden)</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div role="tablist" class="inline-flex rounded-md border border-gray-200 bg-gray-50 p-0.5 text-xs font-medium dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
{#each [{ v: '', l: 'This property' }, { v: '1', l: 'Include sub-properties' }] as opt}
|
||||||
|
{@const active = (data.includeDescendants ? '1' : '') === opt.v}
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
href={withParams({ descendants: opt.v || undefined })}
|
||||||
|
aria-selected={active}
|
||||||
|
class="rounded px-2 py-1 transition {active
|
||||||
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}"
|
||||||
|
>
|
||||||
|
{opt.l}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Chart: electricity + water -->
|
<!-- Chart: electricity + water -->
|
||||||
<section class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
<section class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||||
<div class="mb-3 flex flex-wrap items-baseline justify-between gap-3">
|
<div class="mb-3 flex flex-wrap items-baseline justify-between gap-3">
|
||||||
@@ -52,7 +96,7 @@
|
|||||||
{@const active = String(data.chartRange) === String(opt.v)}
|
{@const active = String(data.chartRange) === String(opt.v)}
|
||||||
<a
|
<a
|
||||||
role="tab"
|
role="tab"
|
||||||
href="?range={opt.v}"
|
href={withParams({ range: String(opt.v) })}
|
||||||
aria-selected={active}
|
aria-selected={active}
|
||||||
class="rounded px-2 py-1 transition {active
|
class="rounded px-2 py-1 transition {active
|
||||||
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import {
|
||||||
|
listEventsForProperties,
|
||||||
|
listSchedulesForProperties
|
||||||
|
} from '$lib/server/services/maintenance';
|
||||||
|
import { getDescendantIds } from '$lib/server/services/properties';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
||||||
|
if (!locals.company) throw error(401);
|
||||||
|
const includeDescendants = url.searchParams.get('descendants') === '1';
|
||||||
|
const propertyIds = includeDescendants
|
||||||
|
? await getDescendantIds(locals.company.id, params.id)
|
||||||
|
: [params.id];
|
||||||
|
const [schedules, events] = await Promise.all([
|
||||||
|
listSchedulesForProperties(locals.company.id, propertyIds),
|
||||||
|
listEventsForProperties(locals.company.id, propertyIds)
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
schedules,
|
||||||
|
events,
|
||||||
|
includeDescendants,
|
||||||
|
descendantCount: propertyIds.length
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import type { LayoutData } from '../$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData & LayoutData } = $props();
|
||||||
|
|
||||||
|
function fmtDate(d: Date | string | null | undefined): string {
|
||||||
|
if (!d) return '—';
|
||||||
|
const dt = typeof d === 'string' ? new Date(d) : d;
|
||||||
|
return Number.isNaN(dt.getTime()) ? '—' : dt.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdue = $derived(
|
||||||
|
data.schedules.filter((s) => s.active && s.nextDueAt && new Date(s.nextDueAt) < new Date())
|
||||||
|
);
|
||||||
|
const upcoming = $derived(
|
||||||
|
data.schedules.filter((s) => s.active && !overdue.includes(s))
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#if data.childCount > 0}
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="text-gray-700 dark:text-gray-300">
|
||||||
|
{#if data.includeDescendants}
|
||||||
|
<span class="font-medium">{data.property.name}</span> + sub-properties
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.descendantCount} total)</span>
|
||||||
|
{:else}
|
||||||
|
<span class="font-medium">{data.property.name}</span>
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.childCount} sub-properties hidden)</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div role="tablist" class="inline-flex rounded-md border border-gray-200 bg-gray-50 p-0.5 text-xs font-medium dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
{#each [{ v: '', l: 'This property' }, { v: '1', l: 'Include sub-properties' }] as opt}
|
||||||
|
{@const active = (data.includeDescendants ? '1' : '') === opt.v}
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
href={opt.v ? '?descendants=1' : '?'}
|
||||||
|
aria-selected={active}
|
||||||
|
class="rounded px-2 py-1 transition {active
|
||||||
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}"
|
||||||
|
>
|
||||||
|
{opt.l}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
|
Schedules
|
||||||
|
</h2>
|
||||||
|
{#if data.schedules.length === 0}
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 bg-white p-6 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||||
|
No maintenance schedules on assets here. Set them up from each asset's page.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700/40">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Schedule</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Asset</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Interval</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Next due</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{#each [...overdue, ...upcoming, ...data.schedules.filter((s) => !s.active)] as s (s.id)}
|
||||||
|
{@const isOverdue = overdue.includes(s)}
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||||
|
<td class="px-4 py-2 text-sm font-medium text-gray-900 dark:text-gray-100">{s.name}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<a href="/assets/{s.assetId}" class="hover:text-primary-600 dark:hover:text-primary-400">{s.assetName}</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">every {s.intervalValue} {s.intervalUnit}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{s.kind === 'time' ? fmtDate(s.nextDueAt) : `${s.nextDueUsage ?? '—'} ${s.intervalUnit}`}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-xs">
|
||||||
|
{#if !s.active}
|
||||||
|
<span class="rounded bg-gray-100 px-1.5 py-0.5 text-gray-600 dark:bg-gray-700 dark:text-gray-300">paused</span>
|
||||||
|
{:else if isOverdue}
|
||||||
|
<span class="rounded bg-red-100 px-1.5 py-0.5 font-medium text-red-700 dark:bg-red-900/30 dark:text-red-300">overdue</span>
|
||||||
|
{:else}
|
||||||
|
<span class="rounded bg-emerald-100 px-1.5 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">active</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
|
Recent events
|
||||||
|
</h2>
|
||||||
|
{#if data.events.length === 0}
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 bg-white p-6 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||||
|
No maintenance events recorded yet.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="divide-y divide-gray-200 overflow-hidden rounded-lg border border-gray-200 bg-white dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
{#each data.events as e (e.id)}
|
||||||
|
<li class="flex items-start justify-between gap-3 px-4 py-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{e.scheduleName ?? 'Ad-hoc service'} ·
|
||||||
|
<a href="/assets/{e.assetId}" class="text-primary-600 hover:underline dark:text-primary-400">{e.assetName}</a>
|
||||||
|
</div>
|
||||||
|
{#if e.notes}
|
||||||
|
<p class="mt-0.5 text-gray-500 dark:text-gray-400">{e.notes}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 text-xs text-gray-500 dark:text-gray-400">{fmtDate(e.performedAt)}</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { and, asc, eq, isNull } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db/client';
|
||||||
|
import { properties } from '$lib/server/db/schema/properties';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||||
|
if (!locals.company) throw error(400, 'No active company');
|
||||||
|
const children = await db
|
||||||
|
.select({
|
||||||
|
id: properties.id,
|
||||||
|
name: properties.name,
|
||||||
|
kind: properties.kind,
|
||||||
|
city: properties.city,
|
||||||
|
countryCode: properties.countryCode
|
||||||
|
})
|
||||||
|
.from(properties)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(properties.companyId, locals.company.id),
|
||||||
|
eq(properties.parentId, params.id),
|
||||||
|
isNull(properties.deletedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(properties.name));
|
||||||
|
return { children };
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import type { LayoutData } from '../$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData & LayoutData } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Sub-properties</h2>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Properties whose parent is <strong>{data.property.name}</strong>. Use these for apartments
|
||||||
|
inside a building, sub-buildings on a campus, and so on.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/properties/new?parent={data.property.id}"
|
||||||
|
class="rounded-md bg-primary-600 px-3 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
+ New sub-property
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.children.length === 0}
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 bg-white p-8 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||||
|
No sub-properties yet.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="divide-y divide-gray-200 overflow-hidden rounded-lg border border-gray-200 bg-white dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
{#each data.children as c (c.id)}
|
||||||
|
<li>
|
||||||
|
<a href="/properties/{c.id}" class="flex items-center justify-between px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{c.name}</p>
|
||||||
|
<p class="truncate text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{[c.kind, c.city, c.countryCode].filter(Boolean).join(' · ') || '—'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500">›</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { listInstancesForProperties } from '$lib/server/services/checklists';
|
||||||
|
import { getDescendantIds } from '$lib/server/services/properties';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
||||||
|
if (!locals.company) throw error(401);
|
||||||
|
const includeDescendants = url.searchParams.get('descendants') === '1';
|
||||||
|
const propertyIds = includeDescendants
|
||||||
|
? await getDescendantIds(locals.company.id, params.id)
|
||||||
|
: [params.id];
|
||||||
|
const instances = await listInstancesForProperties(locals.company.id, propertyIds);
|
||||||
|
return { instances, includeDescendants, descendantCount: propertyIds.length };
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import type { LayoutData } from '../$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData & LayoutData } = $props();
|
||||||
|
|
||||||
|
function fmtDate(d: Date | string | null | undefined): string {
|
||||||
|
if (!d) return '—';
|
||||||
|
const dt = typeof d === 'string' ? new Date(d) : d;
|
||||||
|
return Number.isNaN(dt.getTime()) ? '—' : dt.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = $derived(data.instances.filter((i) => !i.completedAt));
|
||||||
|
const completed = $derived(data.instances.filter((i) => i.completedAt));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#if data.childCount > 0}
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="text-gray-700 dark:text-gray-300">
|
||||||
|
{#if data.includeDescendants}
|
||||||
|
<span class="font-medium">{data.property.name}</span> + sub-properties
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.descendantCount} total)</span>
|
||||||
|
{:else}
|
||||||
|
<span class="font-medium">{data.property.name}</span>
|
||||||
|
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400">({data.childCount} sub-properties hidden)</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div role="tablist" class="inline-flex rounded-md border border-gray-200 bg-gray-50 p-0.5 text-xs font-medium dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
{#each [{ v: '', l: 'This property' }, { v: '1', l: 'Include sub-properties' }] as opt}
|
||||||
|
{@const active = (data.includeDescendants ? '1' : '') === opt.v}
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
href={opt.v ? '?descendants=1' : '?'}
|
||||||
|
aria-selected={active}
|
||||||
|
class="rounded px-2 py-1 transition {active
|
||||||
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}"
|
||||||
|
>
|
||||||
|
{opt.l}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
|
Open ({open.length})
|
||||||
|
</h2>
|
||||||
|
{#if open.length === 0}
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 bg-white p-6 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||||
|
No open checklists for this property.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="divide-y divide-gray-200 overflow-hidden rounded-lg border border-gray-200 bg-white dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
{#each open as i (i.id)}
|
||||||
|
<li class="px-4 py-3 text-sm">
|
||||||
|
<a href="/checklists/{i.id}" class="font-medium text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400">
|
||||||
|
{i.title ?? 'Checklist'}
|
||||||
|
</a>
|
||||||
|
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">created {fmtDate(i.createdAt)}</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if completed.length > 0}
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
|
Completed ({completed.length})
|
||||||
|
</h2>
|
||||||
|
<ul class="divide-y divide-gray-200 overflow-hidden rounded-lg border border-gray-200 bg-white dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
{#each completed as i (i.id)}
|
||||||
|
<li class="px-4 py-3 text-sm">
|
||||||
|
<a href="/checklists/{i.id}" class="text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-400">
|
||||||
|
{i.title ?? 'Checklist'}
|
||||||
|
</a>
|
||||||
|
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">completed {fmtDate(i.completedAt)}</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { fail, redirect, error } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { and, eq, isNull } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { db } from '$lib/server/db/client';
|
||||||
|
import { properties } from '$lib/server/db/schema/properties';
|
||||||
import { createProperty } from '$lib/server/services/properties';
|
import { createProperty } from '$lib/server/services/properties';
|
||||||
import type { Actions } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
|
||||||
const PropertySchema = z.object({
|
const PropertySchema = z.object({
|
||||||
name: z.string().trim().min(1, 'Name is required').max(255),
|
name: z.string().trim().min(1, 'Name is required').max(255),
|
||||||
kind: z.string().trim().max(64).optional().or(z.literal('')),
|
kind: z.string().trim().max(64).optional().or(z.literal('')),
|
||||||
|
parentId: z.string().uuid().optional().or(z.literal('')),
|
||||||
addressLine1: z.string().trim().max(255).optional().or(z.literal('')),
|
addressLine1: z.string().trim().max(255).optional().or(z.literal('')),
|
||||||
addressLine2: z.string().trim().max(255).optional().or(z.literal('')),
|
addressLine2: z.string().trim().max(255).optional().or(z.literal('')),
|
||||||
city: z.string().trim().max(128).optional().or(z.literal('')),
|
city: z.string().trim().max(128).optional().or(z.literal('')),
|
||||||
@@ -19,6 +23,17 @@ function emptyToNull(s: string | undefined): string | null {
|
|||||||
return !s ? null : s;
|
return !s ? null : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
|
if (!locals.company) throw error(400, 'No active company');
|
||||||
|
const eligibleParents = await db
|
||||||
|
.select({ id: properties.id, name: properties.name })
|
||||||
|
.from(properties)
|
||||||
|
.where(and(eq(properties.companyId, locals.company.id), isNull(properties.deletedAt)))
|
||||||
|
.orderBy(properties.name);
|
||||||
|
const preselectParentId = url.searchParams.get('parent') ?? '';
|
||||||
|
return { eligibleParents, preselectParentId };
|
||||||
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async ({ request, locals }) => {
|
default: async ({ request, locals }) => {
|
||||||
if (!locals.user || !locals.company) throw error(401, 'Not authenticated');
|
if (!locals.user || !locals.company) throw error(401, 'Not authenticated');
|
||||||
@@ -33,11 +48,13 @@ export const actions: Actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const v = parsed.data;
|
const v = parsed.data;
|
||||||
|
try {
|
||||||
const { id } = await createProperty({
|
const { id } = await createProperty({
|
||||||
companyId: locals.company.id,
|
companyId: locals.company.id,
|
||||||
createdBy: locals.user.id,
|
createdBy: locals.user.id,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
kind: emptyToNull(v.kind),
|
kind: emptyToNull(v.kind),
|
||||||
|
parentId: v.parentId ? v.parentId : null,
|
||||||
addressLine1: emptyToNull(v.addressLine1),
|
addressLine1: emptyToNull(v.addressLine1),
|
||||||
addressLine2: emptyToNull(v.addressLine2),
|
addressLine2: emptyToNull(v.addressLine2),
|
||||||
city: emptyToNull(v.city),
|
city: emptyToNull(v.city),
|
||||||
@@ -47,5 +64,11 @@ export const actions: Actions = {
|
|||||||
notes: emptyToNull(v.notes)
|
notes: emptyToNull(v.notes)
|
||||||
});
|
});
|
||||||
throw redirect(303, `/properties/${id}`);
|
throw redirect(303, `/properties/${id}`);
|
||||||
|
} catch (e) {
|
||||||
|
// SvelteKit redirects throw; let them bubble.
|
||||||
|
const { isRedirect, isHttpError } = await import('@sveltejs/kit');
|
||||||
|
if (isRedirect(e) || isHttpError(e)) throw e;
|
||||||
|
return fail(400, { error: (e as Error).message, values: raw });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { ActionData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
|
|
||||||
let { form }: { form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
let submitting = $state(false);
|
let submitting = $state(false);
|
||||||
|
|
||||||
const v = $derived((form?.values ?? {}) as Record<string, string>);
|
const v = $derived((form?.values ?? {}) as Record<string, string>);
|
||||||
|
const selectedParent = $derived(v.parentId ?? data.preselectParentId ?? '');
|
||||||
|
const parentName = $derived(
|
||||||
|
data.eligibleParents.find((p) => p.id === selectedParent)?.name ?? null
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-2xl space-y-6">
|
<div class="mx-auto max-w-2xl space-y-6">
|
||||||
@@ -13,6 +17,11 @@
|
|||||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">New property</h1>
|
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">New property</h1>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
A property is a place where assets live (a warehouse, an office, a datacenter, a site).
|
A property is a place where assets live (a warehouse, an office, a datacenter, a site).
|
||||||
|
{#if parentName}
|
||||||
|
<span class="ml-1 text-gray-700 dark:text-gray-300">
|
||||||
|
Creating as a sub-property under <strong>{parentName}</strong>.
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,6 +51,17 @@
|
|||||||
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" />
|
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="parentId" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Parent property</label>
|
||||||
|
<select id="parentId" name="parentId"
|
||||||
|
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100">
|
||||||
|
<option value="">— Top level (no parent) —</option>
|
||||||
|
{#each data.eligibleParents as opt (opt.id)}
|
||||||
|
<option value={opt.id} selected={opt.id === selectedParent}>{opt.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label for="addressLine1" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Address line 1</label>
|
<label for="addressLine1" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Address line 1</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user