From 6c89a89050434554ca9627d0d8ace716c35140e1 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 08:36:31 -0500 Subject: [PATCH 01/25] docs: add Phase 0 schedule API/data contract and fixtures Add docs/schedule-api-contract.md (current state, time semantics, schedule data model, validation rules, endpoints, sync behavior, System Name rules, dynamic audio, backward compatibility, per-phase touchpoints, open items, changelog) and docs/fixtures/*.json (daily, weekly, none, midnight-crossing, adjacent, dynamic-audio, and legacy v2 payloads). Kept byte-identical with the radiantwave repo. --- docs/fixtures/schedule-adjacent.json | 59 ++ docs/fixtures/schedule-daily.json | 59 ++ docs/fixtures/schedule-dynamic-audio.json | 53 ++ docs/fixtures/schedule-legacy-v2.json | 30 + docs/fixtures/schedule-midnight-crossing.json | 59 ++ docs/fixtures/schedule-none.json | 7 + docs/fixtures/schedule-weekly.json | 63 ++ docs/schedule-api-contract.md | 546 ++++++++++++++++++ 8 files changed, 876 insertions(+) create mode 100644 docs/fixtures/schedule-adjacent.json create mode 100644 docs/fixtures/schedule-daily.json create mode 100644 docs/fixtures/schedule-dynamic-audio.json create mode 100644 docs/fixtures/schedule-legacy-v2.json create mode 100644 docs/fixtures/schedule-midnight-crossing.json create mode 100644 docs/fixtures/schedule-none.json create mode 100644 docs/fixtures/schedule-weekly.json create mode 100644 docs/schedule-api-contract.md diff --git a/docs/fixtures/schedule-adjacent.json b/docs/fixtures/schedule-adjacent.json new file mode 100644 index 0000000..abfef1d --- /dev/null +++ b/docs/fixtures/schedule-adjacent.json @@ -0,0 +1,59 @@ +{ + "version": "sched-adjacent-0001", + "type": "daily", + "daily": [ + { + "id": "dd111111-1111-4111-8111-111111111111", + "startTime": "06:00", + "startMinutes": 360, + "endTime": "07:00", + "endMinutes": 420, + "overnight": false, + "durationMinutes": 60, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"], + "dynamicAudio": true + }, + { + "id": "dd222222-2222-4222-8222-222222222222", + "startTime": "07:00", + "startMinutes": 420, + "endTime": "08:30", + "endMinutes": 510, + "overnight": false, + "durationMinutes": 90, + "proclamationIds": [ + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b", + "33333333-3333-4333-8333-333333333333" + ], + "dynamicAudio": false + } + ], + "weekly": {}, + "blocks": [ + { + "id": "dd111111-1111-4111-8111-111111111111", + "startTime": "06:00", + "startMinutes": 360, + "endTime": "07:00", + "endMinutes": 420, + "overnight": false, + "durationMinutes": 60, + "dynamicAudio": true, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"] + }, + { + "id": "dd222222-2222-4222-8222-222222222222", + "startTime": "07:00", + "startMinutes": 420, + "endTime": "08:30", + "endMinutes": 510, + "overnight": false, + "durationMinutes": 90, + "dynamicAudio": false, + "proclamationIds": [ + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b", + "33333333-3333-4333-8333-333333333333" + ] + } + ] +} diff --git a/docs/fixtures/schedule-daily.json b/docs/fixtures/schedule-daily.json new file mode 100644 index 0000000..b59deaf --- /dev/null +++ b/docs/fixtures/schedule-daily.json @@ -0,0 +1,59 @@ +{ + "version": "sched-daily-0001", + "type": "daily", + "daily": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "startTime": "07:00", + "startMinutes": 420, + "endTime": "09:00", + "endMinutes": 540, + "overnight": false, + "durationMinutes": 120, + "proclamationIds": [ + "b40fdcb9-703c-420e-af32-a081964c6f08", + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b" + ], + "dynamicAudio": true + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "startTime": "18:00", + "startMinutes": 1080, + "endTime": "20:30", + "endMinutes": 1230, + "overnight": false, + "durationMinutes": 150, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"], + "dynamicAudio": false + } + ], + "weekly": {}, + "blocks": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "startTime": "07:00", + "startMinutes": 420, + "endTime": "09:00", + "endMinutes": 540, + "overnight": false, + "durationMinutes": 120, + "dynamicAudio": true, + "proclamationIds": [ + "b40fdcb9-703c-420e-af32-a081964c6f08", + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b" + ] + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "startTime": "18:00", + "startMinutes": 1080, + "endTime": "20:30", + "endMinutes": 1230, + "overnight": false, + "durationMinutes": 150, + "dynamicAudio": false, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"] + } + ] +} diff --git a/docs/fixtures/schedule-dynamic-audio.json b/docs/fixtures/schedule-dynamic-audio.json new file mode 100644 index 0000000..6568d06 --- /dev/null +++ b/docs/fixtures/schedule-dynamic-audio.json @@ -0,0 +1,53 @@ +{ + "version": "sched-dynamic-0001", + "type": "daily", + "daily": [ + { + "id": "ee111111-1111-4111-8111-111111111111", + "startTime": "08:00", + "startMinutes": 480, + "endTime": "10:00", + "endMinutes": 600, + "overnight": false, + "durationMinutes": 120, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"], + "dynamicAudio": true + }, + { + "id": "ee222222-2222-4222-8222-222222222222", + "startTime": "20:00", + "startMinutes": 1200, + "endTime": "22:00", + "endMinutes": 1320, + "overnight": false, + "durationMinutes": 120, + "proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"], + "dynamicAudio": false + } + ], + "weekly": {}, + "blocks": [ + { + "id": "ee111111-1111-4111-8111-111111111111", + "startTime": "08:00", + "startMinutes": 480, + "endTime": "10:00", + "endMinutes": 600, + "overnight": false, + "durationMinutes": 120, + "dynamicAudio": true, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"] + }, + { + "id": "ee222222-2222-4222-8222-222222222222", + "startTime": "20:00", + "startMinutes": 1200, + "endTime": "22:00", + "endMinutes": 1320, + "overnight": false, + "durationMinutes": 120, + "dynamicAudio": false, + "proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"] + } + ] +} diff --git a/docs/fixtures/schedule-legacy-v2.json b/docs/fixtures/schedule-legacy-v2.json new file mode 100644 index 0000000..10b9190 --- /dev/null +++ b/docs/fixtures/schedule-legacy-v2.json @@ -0,0 +1,30 @@ +{ + "email": "jane@example.com", + "blocks": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "startTime": "07:00", + "startMinutes": 420, + "endTime": "09:00", + "endMinutes": 540, + "overnight": false, + "durationMinutes": 120, + "dynamicAudio": true, + "proclamationIds": [ + "b40fdcb9-703c-420e-af32-a081964c6f08", + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b" + ] + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "startTime": "18:00", + "startMinutes": 1080, + "endTime": "20:30", + "endMinutes": 1230, + "overnight": false, + "durationMinutes": 150, + "dynamicAudio": false, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"] + } + ] +} diff --git a/docs/fixtures/schedule-midnight-crossing.json b/docs/fixtures/schedule-midnight-crossing.json new file mode 100644 index 0000000..8ab4543 --- /dev/null +++ b/docs/fixtures/schedule-midnight-crossing.json @@ -0,0 +1,59 @@ +{ + "version": "sched-midnight-0001", + "type": "daily", + "daily": [ + { + "id": "cc111111-1111-4111-8111-111111111111", + "startTime": "21:00", + "startMinutes": 1260, + "endTime": "09:00", + "endMinutes": 540, + "overnight": true, + "durationMinutes": 720, + "proclamationIds": [ + "b40fdcb9-703c-420e-af32-a081964c6f08", + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b" + ], + "dynamicAudio": true + }, + { + "id": "cc222222-2222-4222-8222-222222222222", + "startTime": "09:00", + "startMinutes": 540, + "endTime": "21:00", + "endMinutes": 1260, + "overnight": false, + "durationMinutes": 720, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"], + "dynamicAudio": false + } + ], + "weekly": {}, + "blocks": [ + { + "id": "cc111111-1111-4111-8111-111111111111", + "startTime": "21:00", + "startMinutes": 1260, + "endTime": "09:00", + "endMinutes": 540, + "overnight": true, + "durationMinutes": 720, + "dynamicAudio": true, + "proclamationIds": [ + "b40fdcb9-703c-420e-af32-a081964c6f08", + "7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b" + ] + }, + { + "id": "cc222222-2222-4222-8222-222222222222", + "startTime": "09:00", + "startMinutes": 540, + "endTime": "21:00", + "endMinutes": 1260, + "overnight": false, + "durationMinutes": 720, + "dynamicAudio": false, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"] + } + ] +} diff --git a/docs/fixtures/schedule-none.json b/docs/fixtures/schedule-none.json new file mode 100644 index 0000000..4121f83 --- /dev/null +++ b/docs/fixtures/schedule-none.json @@ -0,0 +1,7 @@ +{ + "version": "sched-none-0001", + "type": null, + "daily": [], + "weekly": {}, + "blocks": [] +} diff --git a/docs/fixtures/schedule-weekly.json b/docs/fixtures/schedule-weekly.json new file mode 100644 index 0000000..d71b65a --- /dev/null +++ b/docs/fixtures/schedule-weekly.json @@ -0,0 +1,63 @@ +{ + "version": "sched-weekly-0001", + "type": "weekly", + "daily": [], + "weekly": { + "mon": [ + { + "id": "a1111111-1111-4111-8111-111111111111", + "startTime": "06:00", + "startMinutes": 360, + "endTime": "08:00", + "endMinutes": 480, + "overnight": false, + "durationMinutes": 120, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"], + "dynamicAudio": true + } + ], + "tue": [], + "wed": [ + { + "id": "a2222222-2222-4222-8222-222222222222", + "startTime": "12:00", + "startMinutes": 720, + "endTime": "13:00", + "endMinutes": 780, + "overnight": false, + "durationMinutes": 60, + "proclamationIds": ["7c9e1a2b-3d4e-5f60-8a9b-0c1d2e3f4a5b"], + "dynamicAudio": true + } + ], + "thu": [], + "fri": [ + { + "id": "a3333333-3333-4333-8333-333333333333", + "startTime": "18:00", + "startMinutes": 1080, + "endTime": "22:00", + "endMinutes": 1320, + "overnight": false, + "durationMinutes": 240, + "proclamationIds": ["33333333-3333-4333-8333-333333333333"], + "dynamicAudio": false + } + ], + "sat": [], + "sun": [ + { + "id": "a4444444-4444-4444-8444-444444444444", + "startTime": "23:00", + "startMinutes": 1380, + "endTime": "01:00", + "endMinutes": 60, + "overnight": true, + "durationMinutes": 120, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"], + "dynamicAudio": true + } + ] + }, + "blocks": [] +} diff --git a/docs/schedule-api-contract.md b/docs/schedule-api-contract.md new file mode 100644 index 0000000..9e0dda6 --- /dev/null +++ b/docs/schedule-api-contract.md @@ -0,0 +1,546 @@ +# Schedule API & Data Contract + +Status: Phase 0 (contract only — no application code). This document is the +authoritative API/data contract for the multi-phase scheduler overhaul. It is +kept **byte-identical** in both repositories: + +- `radiantwave` (C++/SDL3 Kiosk app) — `docs/schedule-api-contract.md` +- `portal` (SvelteKit/Node) — `docs/schedule-api-contract.md` + +Any change is applied to both copies in the same step, with a dated line +appended to the [Changelog](#changelog). + +Glossary terms (Kiosk, Portal, Proclamation, Category, Group, Block, Schedule, +License Key, System Name) are used with the meanings defined in the scheduler +overhaul brief. + +--- + +## Current state + +This section reflects the code as it exists at the start of Phase 0. It is +descriptive, not aspirational; the target behaviour is defined in the later +sections. + +### Kiosk (`radiantwave`) + +- **License validation.** `POST /api/v2/license` with body + `{"license_key": "...", "email_address": "..."}`. Implemented in + `src/network/api_client.cpp` (`validateLicense`, `parseLicenseJson`) and + consumed by `src/network/license_service.cpp` (`validateStoredLicense`, + `markLicenseVerified`, `isLicenseWithinGracePeriod`). A 2xx response is the + device's "subscribed" flag; a 7-day offline grace window is stored as + `license_last_verified` in `system_kv` (`src/db/db_keys.h`). Response body + currently read: `{key, email_address}`; additional fields are ignored. +- **Schedule fetch.** `GET /api/v2/schedule?email=&licenseKey=` + (`src/network/api_client.cpp`: `fetchSchedule`, `parseScheduleJson`; + `PATH_SCHEDULE = "/schedule"`). Response shape read by the Kiosk: + `{email, blocks: [{id, startMinutes, endMinutes, overnight, durationMinutes, +dynamicAudio, proclamationIds}]}`. `dynamicAudio` is optional on the wire and + defaults to `true` in `parseScheduleJson`. Missing/blank credentials cause an + early `false`. +- **Schedule persistence.** SQLite tables `schedule_blocks` and + `schedule_block_proclamations` (`src/db/database_manager.cpp:185`, + `replaceScheduleBlocks` at `:1105`, `getScheduleBlocks` at `:1169`). Full + replace; block `sort_order` and per-block proclamation `position` preserved. + No day-of-week column and no schedule `type`; the model is implicitly daily. +- **Schedule sync.** `syncScheduleFromApi` (`src/network/proclamation_sync.cpp:172`) + is invoked only as a **best-effort** side effect of + `syncProclamationsFromApi` (`:168`). There is no boot sync, no network-up + sync, no periodic timer, and no conditional GET (`If-None-Match`/ETag). + `syncProclamationsFromApi` is called from `Home::syncProclamationsIfEmpty` + (`src/home.cpp:461`) and `ProclamationOptionsPage::onPush` + (`src/proclamation_options_page.cpp:49`). There is no explicit atomic + schedule cache file; the SQLite database is the durable store. +- **Schedule playback.** `src/scroller_page.cpp` selects the active block by + comparing `timezone::currentLocalMinutes(cfg.timezone)` + (`src/timezone_helper.h`) against each block via + `scroller::minuteInScheduleBlock` (`src/scroller_streams.h:31`). Block + selection is daily-only (no day-of-week). An active block's ordered + `proclamation_ids` replace the local selection + (`src/scroller_streams.cpp:57`). `effectiveDynamicAudio()` + (`src/scroller_page.cpp:302`) forces dynamic audio when the active block has + `dynamic_audio == true`, otherwise the device-wide + `config.audio.dynamic_audio` applies. A block change is latched in + `scheduleChangePending_` and applied at the next proclamation boundary. +- **Settings persistence.** `~/.local/radiantwave/config.yaml` (YAML) via + `ConfigManager` (`src/config/config_manager.cpp`, `src/config/yaml_parser.cpp`, + schema in `src/config/app_config.h`): `timezone`, `audio.dynamic_audio`, + `proclamation.schedule_active` (Kiosk-local on/off toggle, default `false`). + Identity lives in SQLite `system_kv` (`email_address`, `license_key`, + `license_last_verified`). There is no System Name anywhere. +- **Conventions.** `API_BASE_URL = "https://portal.radiantwavetech.com/api/v2"` + (`src/network/api_client.h`). Kiosk API knowledge is mirrored in + `openapi.yaml` at the repo root. + +### Portal (`portal`) + +- **Framework/tooling.** SvelteKit 2 + Svelte 5 + TypeScript + Tailwind v4; + Prisma 7 over PostgreSQL; Vitest. Migrations: Prisma Migrate + (`prisma/schema.prisma`, `prisma/migrations/`, `npm run migrate`, + `npm run deploy`; config in `prisma.config.ts`). API routes under + `src/routes/api/v1/` (API-key gated) and `src/routes/api/v2/` + (unauthenticated). v2 helpers: `src/lib/server/v2-helpers.ts` + (`jsonOk`, `jsonError`, `readJsonBody`, `rateLimited`, `clientIp`); errors + are `{ "message": "" }`. +- **License validation.** `POST /api/v2/license` + (`src/routes/api/v2/license/+server.ts`): validates `license_key` + + `email_address`, requires the key be active and owned by the account, stamps + `LicenseKey.lastValidatedAt`, returns `serializeLicense({key, emailAddress})` + (`src/lib/server/v2-serializers.ts`). A v1 variant exists + (`src/routes/api/v1/license/+server.ts`). +- **Schedule storage.** `Schedule` is **per user** (`userId String @unique`, + `prisma/schema.prisma:263`), daily-only, `0..4` `Block`s. `Block` + (`:275`) stores `startMinutes`/`endMinutes`, `dynamicAudio Boolean +@default(false)`, and ordered `BlockProclamations` (`sortOrder`). The schema + comment states overnight ranges are disallowed in v1, yet the serializer and + Kiosk both support `overnight` — a known inconsistency. +- **Schedule service/validation.** `src/lib/server/schedule.ts` (DB + orchestration: `getOrCreateSchedule`, `listBlocks`, `createBlock`, + `updateBlock`, `deleteBlock`) and the isomorphic + `src/lib/schedule-utils.ts` (`MAX_BLOCKS = 4`, `SNAP_MINUTES = 15`, + `validateBlock`, `findOverlap`, `isOvernight`, `blockDuration`, + `blockSegments`, `formatTime`, `toHm`, `snapMinutes`). Validation enforces a + 4-block maximum on create, `startMinutes !== endMinutes`, at least one + Proclamation, and no overlaps (overnight-aware via linear segments). +- **Schedule read API.** `GET /api/v2/schedule` + (`src/routes/api/v2/schedule/+server.ts`): auth by paired `email` + + `licenseKey` (legacy alias `key`) query params; checks only that the key + belongs to the email's account (entitlement deliberately not checked); rate + limited 10/min/IP; returns `{email, blocks: [serializeScheduleBlock(...)]}` + where `serializeScheduleBlock` (`src/lib/server/v2-serializers.ts:165`) emits + `{id, startTime, startMinutes, endTime, endMinutes, overnight, +durationMinutes, dynamicAudio, proclamationIds}`. +- **Schedule UI.** Protected user route + `src/routes/(protected)/(user)/schedule/` (`+page.server.ts`, + `+page.svelte`) with `src/lib/components/schedule/DaySchedule.svelte` (single + 24h day timeline, drag-to-create) and `BlockModal.svelte`. Dashboard + (`(protected)/(user)/dashboard/`) lists the user's License Keys. No System + Name is displayed or editable. + +### Gaps between current state and target + +- Schedule is per-user, not per-License-Key. +- No `daily`/`weekly` typing; no day-of-week storage or UI. +- No `version`/ETag, no conditional GET. +- Schedule GET requires `email` + `licenseKey`; there is no license-key-only + route, no `/api/v2/licenses/...` namespace, and no system-name route. +- `dynamicAudio` DB default is `false`; the target default for new Blocks is + `true`. +- No System Name field, validation, or display. +- Sync is opportunistic with no boot/network-up/5-minute cadence and no + atomic cache file. +- Portal stores no timezone (matches target) and shows no informational + geoip-derived timezone. + +--- + +## Time semantics + +- Times are **minute precision**, stored as 24-hour `"HH:MM"` strings + representing **local wall-clock** time. AM/PM is display-only. +- Numerators may also be carried as integer minutes-from-midnight + (`startMinutes`/`endMinutes`, `0..1440`) for the Kiosk; the string form is + canonical on the wire. +- A Block is the **half-open interval `[start, end)`**. +- A Block may start on the same minute the previous Block ends (`end 07:00`, + next `start 07:00` is valid and has no gap). +- A Block whose `end` is earlier than its `start` **crosses midnight** and + belongs to its start day. In weekly mode, Sunday's crossing Block spills into + Monday. `start == end` is **invalid** (zero- or full-length ambiguity). +- **Overlaps are invalid**, including spillover from a midnight-crossing Block + into the next day's Blocks (or, in daily mode, into the same list's early + Blocks). Touching boundaries are **not** overlaps. +- Gaps between Blocks are allowed. During a gap the Kiosk plays whatever + Proclamations are selected locally on the Kiosk. +- The Kiosk evaluates Schedules in its own Timezone setting + (`config.timezone`, IANA code). **The payload carries no timezone.** The + Portal may show the user's geoip-derived timezone informationally only. + +--- + +## Schedule data model (JSON) + +Each License Key has **at most one** Schedule. `type` is `"daily"` or +`"weekly"` and determines the mode; there is no separate global mode. **No** +Schedule means `type` is `null`. + +Suggested shape from the brief, adapted to existing field names (`start`/`end` +→ `startTime`/`endTime`, retaining the existing `startMinutes`/`endMinutes` +integers for the Kiosk): + +```json +{ + "version": "", + "type": "daily" | "weekly" | null, + "daily": [ Block, ... ], + "weekly": { "mon": [Block], "tue": [Block], "wed": [Block], + "thu": [Block], "fri": [Block], "sat": [Block], + "sun": [Block] }, + "blocks": [ LegacyBlock, ... ] +} +``` + +Block: + +```json +{ + "id": "3f1c2b7a-9d4e-4c8a-b2f6-1a2b3c4d5e6f", + "startTime": "21:00", + "startMinutes": 1260, + "endTime": "09:00", + "endMinutes": 540, + "overnight": true, + "durationMinutes": 720, + "proclamationIds": ["b40fdcb9-703c-420e-af32-a081964c6f08"], + "dynamicAudio": true +} +``` + +- `version` is an opaque string derived from the Schedule content (see + [Endpoints](#endpoints)). It is implementation-defined and must change + whenever the serialized schedule changes. +- **Only the fields for the active type are populated.** For `type: "daily"`, + `daily` is populated and `weekly` is omitted or empty. For `type: "weekly"`, + `weekly` is populated with all seven keys and `daily` is omitted or empty. + For `type: null`, both are empty/omitted. +- `weekly` always contains all seven lowercase keys `mon`..`sun`; a day with no + Blocks is an empty array. +- `blocks` is the **legacy flattening** retained for older Kiosks (see + [Backward compatibility](#backward-compatibility)). It uses the existing + `LegacyBlock` shape serialized by the current portal + (`{id, startTime, startMinutes, endTime, endMinutes, overnight, +durationMinutes, dynamicAudio, proclamationIds}`). For `type: "weekly"`, + `blocks` is an **empty array** (the server does not know the Kiosk's + timezone and cannot flatten weekly to a single day). +- `proclamationIds` is the ordered play list (order is significant); + duplicates are not meaningful and are de-duplicated by the Portal. +- `dynamicAudio` defaults to `true` for new Blocks created on the Portal. +- Proclamation **content** is never included; the Kiosk fetches content + separately via `/api/v2/proclamations`. Portal users only ever see + title/category/group. + +--- + +## Validation rules + +These are enforced by the Portal (authoritative) and defended against by the +Kiosk. + +1. A Schedule belongs to exactly one License Key; a License Key has at most one + Schedule. +2. `type` is one of `"daily"`, `"weekly"`, or `null`. +3. A Block has `start` and `end` times; `start == end` is invalid. +4. `end < start` denotes a midnight-crossing Block belonging to its start day. +5. Blocks within a day (and the spillover of a midnight-crossing Block into the + following day) must not overlap. Touching boundaries are allowed. +6. Weekly modes apply rules 3–5 independently per day; a `sun` Block crossing + midnight must not overlap `mon` early Blocks. +7. A Block must contain at least one Proclamation. Empty Blocks are rejected + server-side. The Kiosk defensively **skips invalid Blocks** and treats them + as gaps. +8. Gaps are allowed; during a gap local Kiosk selection applies. +9. `dynamicAudio` is a boolean; absent means `true` for new Blocks. +10. Times are minute-precision `HH:MM` strings; the Portal snaps UI input to + `SNAP_MINUTES` (15) with a `MINUTE_STEP` (5) constraint from the existing + helpers. +11. The existing Portal cap of `MAX_BLOCKS = 4` per day list is **retained** + for Phase 0 (see [Open items](#open-items)). + +The Kiosk must not crash on, and must not play, an invalid Block: it skips it. + +--- + +## Endpoints + +The API remains **unauthenticated** (no API keys) for v2. This is a known +limitation and is recorded under [Open items](#open-items). + +All new endpoints live under the existing versioned namespace +`/api/v2/...` to preserve conventions. v1 routes are unchanged. + +### (a) License validation — existing, additive + +`POST /api/v2/license` + +Request (existing fields unchanged; `systemName` additive and optional): + +```json +{ + "license_key": "ABCD-EFGH-IJKL-MNOP", + "email_address": "jane@example.com", + "systemName": "Living Room" +} +``` + +Response (existing fields unchanged; `systemNameStatus` additive): + +```json +{ "key": "ABCD-EFGH-IJKL-MNOP", "email_address": "jane@example.com", "systemNameStatus": "ok" } +``` + +- `systemName` is optional. When present it is trimmed; when absent the stored + name is left untouched. +- `systemNameStatus` ∈ `{"ok", "unchanged", "conflict", "invalid"}`: + - `ok` — name was set or updated. + - `unchanged` — omitted, or identical after trim/case-insensitive compare. + - `conflict` — another License Key on the same account already holds the name. + - `invalid` — trimmed length outside 1–40. +- **Validation itself succeeds regardless of name status.** A non-`ok` status + never fails the license check; the Kiosk treats it as advisory. +- Errors and rate limiting follow the existing endpoint (400 messages, + 413 body cap, 429 with `Retry-After`). + +### (b) Set System Name — Kiosk-facing + +`PUT /api/v2/licenses/{licenseKey}/system-name` + +Request: + +```json +{ "systemName": "Living Room" } +``` + +Responses: + +| Status | Condition | Body | +| --------------------- | ----------------------------------------- | ------------------------------------------------- | +| 200 | Name set/updated (or unchanged) | `{"systemName": "...", "systemNameStatus": "ok"}` | +| 400 `invalid_name` | Trimmed length outside 1–40 | `{"message": "invalid_name"}` | +| 404 `unknown_license` | No License Key matches | `{"message": "unknown_license"}` | +| 409 `name_taken` | Another key on the account holds the name | `{"message": "name_taken"}` | + +- The Kiosk is the **only** writer. The Portal has no rename capability. +- The response body uses the existing v2 error convention + (`{"message": ""}`). +- Rate limited at the existing 10 requests/min/IP convention. + +### (c) Get Schedule — Kiosk-facing, key-only, conditional + +`GET /api/v2/licenses/{licenseKey}/schedule` + +- 200 with `ETag: ""` and the payload from + [Schedule data model](#schedule-data-model-json). A License Key with no + Schedule returns 200 with `type: null` and empty lists. +- `304 Not Modified` when the request's `If-None-Match` matches the current + `ETag`. +- `404` (`unknown_license`) when no License Key matches. +- `version` doubles as the ETag value; the header value is the quoted form. +- The response is the **target** payload (`version`, `type`, `daily`, `weekly`, + `blocks`). +- Rate limited at the existing 10 requests/min/IP convention. + +### (d) Legacy schedule read — existing, evolved additively + +`GET /api/v2/schedule?email=&licenseKey=` remains supported with +its existing auth semantics. Its response is extended **additively** with the +same `version`, `type`, `daily`, and `weekly` fields, while continuing to +populate `email` and `blocks` exactly as before. Old Kiosks ignore the new +fields; new Kiosks may read either endpoint. This endpoint does **not** emit an +ETag and does **not** return 304. + +### (e) Portal-internal endpoints (names & purpose only) + +Finalized in Phases 3 and 4. They are **not** Kiosk-facing. + +- `POST /api/v2/...` / server actions for Schedule CRUD (create/update/delete + Block, set `type`, assign Proclamations) — Phase 3. +- `POST .../copy` to copy a Schedule between License Keys on the same account — + Phase 4. + +Exact paths and payloads are deferred; they will be recorded here when added. + +--- + +## Sync behavior + +Target Kiosk sync (defaults; adjust only if the code forces it): + +- The Kiosk pulls the Schedule: + - on boot, + - on network-up, + - every 5 minutes. +- Pulls use conditional GET (`If-None-Match` / ETag); a `304` leaves the cache + untouched. +- The last Schedule is cached to disk with an **atomic write** and used offline. +- The schedule on/off toggle is **Kiosk-local** (existing + `proclamation.schedule_active`) and is not synced. +- With no cache and no network, the Kiosk behaves as if schedules are off + (local Proclamation selection applies). +- During gaps, or when schedules are off, the local audio selection applies. + +Current-state deltas (to be closed by later phases): no boot/network-up/5-minute +cadence, no ETag, no explicit atomic cache file. These are recorded under +[Touchpoints per phase](#touchpoints-per-phase). + +Identity for sync is the License Key. The legacy schedule endpoint additionally +requires the paired email; the new endpoint uses the License Key alone. + +--- + +## System Name rules + +- A System Name is a human-readable label for a License Key (e.g. + "Living Room"). It is a **label only** — never used as a reference or key. +- Unique **per account**, compared **case-insensitively**. +- Trimmed, **1–40 characters**. +- Set or changed **only via the Kiosk**. The Portal displays it **read-only** + and has no rename capability. +- The Portal falls back to displaying the License Key when no name is set. +- A Kiosk that omits `systemName` never clears an existing name. +- Wire representations: request field `systemName`; status field + `systemNameStatus` ∈ `{"ok","unchanged","conflict","invalid"}` (see + [Endpoints](#endpoints)). + +--- + +## Dynamic audio + +- Each Block has a boolean `dynamicAudio`. +- The Portal defaults it to **`true` for new Blocks**. +- `true` ⇒ the Kiosk uses dynamic audio for that Block. +- `false` ⇒ the Kiosk uses its locally chosen audio. +- During gaps, or when schedules are off, the local audio selection applies + (existing `config.audio.dynamic_audio` / fixed track). +- Kiosk parsing defaults an absent `dynamicAudio` to `true`. + +--- + +## Backward compatibility + +All existing endpoints and payloads keep working for Kiosks already in the +field. New fields are **additive**; no v2 field is removed or renamed, and v1 +is untouched. + +- **Old Kiosk vs new Portal.** An old Kiosk calls + `POST /api/v2/license` (no `systemName`) and + `GET /api/v2/schedule?email&licenseKey`, reading `blocks` and ignoring + unknown fields. The Portal keeps both, keeps the `blocks` shape byte- + compatible, and adds `version`/`type`/`daily`/`weekly` as extra fields. The + old Kiosk is unaffected and ignores `systemNameStatus`. +- **Daily schedules.** For `type: "daily"`, `blocks` is the flattened daily + list; an old Kiosk behaves exactly as today. +- **Weekly schedules.** `blocks` is an **empty array**. An old Kiosk therefore + sees no schedule and falls back to local Proclamation/audio selection. It + cannot interpret weekly schedules; this is accepted and recorded under + [Open items](#open-items). +- **No schedule.** `type: null`, `blocks: []`; both old and new Kiosks fall + back to local selection. +- **Omission of `systemName`** by an old Kiosk never clears an existing name; + `systemNameStatus` is simply ignored. +- The new endpoints are additive; their absence on old Kiosks is irrelevant. + +--- + +## Touchpoints per phase + +File/module candidates each later phase will change. Phase numbering is assumed +from the brief (Phase 3 = Portal schedule CRUD, Phase 4 = Portal schedule copy) +and is recorded under [Open items](#open-items). + +### Kiosk (`radiantwave`) + +- **Phase 1 (Kiosk scheduler UI, storage, sync, playback)** + - `src/network/api_client.h` / `src/network/api_client.cpp` — parse + `version`/`type`/`daily`/`weekly`, ETag/`If-None-Match`, `PUT` + system-name, `systemNameStatus`. + - `src/network/proclamation_sync.h` / `.cpp` — boot/network-up/5-minute + schedule sync, conditional GET, atomic cache. + - `src/network/license_service.h` / `.cpp` — send `systemName`, handle + `systemNameStatus`. + - `src/db/db_keys.h` — System Name / ETag keys; `src/db/database_manager.h` + / `.cpp` — schedule `type`/day-of-week storage, cache. + - `src/scroller_page.h` / `.cpp`, `src/scroller_streams.h` / `.cpp` — + day-of-week Block selection, spillover. + - `src/timezone_helper.h` — local day-of-week. + - `src/config/app_config.h`, `src/config/config_manager.cpp`, + `src/config/yaml_parser.cpp` — settings surface. + - New page(s) for setting the System Name; tests in `tests/` registered in + `CMakeLists.txt`. + +### Portal (`portal`) + +- **Phase 2 (System Name + license validation)** + - `prisma/schema.prisma` + migration — `LicenseKey.systemName` and + case-insensitive uniqueness. + - `src/routes/api/v2/license/+server.ts` — `systemName` / + `systemNameStatus`. + - `src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts` — new. + - `src/lib/server/v2-serializers.ts`, `src/lib/server/v2-helpers.ts`. + - Dashboard read-only display. +- **Phase 3 (Schedule CRUD: daily/weekly, per-License-Key)** + - `prisma/schema.prisma` + migration — Schedule per License Key, `type`, + day-of-week. + - `src/lib/server/schedule.ts`, `src/lib/schedule-utils.ts` — weekly, + per-license orchestration, cross-day validation. + - `src/lib/server/v2-serializers.ts` — `version`/`type`/`daily`/`weekly`. + - `src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts` — new + (ETag/304). + - `src/routes/api/v2/schedule/+server.ts` — additive evolution. + - `src/routes/(protected)/(user)/schedule/*`, + `src/lib/components/schedule/*` — weekly UI, per-license selection. + - Specs: `schedule.spec.ts`, `page.server.spec.ts`, `schedule-utils.spec.ts`. +- **Phase 4 (Schedule copy)** + - Copy service in `src/lib/server/schedule.ts`; copy route/action; UI. +- **All phases** — `docs/openapi.yaml`, this contract, `docs/fixtures/`. + +--- + +## Open items + +Recorded ambiguities and conservative choices. Each is subject to change in a +later phase. + +1. **Phase numbering.** The brief names Phases 3 (CRUD) and 4 (copy) only; + Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here. +2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the + Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no + schedule". +3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained; the + brief does not state a cap. Whether weekly allows 4/day or 4 total is + unresolved. +4. **Field naming.** The brief's `start`/`end` were mapped to the existing + `startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk. +5. **`version` derivation.** Algorithm (content hash vs max `updatedAt` + + shape) deferred to implementation; it must be stable for equal content and + change on mutation. +6. **Unauthenticated key-only access.** The new endpoints authenticate by + License Key alone (per glossary). Anyone who knows a key could read the + schedule or set the name. Mitigated only by the 10/min/IP rate limit. Known + limitation. +7. **`dynamicAudio` default flip.** DB default changes `false` → `true` for + **new** Blocks; existing Blocks are left at their stored values (no + backfill assumed). +8. **Per-user → per-License-Key migration.** Accounts with multiple License + Keys: which key inherits the existing per-user Schedule is unresolved; + conservative default is the account's first/oldest key. +9. **Cross-day spillover validation.** Weekly `sun` spillover into `mon` (and + daily same-list spillover) must be validated; exact error messaging is + deferred. +10. **Account scope for name uniqueness.** "Per account" = the owning User. + Legacy License Keys with only an `email` and no `userId` are an edge case. +11. **Geoip timezone display.** Informational only, not implemented in the + Portal today. +12. **ETag on the legacy endpoint.** The legacy `GET /api/v2/schedule` emits no + ETag and never returns 304, to preserve old behaviour. +13. **`systemNameStatus` on v1.** Not added; v1 is unchanged. +14. **Trim/case representation.** Names are trimmed; uniqueness is + case-insensitive; the stored casing is preserved as provided. +15. **Sync cadence.** The brief's boot/network-up/5-minute defaults are + targets; the current Kiosk has no such timer. +16. **Atomic cache file.** Path and format deferred; SQLite is the current + durable store. +17. **`type` encoding for no schedule.** `type: null`; `daily`/`weekly` absent + or empty. +18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the + legacy schedule endpoint. + +--- + +## Changelog + +- 2026-09-29 — Initial contract created (Phase 0). Documents current state, + target time semantics, schedule data model, validation rules, endpoints + (`/api/v2/licenses/...`, additive license-validation fields), sync behavior, + System Name rules, dynamic audio, backward compatibility, touchpoints, + open items, and fixtures. From 35558b06524f9c58a68485b290ac3116a7fe4064 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 08:53:17 -0500 Subject: [PATCH 02/25] Portal: add reusable 12-hour TimeInput component TimeInput displays and accepts AM/PM times, offers the 5-minute options as suggestions, and allows typing any minute 00-59; its value model is a 24h HH:MM string. Adds isomorphic parseTimeInput/hmToMinutes/fiveMinuteOptions helpers and unit + component tests for them. --- src/lib/components/TimeInput.svelte | 103 ++++++++++++++++++++ src/lib/components/TimeInput.svelte.spec.ts | 66 +++++++++++++ src/lib/schedule-utils.spec.ts | 49 ++++++++++ src/lib/schedule-utils.ts | 56 +++++++++++ 4 files changed, 274 insertions(+) create mode 100644 src/lib/components/TimeInput.svelte create mode 100644 src/lib/components/TimeInput.svelte.spec.ts diff --git a/src/lib/components/TimeInput.svelte b/src/lib/components/TimeInput.svelte new file mode 100644 index 0000000..3bbbd9f --- /dev/null +++ b/src/lib/components/TimeInput.svelte @@ -0,0 +1,103 @@ + + +
+ {#if label} + + {/if} + + + {#each options as minutes (minutes)} + + {/each} + + {#if error} + + {/if} +
diff --git a/src/lib/components/TimeInput.svelte.spec.ts b/src/lib/components/TimeInput.svelte.spec.ts new file mode 100644 index 0000000..2feff75 --- /dev/null +++ b/src/lib/components/TimeInput.svelte.spec.ts @@ -0,0 +1,66 @@ +import { page } from 'vitest/browser'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import TimeInput from './TimeInput.svelte'; + +function mount(props: Record = {}) { + return render(TimeInput, { value: '07:05', label: 'Time', ...props }); +} + +async function type(text: string) { + const input = (await page.getByRole('combobox', { name: 'Time' }).element()) as HTMLInputElement; + input.value = text; + input.dispatchEvent(new Event('input', { bubbles: true })); +} + +describe('TimeInput', () => { + it('displays the 24-hour value as a 12-hour label', async () => { + mount({ value: '21:05' }); + + await expect.element(page.getByRole('combobox', { name: 'Time' })).toHaveValue('9:05 PM'); + }); + + it('calls onchange with a 24-hour value for tolerant input', async () => { + const onchange = vi.fn(); + mount({ onchange }); + + await type('7:05 pm'); + + expect(onchange).toHaveBeenCalledWith('19:05'); + }); + + it('accepts typed minutes that are not 5-minute steps', async () => { + const onchange = vi.fn(); + mount({ value: '00:00', onchange }); + + await type('7:07'); + + expect(onchange).toHaveBeenCalledWith('07:07'); + }); + + it('accepts an hour with no minutes', async () => { + const onchange = vi.fn(); + mount({ onchange }); + + await type('7pm'); + + expect(onchange).toHaveBeenCalledWith('19:00'); + }); + + it('shows clear feedback and reports invalid input', async () => { + const onvalidity = vi.fn(); + mount({ onvalidity }); + + await type('13:00 PM'); + + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + expect(onvalidity).toHaveBeenCalledWith(false); + }); + + it('offers the 5-minute clock options', async () => { + mount(); + + const options = document.querySelectorAll('datalist option'); + expect(options).toHaveLength((24 * 60) / 5); + }); +}); diff --git a/src/lib/schedule-utils.spec.ts b/src/lib/schedule-utils.spec.ts index afa03bd..8d27ef5 100644 --- a/src/lib/schedule-utils.spec.ts +++ b/src/lib/schedule-utils.spec.ts @@ -3,8 +3,11 @@ import { blockDuration, blockSegments, findOverlap, + fiveMinuteOptions, formatTime, + hmToMinutes, isOvernight, + parseTimeInput, snapMinutes, toHm, validateBlock, @@ -44,6 +47,52 @@ describe('toHm', () => { }); }); +describe('hmToMinutes', () => { + it('parses 24-hour HH:MM strings', () => { + expect(hmToMinutes('00:00')).toBe(0); + expect(hmToMinutes('07:05')).toBe(7 * 60 + 5); + expect(hmToMinutes('23:59')).toBe(23 * 60 + 59); + }); + + it('rejects malformed or out-of-range values', () => { + expect(hmToMinutes('7:05')).toBeNull(); + expect(hmToMinutes('24:00')).toBeNull(); + expect(hmToMinutes('12:60')).toBeNull(); + expect(hmToMinutes('')).toBeNull(); + }); +}); + +describe('parseTimeInput', () => { + it('parses tolerant 12-hour input', () => { + expect(parseTimeInput('7:05 pm')).toEqual({ ok: true, minutes: 19 * 60 + 5 }); + expect(parseTimeInput('7:05PM')).toEqual({ ok: true, minutes: 19 * 60 + 5 }); + expect(parseTimeInput('7pm')).toEqual({ ok: true, minutes: 19 * 60 }); + expect(parseTimeInput('7:7')).toEqual({ ok: true, minutes: 7 * 60 + 7 }); + expect(parseTimeInput('12:00 am')).toEqual({ ok: true, minutes: 0 }); + expect(parseTimeInput('12:30 PM')).toEqual({ ok: true, minutes: 12 * 60 + 30 }); + expect(parseTimeInput('11:59 pm')).toEqual({ ok: true, minutes: 23 * 60 + 59 }); + expect(parseTimeInput('0:30')).toEqual({ ok: true, minutes: 30 }); + }); + + it('rejects ambiguous or out-of-range input', () => { + expect(parseTimeInput('13:00 PM').ok).toBe(false); + expect(parseTimeInput('7:70').ok).toBe(false); + expect(parseTimeInput('25:00').ok).toBe(false); + expect(parseTimeInput('').ok).toBe(false); + expect(parseTimeInput('noon').ok).toBe(false); + }); +}); + +describe('fiveMinuteOptions', () => { + it('lists every 5-minute step across the day', () => { + const options = fiveMinuteOptions(); + expect(options[0]).toBe(0); + expect(options[1]).toBe(5); + expect(options[options.length - 1]).toBe(23 * 60 + 55); + expect(options).toHaveLength((24 * 60) / 5); + }); +}); + describe('snapMinutes', () => { it('rounds to the nearest 15 minutes and clamps to the day', () => { expect(snapMinutes(0)).toBe(0); diff --git a/src/lib/schedule-utils.ts b/src/lib/schedule-utils.ts index 2f16e65..935f470 100644 --- a/src/lib/schedule-utils.ts +++ b/src/lib/schedule-utils.ts @@ -45,6 +45,62 @@ export function toHm(minutes: number): string { return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; } +/** + * Parse a 24-hour `HH:MM` string into minutes from midnight. Returns null when + * the input is not a valid 24-hour time. + */ +export function hmToMinutes(hm: string): number | null { + const match = /^(\d{2}):(\d{2})$/.exec(hm.trim()); + if (!match) return null; + const hours = Number(match[1]); + const minutes = Number(match[2]); + if (hours > 23 || minutes > 59) return null; + return hours * 60 + minutes; +} + +export type TimeInputResult = { ok: true; minutes: number } | { ok: false; error: string }; + +/** + * Parse a user-typed 12-hour clock time into minutes from midnight. Tolerant of + * "7:05 pm", "7:05PM", "7pm", "7:7", "12:00 am". A period is optional; when + * present the hour must be 1-12, so "13:00 PM" is rejected. Minutes may be any + * value 00-59, not just the 5-minute steps. + */ +export function parseTimeInput(raw: string): TimeInputResult { + const text = raw.trim().toLowerCase(); + if (!text) return { ok: false, error: 'Enter a time.' }; + + const match = /^(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|a|p)?$/.exec(text); + if (!match) return { ok: false, error: 'Enter a time like 7:05 PM.' }; + + const hour = Number(match[1]); + const minute = match[2] === undefined ? 0 : Number(match[2]); + const period = match[3]; + + if (minute > 59) return { ok: false, error: 'Minutes must be 00-59.' }; + + if (period) { + if (hour < 1 || hour > 12) { + return { ok: false, error: 'Hour must be 1-12 when AM/PM is given.' }; + } + const isPm = period === 'pm' || period === 'p'; + const hours24 = (hour % 12) + (isPm ? 12 : 0); + return { ok: true, minutes: hours24 * 60 + minute }; + } + + if (hour > 23) return { ok: false, error: 'Hour must be 0-23.' }; + return { ok: true, minutes: hour * 60 + minute }; +} + +/** The 5-minute clock options (00:00, 00:05, ..., 23:55) in play order. */ +export function fiveMinuteOptions(): number[] { + const options: number[] = []; + for (let minutes = 0; minutes < MINUTES_PER_DAY; minutes += 5) { + options.push(minutes); + } + return options; +} + /** Snap a minute value to the nearest `SNAP_MINUTES` interval, clamped to 24h. */ export function snapMinutes(minutes: number): number { const snapped = Math.round(minutes / SNAP_MINUTES) * SNAP_MINUTES; From 179df1e2529cb4453cbb9e100268e0ddceb8c302 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 08:53:22 -0500 Subject: [PATCH 03/25] Portal: 12-hour schedule times and dynamicAudio default on Use TimeInput for Block start/end and show the day timeline axis with AM/PM, so no 24-hour time is displayed in the schedule UI. New Blocks now default dynamicAudio to true in the modal and server-side when the field is absent; existing stored Blocks are untouched. --- src/lib/components/schedule/BlockModal.svelte | 145 ++++-------------- .../schedule/BlockModal.svelte.spec.ts | 75 ++++----- .../components/schedule/DaySchedule.svelte | 2 +- src/lib/server/schedule.spec.ts | 30 +++- src/lib/server/schedule.ts | 12 +- .../(protected)/(user)/schedule/+page.svelte | 2 +- 6 files changed, 113 insertions(+), 153 deletions(-) diff --git a/src/lib/components/schedule/BlockModal.svelte b/src/lib/components/schedule/BlockModal.svelte index 9e1e263..239e0c0 100644 --- a/src/lib/components/schedule/BlockModal.svelte +++ b/src/lib/components/schedule/BlockModal.svelte @@ -2,10 +2,12 @@ import { enhance } from '$app/forms'; import type { SubmitFunction } from '@sveltejs/kit'; import ProclamationBrowser from './ProclamationBrowser.svelte'; + import TimeInput from '$lib/components/TimeInput.svelte'; import { - MINUTE_STEP, formatTime, + hmToMinutes, isOvernight, + toHm, validateBlock, type BlockSibling } from '$lib/schedule-utils'; @@ -26,7 +28,7 @@ initialStartMinutes = 9 * 60, initialEndMinutes = 10 * 60, initialProclamationIds = [], - initialDynamicAudio = false, + initialDynamicAudio = true, categories = [], groups = [], proclamations = [], @@ -56,58 +58,38 @@ } = $props(); let step = $state<'details' | 'browse'>('details'); - let startHour = $state('9'); - let startMinute = $state('0'); - let startPeriod = $state<'AM' | 'PM'>('AM'); - let endHour = $state('10'); - let endMinute = $state('0'); - let endPeriod = $state<'AM' | 'PM'>('AM'); + // Seeded from the props in the effect below whenever the modal opens. + let startMinutes = $state(9 * 60); + let endMinutes = $state(10 * 60); + let startValid = $state(true); + let endValid = $state(true); let selectedIds = $state([]); - let dynamicAudio = $state(false); + let dynamicAudio = $state(true); let error = $state(''); - const hourOptions = Array.from({ length: 12 }, (_, i) => String(i + 1)); - const periodOptions: ('AM' | 'PM')[] = ['AM', 'PM']; - - function minuteOptions(current: string): string[] { - const base = Array.from({ length: 60 / MINUTE_STEP }, (_, i) => String(i * MINUTE_STEP)); - if (base.includes(current)) return base; - return [...base, current].sort((a, b) => Number(a) - Number(b)); + function setStart(hm: string) { + const minutes = hmToMinutes(hm); + if (minutes !== null) startMinutes = minutes; } - function to12Hour(totalMinutes: number): { hour: number; minute: number; period: 'AM' | 'PM' } { - const hours24 = Math.floor(totalMinutes / 60) % 24; - return { - hour: hours24 % 12 === 0 ? 12 : hours24 % 12, - minute: totalMinutes % 60, - period: hours24 < 12 ? 'AM' : 'PM' - }; - } - - function to24Hour(hour12: string, minute: string, period: 'AM' | 'PM'): number { - const hours = (Number(hour12) % 12) + (period === 'PM' ? 12 : 0); - return (hours * 60 + Number(minute)) | 0; + function setEnd(hm: string) { + const minutes = hmToMinutes(hm); + if (minutes !== null) endMinutes = minutes; } // Re-seed the fields whenever the modal is (re)opened. $effect(() => { if (!open) return; - const start = to12Hour(initialStartMinutes); - startHour = String(start.hour); - startMinute = String(start.minute); - startPeriod = start.period; - const end = to12Hour(initialEndMinutes); - endHour = String(end.hour); - endMinute = String(end.minute); - endPeriod = end.period; + startMinutes = initialStartMinutes; + endMinutes = initialEndMinutes; + startValid = true; + endValid = true; selectedIds = [...initialProclamationIds]; dynamicAudio = initialDynamicAudio; error = ''; step = 'details'; }); - const startMinutes = $derived(to24Hour(startHour, startMinute, startPeriod)); - const endMinutes = $derived(to24Hour(endHour, endMinute, endPeriod)); const overnight = $derived( startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes }) ); @@ -122,6 +104,7 @@ ); function validate(): string | null { + if (!startValid || !endValid) return 'Enter a valid start and end time.'; const result = validateBlock( { startMinutes, endMinutes, proclamationIds: selectedIds }, siblings, @@ -228,81 +211,19 @@ {#if step === 'details'}
-
- Start time -
- - : - - -
-
+ (startValid = valid)} + /> -
- End time -
- - : - - -
-
+ (endValid = valid)} + />
diff --git a/src/lib/components/schedule/BlockModal.svelte.spec.ts b/src/lib/components/schedule/BlockModal.svelte.spec.ts index 21531df..79fb486 100644 --- a/src/lib/components/schedule/BlockModal.svelte.spec.ts +++ b/src/lib/components/schedule/BlockModal.svelte.spec.ts @@ -46,53 +46,50 @@ function submit() { return submitRef.current({ formData: new FormData(form), cancel: () => {} }); } +async function typeTime(name: string, text: string) { + const input = (await page.getByRole('combobox', { name }).element()) as HTMLInputElement; + input.value = text; + input.dispatchEvent(new Event('input', { bubbles: true })); +} + describe('BlockModal', () => { - it('uses separate hour and minute selects for start and end', async () => { + it('uses a 12-hour TimeInput for start and end', async () => { mount(); - await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toBeInTheDocument(); - await expect - .element(page.getByRole('combobox', { name: /^Start minute$/ })) - .toBeInTheDocument(); - await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toBeInTheDocument(); - await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toBeInTheDocument(); + await expect.element(page.getByRole('combobox', { name: 'Start time' })).toBeInTheDocument(); + await expect.element(page.getByRole('combobox', { name: 'End time' })).toBeInTheDocument(); }); - it('pre-fills the selects from the initial times', async () => { + it('pre-fills the fields from the initial times', async () => { mount({ initialStartMinutes: 7 * 60 + 30, initialEndMinutes: 9 * 60 + 15 }); - await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('7'); - await expect.element(page.getByRole('combobox', { name: /^Start minute$/ })).toHaveValue('30'); - await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('9'); - await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toHaveValue('15'); + await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('7:30 AM'); + await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('9:15 AM'); }); - it('uses a 12-hour clock with an AM/PM select for each time', async () => { + it('shows a 12-hour clock with AM/PM', async () => { mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 0 }); - await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('9'); - await expect.element(page.getByRole('combobox', { name: /^Start period$/ })).toHaveValue('PM'); - await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('12'); - await expect.element(page.getByRole('combobox', { name: /^End period$/ })).toHaveValue('AM'); - - // No 24-hour option exists any more. - await expect - .element(page.getByRole('combobox', { name: /^Start hour$/ })) - .not.toHaveValue('21'); + await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('9:00 PM'); + await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('12:00 AM'); }); - it('recomputes the time when the AM/PM switch changes', async () => { + it('recomputes the time from typed input', async () => { mount({ initialStartMinutes: 9 * 60, initialEndMinutes: 10 * 60 }); - const period = (await page - .getByRole('combobox', { name: /^End period$/ }) - .element()) as HTMLSelectElement; - period.value = 'PM'; - period.dispatchEvent(new Event('change', { bubbles: true })); + await typeTime('End time', '10:00 PM'); await expect.element(page.getByText('9:00 AM – 10:00 PM')).toBeInTheDocument(); }); + it('shows validation feedback for unparseable input', async () => { + mount(); + + await typeTime('Start time', '25:00'); + + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + }); + it('shows the review times and selected proclamation titles', async () => { mount(); @@ -113,7 +110,6 @@ describe('BlockModal', () => { initialEndMinutes: 9 * 60 }); - await page.getByRole('combobox', { name: /^Start hour$/ }).element(); submit(); await expect @@ -124,7 +120,6 @@ describe('BlockModal', () => { it('requires at least one proclamation', async () => { mount({ initialProclamationIds: [] }); - await page.getByRole('combobox', { name: /^Start hour$/ }).element(); submit(); await expect.element(page.getByText('Select at least one proclamation.')).toBeInTheDocument(); @@ -133,7 +128,6 @@ describe('BlockModal', () => { it('does not show an error for a valid selection', async () => { mount(); - await page.getByRole('combobox', { name: /^Start hour$/ }).element(); const result = submit(); expect(typeof result).toBe('function'); @@ -165,21 +159,32 @@ describe('BlockModal', () => { const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ }); await expect.element(checkbox).toBeInTheDocument(); - expect((checkbox.element() as HTMLInputElement).checked).toBe(false); - await expect .element(page.getByText(/Dynamic audio will be enabled for this block/)) .toBeInTheDocument(); }); + it('defaults dynamic audio on for new blocks', async () => { + mount(); + + const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ }); + expect((checkbox.element() as HTMLInputElement).checked).toBe(true); + }); + it('pre-checks dynamic audio when editing a block that has it enabled', async () => { mount({ initialDynamicAudio: true }); const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ }); - await expect.element(checkbox).toBeInTheDocument(); expect((checkbox.element() as HTMLInputElement).checked).toBe(true); }); + it('leaves dynamic audio off when editing a block that has it disabled', async () => { + mount({ initialDynamicAudio: false }); + + const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ }); + expect((checkbox.element() as HTMLInputElement).checked).toBe(false); + }); + it('toggles the dynamic audio flag in the submitted payload', async () => { mount(); @@ -188,6 +193,6 @@ describe('BlockModal', () => { (checkbox.element() as HTMLInputElement).click(); const hidden = document.querySelector('input[name="dynamicAudio"]') as HTMLInputElement; - await expect.poll(() => hidden.value).toBe('true'); + await expect.poll(() => hidden.value).toBe('false'); }); }); diff --git a/src/lib/components/schedule/DaySchedule.svelte b/src/lib/components/schedule/DaySchedule.svelte index 67aea11..f8e9c1c 100644 --- a/src/lib/components/schedule/DaySchedule.svelte +++ b/src/lib/components/schedule/DaySchedule.svelte @@ -104,7 +104,7 @@ class="absolute right-1 -translate-y-1/2 text-xs text-slate-400 dark:text-slate-500" style={`top:${(hour / 24) * 100}%`} > - {String(hour).padStart(2, '0')}:00 + {formatTime(hour * 60)}
{/each}
diff --git a/src/lib/server/schedule.spec.ts b/src/lib/server/schedule.spec.ts index 150af0e..2006c91 100644 --- a/src/lib/server/schedule.spec.ts +++ b/src/lib/server/schedule.spec.ts @@ -90,7 +90,7 @@ describe('createBlock', () => { scheduleId: 's1', startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, - dynamicAudio: false, + dynamicAudio: true, proclamations: { create: [ { proclamationId: 'p1', sortOrder: 0 }, @@ -101,6 +101,32 @@ describe('createBlock', () => { }); }); + it('defaults dynamic audio on when the field is absent', async () => { + blockFindMany.mockResolvedValue([]); + blockCreate.mockResolvedValue({ id: 'new-block' }); + + await createBlock('u1', validInput); + + expect(blockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ dynamicAudio: true }) + }) + ); + }); + + it('persists dynamic audio off when the field is explicitly false', async () => { + blockFindMany.mockResolvedValue([]); + blockCreate.mockResolvedValue({ id: 'new-block' }); + + await createBlock('u1', { ...validInput, dynamicAudio: false }); + + expect(blockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ dynamicAudio: false }) + }) + ); + }); + it('persists the dynamic audio flag', async () => { blockFindMany.mockResolvedValue([]); blockCreate.mockResolvedValue({ id: 'new-block' }); @@ -168,7 +194,7 @@ describe('updateBlock', () => { data: { startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, - dynamicAudio: false, + dynamicAudio: true, proclamations: { deleteMany: {}, create: [{ proclamationId: 'p1', sortOrder: 0 }] diff --git a/src/lib/server/schedule.ts b/src/lib/server/schedule.ts index 48149b7..4023d94 100644 --- a/src/lib/server/schedule.ts +++ b/src/lib/server/schedule.ts @@ -25,6 +25,14 @@ function uniqueIds(ids: string[]): string[] { return [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))]; } +/** + * New Blocks default `dynamicAudio` to true when the field is absent. Existing + * stored Blocks are never rewritten by this default. + */ +function dynamicAudioOrDefault(input: BlockInput): boolean { + return input.dynamicAudio !== false; +} + /** Ensure the user has a Schedule row, creating the singleton on first use. */ export async function getOrCreateSchedule(userId: string) { const existing = await prisma.schedule.findUnique({ where: { userId } }); @@ -67,7 +75,7 @@ export async function createBlock(userId: string, input: BlockInput): Promise ({ proclamationId, @@ -105,7 +113,7 @@ export async function updateBlock( data: { startMinutes: input.startMinutes, endMinutes: input.endMinutes, - dynamicAudio: input.dynamicAudio === true, + dynamicAudio: dynamicAudioOrDefault(input), proclamations: { deleteMany: {}, create: proclamationIds.map((proclamationId, index) => ({ diff --git a/src/routes/(protected)/(user)/schedule/+page.svelte b/src/routes/(protected)/(user)/schedule/+page.svelte index 1f37e36..0ecc0df 100644 --- a/src/routes/(protected)/(user)/schedule/+page.svelte +++ b/src/routes/(protected)/(user)/schedule/+page.svelte @@ -21,7 +21,7 @@ initialStart = startMinutes; initialEnd = endMinutes; initialIds = []; - initialDynamicAudio = false; + initialDynamicAudio = true; modalOpen = true; } From 1d76e8dff956ea2b285c9ef9ce9a927bf4155c2d Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 09:51:48 -0500 Subject: [PATCH 04/25] Portal: add nullable System Name to LicenseKey with per-account unique index Adds systemName (as typed, trimmed) and systemNameLower (case-folded) to LicenseKey plus a unique index on (userId, systemNameLower) so names are case-insensitively unique per account when non-null. Additive and reversible; migration tested up/down. Regenerates the Prisma client. --- .../migration.sql | 6 ++ prisma/schema.prisma | 7 ++ src/lib/generated/prisma/internal/class.ts | 8 +- .../prisma/internal/prismaNamespace.ts | 4 +- .../prisma/internal/prismaNamespaceBrowser.ts | 4 +- src/lib/generated/prisma/models/LicenseKey.ts | 80 ++++++++++++++++++- 6 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20260929120000_add_license_key_system_name/migration.sql diff --git a/prisma/migrations/20260929120000_add_license_key_system_name/migration.sql b/prisma/migrations/20260929120000_add_license_key_system_name/migration.sql new file mode 100644 index 0000000..c8d0f63 --- /dev/null +++ b/prisma/migrations/20260929120000_add_license_key_system_name/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "LicenseKey" ADD COLUMN "systemName" TEXT, +ADD COLUMN "systemNameLower" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "LicenseKey_userId_systemNameLower_key" ON "LicenseKey"("userId", "systemNameLower"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4243932..61f7185 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -205,10 +205,17 @@ model LicenseKey { userId String? user User? @relation(fields: [userId], references: [id], onDelete: SetNull) email String? + // Human-readable label set only by the Kiosk (e.g. "Living Room"). Trimmed, + // 1-40 chars. Never a reference or foreign key; the License Key is the only + // identifier. `systemNameLower` is a case-folded copy used to enforce + // per-account, case-insensitive uniqueness when non-null. + systemName String? + systemNameLower String? // Legacy subscription level. Retained in the database but unmanaged; hidden // from the client and left NULL. level String? @ignore + @@unique([userId, systemNameLower]) @@index([userId]) @@index([email]) } diff --git a/src/lib/generated/prisma/internal/class.ts b/src/lib/generated/prisma/internal/class.ts index b61ea9d..34c269c 100644 --- a/src/lib/generated/prisma/internal/class.ts +++ b/src/lib/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.8.0", "engineVersion": "3c6e192761c0362d496ed980de936e2f3cebcd3a", "activeProvider": "postgresql", - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/lib/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n roleId Int? @default(4) // New users default to \"User\"\n role UserRole? @relation(fields: [roleId], references: [id])\n sessions Session[]\n accounts Account[]\n profiles Profile[]\n licenseKeys LicenseKey[]\n userProclamations UserProclamations[]\n schedule Schedule?\n\n @@unique([email])\n @@map(\"user\")\n}\n\n// I chose to create a Profile table here because honestly, I didn't want to waste\n// a bunch of time trying to figure out how to customize the default Better-Auth model. \nmodel Profile {\n id String @id @default(uuid())\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n email String?\n firstName String\n lastName String\n phoneNumber String\n ghlContactId String? @map(\"ghl_contact_id\")\n addresses Address[]\n\n @@index([userId])\n @@index([email])\n}\n\nmodel Address {\n id String @id @default(uuid())\n profileId String\n profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n street String\n city String\n state String\n postalCode String\n country String\n type String @default(\"Mailing\")\n isDefaultMailing Boolean @default(false)\n isDefaultBilling Boolean @default(false)\n isDefaultShipping Boolean @default(false)\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel UserRole {\n id Int @id @default(autoincrement())\n name String\n users User[]\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Proclamation {\n id String @id @default(uuid())\n name String @unique\n content String\n solfeggioFrequencyId Int\n solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])\n categoryId Int\n category ProclamationCategory @relation(fields: [categoryId], references: [id])\n groupId Int\n group ProclamationGroup @relation(fields: [groupId], references: [id])\n userProclamations UserProclamations[]\n blockProclamations BlockProclamations[]\n // Legacy subscription-level FK. Retained in the database but unmanaged; the\n // FK constraint is dropped and the value is left NULL. Hidden from the client.\n subscriptionLevelId Int? @ignore\n}\n\nmodel ProclamationGroup {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\nmodel UserProclamations {\n userId String\n user User @relation(fields: [userId], references: [id])\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([userId, proclamationId])\n}\n\nmodel SolfeggioFrequency {\n id Int @id @default(autoincrement())\n name String @unique\n value Int @unique\n proclamations Proclamation[]\n}\n\nmodel ProclamationCategory {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\n// Archive of the former child sub-categories removed when the\n// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.\n// `@@ignore` keeps it out of the generated client while Prisma remains aware\n// of the table so `migrate dev` does not report it as an untracked table.\nmodel ProclamationCategoryLegacy {\n id Int @id\n name String\n parentId Int?\n sortOrder Int @default(0)\n\n @@map(\"ProclamationCategoryLegacy\")\n @@ignore\n}\n\n// Archive of the category->subscription-level mappings for the child\n// categories removed when the category hierarchy was flattened. Kept with\n// `@@ignore` so Prisma remains aware of the table but it is excluded from the\n// generated client and Prisma's own migrations.\nmodel ProclamationCategorySubscriptionLevelLegacy {\n categoryId Int\n subscriptionLevelId Int\n\n @@id([categoryId, subscriptionLevelId])\n @@map(\"ProclamationCategorySubscriptionLevelLegacy\")\n @@ignore\n}\n\nmodel LicenseKey {\n id String @id @default(uuid())\n key String @unique\n active Boolean\n createdAt DateTime @default(now())\n lastValidatedAt DateTime?\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: SetNull)\n email String?\n // Legacy subscription level. Retained in the database but unmanaged; hidden\n // from the client and left NULL.\n level String? @ignore\n\n @@index([userId])\n @@index([email])\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n keyPrefix String\n keyHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime?\n revokedAt DateTime?\n}\n\n// Portal-editable 7-color palette per canonical solfeggio frequency id (1-9).\n// `solfeggioFrequencyId` is the canonical id the kiosk uses (see\n// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.\nmodel ColorPalette {\n solfeggioFrequencyId Int @id\n colors Json // [\"#RRGGBB\", ... x7]\n\n @@map(\"ColorPalette\")\n}\n\n// Append-only audit trail of database mutations. Written centrally by the\n// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.\nmodel AuditLog {\n id String @id @default(uuid())\n createdAt DateTime @default(now())\n userId String?\n email String?\n roleId Int?\n roleName String?\n page String\n action String\n entity String?\n entityId String?\n before Json?\n after Json?\n ip String?\n\n @@index([createdAt])\n @@index([roleId])\n @@index([action])\n @@index([page])\n @@map(\"AuditLog\")\n}\n\n// A user's daily listening schedule. Exactly one per user (enforced by the\n// unique userId); contains 0-4 time Blocks. Times are wall-clock minutes from\n// midnight (0-1440), interpreted by the kiosk in the user's configured\n// timezone. The portal never stores a timezone.\nmodel Schedule {\n id String @id @default(uuid())\n userId String @unique\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n blocks Block[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\n// A single non-overlapping time range within a Schedule, plus the ordered set\n// of selected Proclamations to play during it. Overnight ranges are disallowed\n// in v1, so startMinutes is always less than endMinutes.\nmodel Block {\n id String @id @default(uuid())\n scheduleId String\n schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)\n startMinutes Int\n endMinutes Int\n // When true, the kiosk enables dynamic audio for the duration of this block,\n // overriding any audio selections made on the device.\n dynamicAudio Boolean @default(false)\n proclamations BlockProclamations[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([scheduleId])\n}\n\n// Join table: the Proclamations selected for a Block. Carries the Proclamation\n// content for playback via the API; the portal UI only ever reads\n// title/category/group.\nmodel BlockProclamations {\n blockId String\n block Block @relation(fields: [blockId], references: [id], onDelete: Cascade)\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([blockId, proclamationId])\n @@index([proclamationId])\n}\n", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/lib/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n roleId Int? @default(4) // New users default to \"User\"\n role UserRole? @relation(fields: [roleId], references: [id])\n sessions Session[]\n accounts Account[]\n profiles Profile[]\n licenseKeys LicenseKey[]\n userProclamations UserProclamations[]\n schedule Schedule?\n\n @@unique([email])\n @@map(\"user\")\n}\n\n// I chose to create a Profile table here because honestly, I didn't want to waste\n// a bunch of time trying to figure out how to customize the default Better-Auth model. \nmodel Profile {\n id String @id @default(uuid())\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n email String?\n firstName String\n lastName String\n phoneNumber String\n ghlContactId String? @map(\"ghl_contact_id\")\n addresses Address[]\n\n @@index([userId])\n @@index([email])\n}\n\nmodel Address {\n id String @id @default(uuid())\n profileId String\n profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n street String\n city String\n state String\n postalCode String\n country String\n type String @default(\"Mailing\")\n isDefaultMailing Boolean @default(false)\n isDefaultBilling Boolean @default(false)\n isDefaultShipping Boolean @default(false)\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel UserRole {\n id Int @id @default(autoincrement())\n name String\n users User[]\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Proclamation {\n id String @id @default(uuid())\n name String @unique\n content String\n solfeggioFrequencyId Int\n solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])\n categoryId Int\n category ProclamationCategory @relation(fields: [categoryId], references: [id])\n groupId Int\n group ProclamationGroup @relation(fields: [groupId], references: [id])\n userProclamations UserProclamations[]\n blockProclamations BlockProclamations[]\n // Legacy subscription-level FK. Retained in the database but unmanaged; the\n // FK constraint is dropped and the value is left NULL. Hidden from the client.\n subscriptionLevelId Int? @ignore\n}\n\nmodel ProclamationGroup {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\nmodel UserProclamations {\n userId String\n user User @relation(fields: [userId], references: [id])\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([userId, proclamationId])\n}\n\nmodel SolfeggioFrequency {\n id Int @id @default(autoincrement())\n name String @unique\n value Int @unique\n proclamations Proclamation[]\n}\n\nmodel ProclamationCategory {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\n// Archive of the former child sub-categories removed when the\n// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.\n// `@@ignore` keeps it out of the generated client while Prisma remains aware\n// of the table so `migrate dev` does not report it as an untracked table.\nmodel ProclamationCategoryLegacy {\n id Int @id\n name String\n parentId Int?\n sortOrder Int @default(0)\n\n @@map(\"ProclamationCategoryLegacy\")\n @@ignore\n}\n\n// Archive of the category->subscription-level mappings for the child\n// categories removed when the category hierarchy was flattened. Kept with\n// `@@ignore` so Prisma remains aware of the table but it is excluded from the\n// generated client and Prisma's own migrations.\nmodel ProclamationCategorySubscriptionLevelLegacy {\n categoryId Int\n subscriptionLevelId Int\n\n @@id([categoryId, subscriptionLevelId])\n @@map(\"ProclamationCategorySubscriptionLevelLegacy\")\n @@ignore\n}\n\nmodel LicenseKey {\n id String @id @default(uuid())\n key String @unique\n active Boolean\n createdAt DateTime @default(now())\n lastValidatedAt DateTime?\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: SetNull)\n email String?\n // Human-readable label set only by the Kiosk (e.g. \"Living Room\"). Trimmed,\n // 1-40 chars. Never a reference or foreign key; the License Key is the only\n // identifier. `systemNameLower` is a case-folded copy used to enforce\n // per-account, case-insensitive uniqueness when non-null.\n systemName String?\n systemNameLower String?\n // Legacy subscription level. Retained in the database but unmanaged; hidden\n // from the client and left NULL.\n level String? @ignore\n\n @@unique([userId, systemNameLower])\n @@index([userId])\n @@index([email])\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n keyPrefix String\n keyHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime?\n revokedAt DateTime?\n}\n\n// Portal-editable 7-color palette per canonical solfeggio frequency id (1-9).\n// `solfeggioFrequencyId` is the canonical id the kiosk uses (see\n// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.\nmodel ColorPalette {\n solfeggioFrequencyId Int @id\n colors Json // [\"#RRGGBB\", ... x7]\n\n @@map(\"ColorPalette\")\n}\n\n// Append-only audit trail of database mutations. Written centrally by the\n// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.\nmodel AuditLog {\n id String @id @default(uuid())\n createdAt DateTime @default(now())\n userId String?\n email String?\n roleId Int?\n roleName String?\n page String\n action String\n entity String?\n entityId String?\n before Json?\n after Json?\n ip String?\n\n @@index([createdAt])\n @@index([roleId])\n @@index([action])\n @@index([page])\n @@map(\"AuditLog\")\n}\n\n// A user's daily listening schedule. Exactly one per user (enforced by the\n// unique userId); contains 0-4 time Blocks. Times are wall-clock minutes from\n// midnight (0-1440), interpreted by the kiosk in the user's configured\n// timezone. The portal never stores a timezone.\nmodel Schedule {\n id String @id @default(uuid())\n userId String @unique\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n blocks Block[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\n// A single non-overlapping time range within a Schedule, plus the ordered set\n// of selected Proclamations to play during it. Overnight ranges are disallowed\n// in v1, so startMinutes is always less than endMinutes.\nmodel Block {\n id String @id @default(uuid())\n scheduleId String\n schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)\n startMinutes Int\n endMinutes Int\n // When true, the kiosk enables dynamic audio for the duration of this block,\n // overriding any audio selections made on the device.\n dynamicAudio Boolean @default(false)\n proclamations BlockProclamations[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([scheduleId])\n}\n\n// Join table: the Proclamations selected for a Block. Carries the Proclamation\n// content for playback via the API; the portal UI only ever reads\n// title/category/group.\nmodel BlockProclamations {\n blockId String\n block Block @relation(fields: [blockId], references: [id], onDelete: Cascade)\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([blockId, proclamationId])\n @@index([proclamationId])\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"role\",\"kind\":\"object\",\"type\":\"UserRole\",\"relationName\":\"UserToUserRole\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"ProfileToUser\"},{\"name\":\"licenseKeys\",\"kind\":\"object\",\"type\":\"LicenseKey\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"ScheduleToUser\"}],\"dbName\":\"user\"},\"Profile\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProfileToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"phoneNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ghlContactId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"ghl_contact_id\"},{\"name\":\"addresses\",\"kind\":\"object\",\"type\":\"Address\",\"relationName\":\"AddressToProfile\"}],\"dbName\":null},\"Address\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profileId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profile\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AddressToProfile\"},{\"name\":\"street\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"city\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"postalCode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"country\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDefaultMailing\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultBilling\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultShipping\",\"kind\":\"scalar\",\"type\":\"Boolean\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"UserRole\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserRole\"}],\"dbName\":null},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Proclamation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"solfeggioFrequency\",\"kind\":\"object\",\"type\":\"SolfeggioFrequency\",\"relationName\":\"ProclamationToSolfeggioFrequency\"},{\"name\":\"categoryId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"category\",\"kind\":\"object\",\"type\":\"ProclamationCategory\",\"relationName\":\"ProclamationToProclamationCategory\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"ProclamationGroup\",\"relationName\":\"ProclamationToProclamationGroup\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"blockProclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockProclamationsToProclamation\"}],\"dbName\":null},\"ProclamationGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationGroup\"}],\"dbName\":null},\"UserProclamations\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SolfeggioFrequency\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToSolfeggioFrequency\"}],\"dbName\":null},\"ProclamationCategory\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationCategory\"}],\"dbName\":null},\"LicenseKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastValidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyPrefix\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ColorPalette\":{\"fields\":[{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":\"ColorPalette\"},\"AuditLog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"roleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"page\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entity\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"before\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"after\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"ip\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"AuditLog\"},\"Schedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ScheduleToUser\"},{\"name\":\"blocks\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Block\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"scheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"startMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"endMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"dynamicAudio\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"BlockProclamations\":{\"fields\":[{\"name\":\"blockId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"block\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"BlockProclamationsToProclamation\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"role\",\"kind\":\"object\",\"type\":\"UserRole\",\"relationName\":\"UserToUserRole\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"ProfileToUser\"},{\"name\":\"licenseKeys\",\"kind\":\"object\",\"type\":\"LicenseKey\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"ScheduleToUser\"}],\"dbName\":\"user\"},\"Profile\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProfileToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"phoneNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ghlContactId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"ghl_contact_id\"},{\"name\":\"addresses\",\"kind\":\"object\",\"type\":\"Address\",\"relationName\":\"AddressToProfile\"}],\"dbName\":null},\"Address\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profileId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profile\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AddressToProfile\"},{\"name\":\"street\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"city\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"postalCode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"country\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDefaultMailing\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultBilling\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultShipping\",\"kind\":\"scalar\",\"type\":\"Boolean\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"UserRole\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserRole\"}],\"dbName\":null},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Proclamation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"solfeggioFrequency\",\"kind\":\"object\",\"type\":\"SolfeggioFrequency\",\"relationName\":\"ProclamationToSolfeggioFrequency\"},{\"name\":\"categoryId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"category\",\"kind\":\"object\",\"type\":\"ProclamationCategory\",\"relationName\":\"ProclamationToProclamationCategory\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"ProclamationGroup\",\"relationName\":\"ProclamationToProclamationGroup\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"blockProclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockProclamationsToProclamation\"}],\"dbName\":null},\"ProclamationGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationGroup\"}],\"dbName\":null},\"UserProclamations\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SolfeggioFrequency\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToSolfeggioFrequency\"}],\"dbName\":null},\"ProclamationCategory\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationCategory\"}],\"dbName\":null},\"LicenseKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastValidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"systemName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"systemNameLower\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyPrefix\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ColorPalette\":{\"fields\":[{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":\"ColorPalette\"},\"AuditLog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"roleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"page\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entity\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"before\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"after\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"ip\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"AuditLog\"},\"Schedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ScheduleToUser\"},{\"name\":\"blocks\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Block\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"scheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"startMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"endMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"dynamicAudio\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"BlockProclamations\":{\"fields\":[{\"name\":\"blockId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"block\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"BlockProclamationsToProclamation\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") config.parameterizationSchema = { - strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"users\",\"_count\",\"role\",\"user\",\"sessions\",\"accounts\",\"profile\",\"addresses\",\"profiles\",\"licenseKeys\",\"proclamations\",\"solfeggioFrequency\",\"category\",\"group\",\"userProclamations\",\"blocks\",\"schedule\",\"block\",\"proclamation\",\"blockProclamations\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Profile.findUnique\",\"Profile.findUniqueOrThrow\",\"Profile.findFirst\",\"Profile.findFirstOrThrow\",\"Profile.findMany\",\"Profile.createOne\",\"Profile.createMany\",\"Profile.createManyAndReturn\",\"Profile.updateOne\",\"Profile.updateMany\",\"Profile.updateManyAndReturn\",\"Profile.upsertOne\",\"Profile.deleteOne\",\"Profile.deleteMany\",\"Profile.groupBy\",\"Profile.aggregate\",\"Address.findUnique\",\"Address.findUniqueOrThrow\",\"Address.findFirst\",\"Address.findFirstOrThrow\",\"Address.findMany\",\"Address.createOne\",\"Address.createMany\",\"Address.createManyAndReturn\",\"Address.updateOne\",\"Address.updateMany\",\"Address.updateManyAndReturn\",\"Address.upsertOne\",\"Address.deleteOne\",\"Address.deleteMany\",\"Address.groupBy\",\"Address.aggregate\",\"Session.findUnique\",\"Session.findUniqueOrThrow\",\"Session.findFirst\",\"Session.findFirstOrThrow\",\"Session.findMany\",\"Session.createOne\",\"Session.createMany\",\"Session.createManyAndReturn\",\"Session.updateOne\",\"Session.updateMany\",\"Session.updateManyAndReturn\",\"Session.upsertOne\",\"Session.deleteOne\",\"Session.deleteMany\",\"Session.groupBy\",\"Session.aggregate\",\"Account.findUnique\",\"Account.findUniqueOrThrow\",\"Account.findFirst\",\"Account.findFirstOrThrow\",\"Account.findMany\",\"Account.createOne\",\"Account.createMany\",\"Account.createManyAndReturn\",\"Account.updateOne\",\"Account.updateMany\",\"Account.updateManyAndReturn\",\"Account.upsertOne\",\"Account.deleteOne\",\"Account.deleteMany\",\"Account.groupBy\",\"Account.aggregate\",\"UserRole.findUnique\",\"UserRole.findUniqueOrThrow\",\"UserRole.findFirst\",\"UserRole.findFirstOrThrow\",\"UserRole.findMany\",\"UserRole.createOne\",\"UserRole.createMany\",\"UserRole.createManyAndReturn\",\"UserRole.updateOne\",\"UserRole.updateMany\",\"UserRole.updateManyAndReturn\",\"UserRole.upsertOne\",\"UserRole.deleteOne\",\"UserRole.deleteMany\",\"UserRole.groupBy\",\"UserRole.aggregate\",\"Verification.findUnique\",\"Verification.findUniqueOrThrow\",\"Verification.findFirst\",\"Verification.findFirstOrThrow\",\"Verification.findMany\",\"Verification.createOne\",\"Verification.createMany\",\"Verification.createManyAndReturn\",\"Verification.updateOne\",\"Verification.updateMany\",\"Verification.updateManyAndReturn\",\"Verification.upsertOne\",\"Verification.deleteOne\",\"Verification.deleteMany\",\"Verification.groupBy\",\"Verification.aggregate\",\"Proclamation.findUnique\",\"Proclamation.findUniqueOrThrow\",\"Proclamation.findFirst\",\"Proclamation.findFirstOrThrow\",\"Proclamation.findMany\",\"Proclamation.createOne\",\"Proclamation.createMany\",\"Proclamation.createManyAndReturn\",\"Proclamation.updateOne\",\"Proclamation.updateMany\",\"Proclamation.updateManyAndReturn\",\"Proclamation.upsertOne\",\"Proclamation.deleteOne\",\"Proclamation.deleteMany\",\"Proclamation.groupBy\",\"Proclamation.aggregate\",\"ProclamationGroup.findUnique\",\"ProclamationGroup.findUniqueOrThrow\",\"ProclamationGroup.findFirst\",\"ProclamationGroup.findFirstOrThrow\",\"ProclamationGroup.findMany\",\"ProclamationGroup.createOne\",\"ProclamationGroup.createMany\",\"ProclamationGroup.createManyAndReturn\",\"ProclamationGroup.updateOne\",\"ProclamationGroup.updateMany\",\"ProclamationGroup.updateManyAndReturn\",\"ProclamationGroup.upsertOne\",\"ProclamationGroup.deleteOne\",\"ProclamationGroup.deleteMany\",\"ProclamationGroup.groupBy\",\"ProclamationGroup.aggregate\",\"UserProclamations.findUnique\",\"UserProclamations.findUniqueOrThrow\",\"UserProclamations.findFirst\",\"UserProclamations.findFirstOrThrow\",\"UserProclamations.findMany\",\"UserProclamations.createOne\",\"UserProclamations.createMany\",\"UserProclamations.createManyAndReturn\",\"UserProclamations.updateOne\",\"UserProclamations.updateMany\",\"UserProclamations.updateManyAndReturn\",\"UserProclamations.upsertOne\",\"UserProclamations.deleteOne\",\"UserProclamations.deleteMany\",\"UserProclamations.groupBy\",\"UserProclamations.aggregate\",\"SolfeggioFrequency.findUnique\",\"SolfeggioFrequency.findUniqueOrThrow\",\"SolfeggioFrequency.findFirst\",\"SolfeggioFrequency.findFirstOrThrow\",\"SolfeggioFrequency.findMany\",\"SolfeggioFrequency.createOne\",\"SolfeggioFrequency.createMany\",\"SolfeggioFrequency.createManyAndReturn\",\"SolfeggioFrequency.updateOne\",\"SolfeggioFrequency.updateMany\",\"SolfeggioFrequency.updateManyAndReturn\",\"SolfeggioFrequency.upsertOne\",\"SolfeggioFrequency.deleteOne\",\"SolfeggioFrequency.deleteMany\",\"SolfeggioFrequency.groupBy\",\"SolfeggioFrequency.aggregate\",\"ProclamationCategory.findUnique\",\"ProclamationCategory.findUniqueOrThrow\",\"ProclamationCategory.findFirst\",\"ProclamationCategory.findFirstOrThrow\",\"ProclamationCategory.findMany\",\"ProclamationCategory.createOne\",\"ProclamationCategory.createMany\",\"ProclamationCategory.createManyAndReturn\",\"ProclamationCategory.updateOne\",\"ProclamationCategory.updateMany\",\"ProclamationCategory.updateManyAndReturn\",\"ProclamationCategory.upsertOne\",\"ProclamationCategory.deleteOne\",\"ProclamationCategory.deleteMany\",\"ProclamationCategory.groupBy\",\"ProclamationCategory.aggregate\",\"LicenseKey.findUnique\",\"LicenseKey.findUniqueOrThrow\",\"LicenseKey.findFirst\",\"LicenseKey.findFirstOrThrow\",\"LicenseKey.findMany\",\"LicenseKey.createOne\",\"LicenseKey.createMany\",\"LicenseKey.createManyAndReturn\",\"LicenseKey.updateOne\",\"LicenseKey.updateMany\",\"LicenseKey.updateManyAndReturn\",\"LicenseKey.upsertOne\",\"LicenseKey.deleteOne\",\"LicenseKey.deleteMany\",\"LicenseKey.groupBy\",\"LicenseKey.aggregate\",\"ApiKey.findUnique\",\"ApiKey.findUniqueOrThrow\",\"ApiKey.findFirst\",\"ApiKey.findFirstOrThrow\",\"ApiKey.findMany\",\"ApiKey.createOne\",\"ApiKey.createMany\",\"ApiKey.createManyAndReturn\",\"ApiKey.updateOne\",\"ApiKey.updateMany\",\"ApiKey.updateManyAndReturn\",\"ApiKey.upsertOne\",\"ApiKey.deleteOne\",\"ApiKey.deleteMany\",\"ApiKey.groupBy\",\"ApiKey.aggregate\",\"ColorPalette.findUnique\",\"ColorPalette.findUniqueOrThrow\",\"ColorPalette.findFirst\",\"ColorPalette.findFirstOrThrow\",\"ColorPalette.findMany\",\"ColorPalette.createOne\",\"ColorPalette.createMany\",\"ColorPalette.createManyAndReturn\",\"ColorPalette.updateOne\",\"ColorPalette.updateMany\",\"ColorPalette.updateManyAndReturn\",\"ColorPalette.upsertOne\",\"ColorPalette.deleteOne\",\"ColorPalette.deleteMany\",\"ColorPalette.groupBy\",\"ColorPalette.aggregate\",\"AuditLog.findUnique\",\"AuditLog.findUniqueOrThrow\",\"AuditLog.findFirst\",\"AuditLog.findFirstOrThrow\",\"AuditLog.findMany\",\"AuditLog.createOne\",\"AuditLog.createMany\",\"AuditLog.createManyAndReturn\",\"AuditLog.updateOne\",\"AuditLog.updateMany\",\"AuditLog.updateManyAndReturn\",\"AuditLog.upsertOne\",\"AuditLog.deleteOne\",\"AuditLog.deleteMany\",\"AuditLog.groupBy\",\"AuditLog.aggregate\",\"Schedule.findUnique\",\"Schedule.findUniqueOrThrow\",\"Schedule.findFirst\",\"Schedule.findFirstOrThrow\",\"Schedule.findMany\",\"Schedule.createOne\",\"Schedule.createMany\",\"Schedule.createManyAndReturn\",\"Schedule.updateOne\",\"Schedule.updateMany\",\"Schedule.updateManyAndReturn\",\"Schedule.upsertOne\",\"Schedule.deleteOne\",\"Schedule.deleteMany\",\"Schedule.groupBy\",\"Schedule.aggregate\",\"Block.findUnique\",\"Block.findUniqueOrThrow\",\"Block.findFirst\",\"Block.findFirstOrThrow\",\"Block.findMany\",\"Block.createOne\",\"Block.createMany\",\"Block.createManyAndReturn\",\"Block.updateOne\",\"Block.updateMany\",\"Block.updateManyAndReturn\",\"Block.upsertOne\",\"Block.deleteOne\",\"Block.deleteMany\",\"Block.groupBy\",\"Block.aggregate\",\"BlockProclamations.findUnique\",\"BlockProclamations.findUniqueOrThrow\",\"BlockProclamations.findFirst\",\"BlockProclamations.findFirstOrThrow\",\"BlockProclamations.findMany\",\"BlockProclamations.createOne\",\"BlockProclamations.createMany\",\"BlockProclamations.createManyAndReturn\",\"BlockProclamations.updateOne\",\"BlockProclamations.updateMany\",\"BlockProclamations.updateManyAndReturn\",\"BlockProclamations.upsertOne\",\"BlockProclamations.deleteOne\",\"BlockProclamations.deleteMany\",\"BlockProclamations.groupBy\",\"BlockProclamations.aggregate\",\"AND\",\"OR\",\"NOT\",\"blockId\",\"proclamationId\",\"sortOrder\",\"createdAt\",\"updatedAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"id\",\"scheduleId\",\"startMinutes\",\"endMinutes\",\"dynamicAudio\",\"userId\",\"every\",\"some\",\"none\",\"email\",\"roleId\",\"roleName\",\"page\",\"action\",\"entity\",\"entityId\",\"before\",\"after\",\"ip\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"solfeggioFrequencyId\",\"colors\",\"keyPrefix\",\"keyHash\",\"expiresAt\",\"revokedAt\",\"key\",\"active\",\"lastValidatedAt\",\"name\",\"value\",\"content\",\"categoryId\",\"groupId\",\"identifier\",\"accountId\",\"providerId\",\"accessToken\",\"refreshToken\",\"idToken\",\"accessTokenExpiresAt\",\"refreshTokenExpiresAt\",\"scope\",\"password\",\"token\",\"ipAddress\",\"userAgent\",\"profileId\",\"street\",\"city\",\"state\",\"postalCode\",\"country\",\"type\",\"isDefaultMailing\",\"isDefaultBilling\",\"isDefaultShipping\",\"firstName\",\"lastName\",\"phoneNumber\",\"ghlContactId\",\"emailVerified\",\"image\",\"blockId_proclamationId\",\"userId_proclamationId\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), - graph: "gAi2AbACEgUAAN0EACAHAADeBAAgCAAA3wQAIAsAAOAEACAMAADhBAAgEQAA0QQAIBMAAOIEACDPAgAA3AQAMNACAAAFABDRAgAA3AQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB6wIBAAAAAewCAgCjBAAhhAMBAKEEACGkAyAAxgQAIaUDAQCiBAAhAQAAAAEAIAYDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYQDAQChBAAhAQAAAAMAIBIFAADdBAAgBwAA3gQAIAgAAN8EACALAADgBAAgDAAA4QQAIBEAANEEACATAADiBAAgzwIAANwEADDQAgAABQAQ0QIAANwEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHrAgEAoQQAIewCAgCjBAAhhAMBAKEEACGkAyAAxgQAIaUDAQCiBAAhCQUAAKMHACAHAACkBwAgCAAApQcAIAsAAKYHACAMAACnBwAgEQAAoAcAIBMAAJkHACDsAgAAmgUAIKUDAACaBQAgAwAAAAUAIAEAAAYAMAIAAAEAIAEAAAAFACAMBgAAlAQAIM8CAADbBAAw0AIAAAkAENECAADbBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACH_AkAAkwQAIZMDAQChBAAhlAMBAKIEACGVAwEAogQAIQMGAACYBQAglAMAAJoFACCVAwAAmgUAIAwGAACUBAAgzwIAANsEADDQAgAACQAQ0QIAANsEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAecCAQChBAAh_wJAAJMEACGTAwEAAAABlAMBAKIEACGVAwEAogQAIQMAAAAJACABAAAKADACAAALACARBgAAlAQAIM8CAADaBAAw0AIAAA0AENECAADaBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACGKAwEAoQQAIYsDAQChBAAhjAMBAKIEACGNAwEAogQAIY4DAQCiBAAhjwNAALAEACGQA0AAsAQAIZEDAQCiBAAhkgMBAKIEACEIBgAAmAUAIIwDAACaBQAgjQMAAJoFACCOAwAAmgUAII8DAACaBQAgkAMAAJoFACCRAwAAmgUAIJIDAACaBQAgEQYAAJQEACDPAgAA2gQAMNACAAANABDRAgAA2gQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB5wIBAKEEACGKAwEAoQQAIYsDAQChBAAhjAMBAKIEACGNAwEAogQAIY4DAQCiBAAhjwNAALAEACGQA0AAsAQAIZEDAQCiBAAhkgMBAKIEACEDAAAADQAgAQAADgAwAgAADwAgDAYAANUEACAKAADZBAAgzwIAANgEADDQAgAAEQAQ0QIAANgEADDiAgEAoQQAIecCAQCiBAAh6wIBAKIEACGgAwEAoQQAIaEDAQChBAAhogMBAKEEACGjAwEAogQAIQUGAACYBQAgCgAAogcAIOcCAACaBQAg6wIAAJoFACCjAwAAmgUAIAwGAADVBAAgCgAA2QQAIM8CAADYBAAw0AIAABEAENECAADYBAAw4gIBAAAAAecCAQCiBAAh6wIBAKIEACGgAwEAoQQAIaEDAQChBAAhogMBAKEEACGjAwEAogQAIQMAAAARACABAAASADACAAATACABAAAABQAgDwkAANcEACDPAgAA1gQAMNACAAAWABDRAgAA1gQAMOICAQChBAAhlgMBAKEEACGXAwEAoQQAIZgDAQChBAAhmQMBAKEEACGaAwEAoQQAIZsDAQChBAAhnAMBAKEEACGdAyAAxgQAIZ4DIADGBAAhnwMgAMYEACEBCQAAoQcAIA8JAADXBAAgzwIAANYEADDQAgAAFgAQ0QIAANYEADDiAgEAAAABlgMBAKEEACGXAwEAoQQAIZgDAQChBAAhmQMBAKEEACGaAwEAoQQAIZsDAQChBAAhnAMBAKEEACGdAyAAxgQAIZ4DIADGBAAhnwMgAMYEACEDAAAAFgAgAQAAFwAwAgAAGAAgAQAAABYAIAsGAADVBAAgzwIAANQEADDQAgAAGwAQ0QIAANQEADDVAkAAkwQAIeICAQChBAAh5wIBAKIEACHrAgEAogQAIYEDAQChBAAhggMgAMYEACGDA0AAsAQAIQQGAACYBQAg5wIAAJoFACDrAgAAmgUAIIMDAACaBQAgCwYAANUEACDPAgAA1AQAMNACAAAbABDRAgAA1AQAMNUCQACTBAAh4gIBAAAAAecCAQCiBAAh6wIBAKIEACGBAwEAAAABggMgAMYEACGDA0AAsAQAIQMAAAAbACABAAAcADACAAAdACABAAAABQAgCQYAAJQEACAVAADMBAAgzwIAANMEADDQAgAAIAAQ0QIAANMEADDTAgEAoQQAIdUCQACTBAAh1gJAAJMEACHnAgEAoQQAIQIGAACYBQAgFQAAnAcAIAoGAACUBAAgFQAAzAQAIM8CAADTBAAw0AIAACAAENECAADTBAAw0wIBAKEEACHVAkAAkwQAIdYCQACTBAAh5wIBAKEEACGnAwAA0gQAIAMAAAAgACABAAAhADACAAAiACAODgAAzgQAIA8AAM8EACAQAADQBAAgEQAA0QQAIBYAAMgEACDPAgAAzQQAMNACAAAkABDRAgAAzQQAMOICAQChBAAh-wICAKkEACGEAwEAoQQAIYYDAQChBAAhhwMCAKkEACGIAwIAqQQAIQUOAACdBwAgDwAAngcAIBAAAJ8HACARAACgBwAgFgAAmgcAIA4OAADOBAAgDwAAzwQAIBAAANAEACARAADRBAAgFgAAyAQAIM8CAADNBAAw0AIAACQAENECAADNBAAw4gIBAAAAAfsCAgCpBAAhhAMBAAAAAYYDAQChBAAhhwMCAKkEACGIAwIAqQQAIQMAAAAkACABAAAlADACAAAmACABAAAAJAAgAwAAACQAIAEAACUAMAIAACYAIAEAAAAkACADAAAAJAAgAQAAJQAwAgAAJgAgAQAAACQAIAMAAAAgACABAAAhADACAAAiACAKFAAAywQAIBUAAMwEACDPAgAAygQAMNACAAAuABDRAgAAygQAMNICAQChBAAh0wIBAKEEACHUAgIAqQQAIdUCQACTBAAh1gJAAJMEACECFAAAmwcAIBUAAJwHACALFAAAywQAIBUAAMwEACDPAgAAygQAMNACAAAuABDRAgAAygQAMNICAQChBAAh0wIBAKEEACHUAgIAqQQAIdUCQACTBAAh1gJAAJMEACGmAwAAyQQAIAMAAAAuACABAAAvADACAAAwACAMDQAAyAQAIBMAAMcEACDPAgAAxQQAMNACAAAyABDRAgAAxQQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIeMCAQChBAAh5AICAKkEACHlAgIAqQQAIeYCIADGBAAhAg0AAJoHACATAACZBwAgDA0AAMgEACATAADHBAAgzwIAAMUEADDQAgAAMgAQ0QIAAMUEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAeMCAQChBAAh5AICAKkEACHlAgIAqQQAIeYCIADGBAAhAwAAADIAIAEAADMAMAIAADQAIAEAAAAyACADAAAALgAgAQAALwAwAgAAMAAgAQAAAC4AIAEAAAAgACABAAAALgAgCQYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIQEAAAA7ACABAAAACQAgAQAAAA0AIAEAAAARACABAAAAGwAgAQAAACAAIAEAAAABACADAAAABQAgAQAABgAwAgAAAQAgAwAAAAUAIAEAAAYAMAIAAAEAIAMAAAAFACABAAAGADACAAABACAPBQAAmAcAIAcAAPYGACAIAAD3BgAgCwAA-AYAIAwAAPkGACARAAD6BgAgEwAA-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_wJAAAAAAZMDAQAAAAGUAwEAAAABlQMBAAAAAQEcAACMAQAgCNUCQAAAAAHWAkAAAAAB4gIBAAAAAecCAQAAAAH_AkAAAAABkwMBAAAAAZQDAQAAAAGVAwEAAAABARwAAI4BADABHAAAjgEAMAkGAACGBwAg1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh5wIBAOoEACH_AkAA6QQAIZMDAQDqBAAhlAMBAKAFACGVAwEAoAUAIQIAAAALACAcAACRAQAgCNUCQADpBAAh1gJAAOkEACHiAgEA6gQAIecCAQDqBAAh_wJAAOkEACGTAwEA6gQAIZQDAQCgBQAhlQMBAKAFACECAAAACQAgHAAAkwEAIAIAAAAJACAcAACTAQAgAwAAAAsAICMAAIwBACAkAACRAQAgAQAAAAsAIAEAAAAJACAFBAAAgwcAICsAAIUHACAsAACEBwAglAMAAJoFACCVAwAAmgUAIAvPAgAAwQQAMNACAACaAQAQ0QIAAMEEADDVAkAAhQQAIdYCQACFBAAh4gIBAIMEACHnAgEAgwQAIf8CQACFBAAhkwMBAIMEACGUAwEAlwQAIZUDAQCXBAAhAwAAAAkAIAEAAJkBADAoAACaAQAgAwAAAAkAIAEAAAoAMAIAAAsAIAEAAAAPACABAAAADwAgAwAAAA0AIAEAAA4AMAIAAA8AIAMAAAANACABAAAOADACAAAPACADAAAADQAgAQAADgAwAgAADwAgDgYAAIIHACDVAkAAAAAB1gJAAAAAAeICAQAAAAHnAgEAAAABigMBAAAAAYsDAQAAAAGMAwEAAAABjQMBAAAAAY4DAQAAAAGPA0AAAAABkANAAAAAAZEDAQAAAAGSAwEAAAABARwAAKIBACAN1QJAAAAAAdYCQAAAAAHiAgEAAAAB5wIBAAAAAYoDAQAAAAGLAwEAAAABjAMBAAAAAY0DAQAAAAGOAwEAAAABjwNAAAAAAZADQAAAAAGRAwEAAAABkgMBAAAAAQEcAACkAQAwARwAAKQBADAOBgAAgQcAINUCQADpBAAh1gJAAOkEACHiAgEA6gQAIecCAQDqBAAhigMBAOoEACGLAwEA6gQAIYwDAQCgBQAhjQMBAKAFACGOAwEAoAUAIY8DQACqBQAhkANAAKoFACGRAwEAoAUAIZIDAQCgBQAhAgAAAA8AIBwAAKcBACAN1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh5wIBAOoEACGKAwEA6gQAIYsDAQDqBAAhjAMBAKAFACGNAwEAoAUAIY4DAQCgBQAhjwNAAKoFACGQA0AAqgUAIZEDAQCgBQAhkgMBAKAFACECAAAADQAgHAAAqQEAIAIAAAANACAcAACpAQAgAwAAAA8AICMAAKIBACAkAACnAQAgAQAAAA8AIAEAAAANACAKBAAA_gYAICsAAIAHACAsAAD_BgAgjAMAAJoFACCNAwAAmgUAII4DAACaBQAgjwMAAJoFACCQAwAAmgUAIJEDAACaBQAgkgMAAJoFACAQzwIAAMAEADDQAgAAsAEAENECAADABAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh5wIBAIMEACGKAwEAgwQAIYsDAQCDBAAhjAMBAJcEACGNAwEAlwQAIY4DAQCXBAAhjwNAAKwEACGQA0AArAQAIZEDAQCXBAAhkgMBAJcEACEDAAAADQAgAQAArwEAMCgAALABACADAAAADQAgAQAADgAwAgAADwAgBgMAAL8EACDPAgAAvgQAMNACAAADABDRAgAAvgQAMOICAgAAAAGEAwEAoQQAIQEAAACzAQAgAQAAALMBACABAwAA_QYAIAMAAAADACABAAC2AQAwAgAAswEAIAMAAAADACABAAC2AQAwAgAAswEAIAMAAAADACABAAC2AQAwAgAAswEAIAMDAAD8BgAg4gICAAAAAYQDAQAAAAEBHAAAugEAIALiAgIAAAABhAMBAAAAAQEcAAC8AQAwARwAALwBADADAwAAlwYAIOICAgDoBAAhhAMBAOoEACECAAAAswEAIBwAAL8BACAC4gICAOgEACGEAwEA6gQAIQIAAAADACAcAADBAQAgAgAAAAMAIBwAAMEBACADAAAAswEAICMAALoBACAkAAC_AQAgAQAAALMBACABAAAAAwAgBQQAAJIGACApAACTBgAgKgAAlgYAICsAAJUGACAsAACUBgAgBc8CAAC9BAAw0AIAAMgBABDRAgAAvQQAMOICAgCEBAAhhAMBAIMEACEDAAAAAwAgAQAAxwEAMCgAAMgBACADAAAAAwAgAQAAtgEAMAIAALMBACAJzwIAALwEADDQAgAAzgEAENECAAC8BAAw1QJAAJMEACHWAkAAkwQAIeICAQAAAAH_AkAAkwQAIYUDAQChBAAhiQMBAKEEACEBAAAAywEAIAEAAADLAQAgCc8CAAC8BAAw0AIAAM4BABDRAgAAvAQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIf8CQACTBAAhhQMBAKEEACGJAwEAoQQAIQADAAAAzgEAIAEAAM8BADACAADLAQAgAwAAAM4BACABAADPAQAwAgAAywEAIAMAAADOAQAgAQAAzwEAMAIAAMsBACAG1QJAAAAAAdYCQAAAAAHiAgEAAAAB_wJAAAAAAYUDAQAAAAGJAwEAAAABARwAANMBACAG1QJAAAAAAdYCQAAAAAHiAgEAAAAB_wJAAAAAAYUDAQAAAAGJAwEAAAABARwAANUBADABHAAA1QEAMAbVAkAA6QQAIdYCQADpBAAh4gIBAOoEACH_AkAA6QQAIYUDAQDqBAAhiQMBAOoEACECAAAAywEAIBwAANgBACAG1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh_wJAAOkEACGFAwEA6gQAIYkDAQDqBAAhAgAAAM4BACAcAADaAQAgAgAAAM4BACAcAADaAQAgAwAAAMsBACAjAADTAQAgJAAA2AEAIAEAAADLAQAgAQAAAM4BACADBAAAjwYAICsAAJEGACAsAACQBgAgCc8CAAC7BAAw0AIAAOEBABDRAgAAuwQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIf8CQACFBAAhhQMBAIMEACGJAwEAgwQAIQMAAADOAQAgAQAA4AEAMCgAAOEBACADAAAAzgEAIAEAAM8BADACAADLAQAgAQAAACYAIAEAAAAmACADAAAAJAAgAQAAJQAwAgAAJgAgAwAAACQAIAEAACUAMAIAACYAIAMAAAAkACABAAAlADACAAAmACALDgAA3QUAIA8AAPMFACAQAADeBQAgEQAA3wUAIBYAAOAFACDiAgEAAAAB-wICAAAAAYQDAQAAAAGGAwEAAAABhwMCAAAAAYgDAgAAAAEBHAAA6QEAIAbiAgEAAAAB-wICAAAAAYQDAQAAAAGGAwEAAAABhwMCAAAAAYgDAgAAAAEBHAAA6wEAMAEcAADrAQAwCw4AAMEFACAPAADxBQAgEAAAwgUAIBEAAMMFACAWAADEBQAg4gIBAOoEACH7AgIA6AQAIYQDAQDqBAAhhgMBAOoEACGHAwIA6AQAIYgDAgDoBAAhAgAAACYAIBwAAO4BACAG4gIBAOoEACH7AgIA6AQAIYQDAQDqBAAhhgMBAOoEACGHAwIA6AQAIYgDAgDoBAAhAgAAACQAIBwAAPABACACAAAAJAAgHAAA8AEAIAMAAAAmACAjAADpAQAgJAAA7gEAIAEAAAAmACABAAAAJAAgBQQAAIoGACApAACLBgAgKgAAjgYAICsAAI0GACAsAACMBgAgCc8CAAC6BAAw0AIAAPcBABDRAgAAugQAMOICAQCDBAAh-wICAIQEACGEAwEAgwQAIYYDAQCDBAAhhwMCAIQEACGIAwIAhAQAIQMAAAAkACABAAD2AQAwKAAA9wEAIAMAAAAkACABAAAlADACAAAmACAHDQAAtAQAIM8CAAC5BAAw0AIAAP0BABDRAgAAuQQAMNQCAgCpBAAh4gICAAAAAYQDAQAAAAEBAAAA-gEAIAEAAAD6AQAgBw0AALQEACDPAgAAuQQAMNACAAD9AQAQ0QIAALkEADDUAgIAqQQAIeICAgCpBAAhhAMBAKEEACEBDQAA4gUAIAMAAAD9AQAgAQAA_gEAMAIAAPoBACADAAAA_QEAIAEAAP4BADACAAD6AQAgAwAAAP0BACABAAD-AQAwAgAA-gEAIAQNAACJBgAg1AICAAAAAeICAgAAAAGEAwEAAAABARwAAIICACAD1AICAAAAAeICAgAAAAGEAwEAAAABARwAAIQCADABHAAAhAIAMAQNAAD_BQAg1AICAOgEACHiAgIA6AQAIYQDAQDqBAAhAgAAAPoBACAcAACHAgAgA9QCAgDoBAAh4gICAOgEACGEAwEA6gQAIQIAAAD9AQAgHAAAiQIAIAIAAAD9AQAgHAAAiQIAIAMAAAD6AQAgIwAAggIAICQAAIcCACABAAAA-gEAIAEAAAD9AQAgBQQAAPoFACApAAD7BQAgKgAA_gUAICsAAP0FACAsAAD8BQAgBs8CAAC4BAAw0AIAAJACABDRAgAAuAQAMNQCAgCEBAAh4gICAIQEACGEAwEAgwQAIQMAAAD9AQAgAQAAjwIAMCgAAJACACADAAAA_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_QIBAKEEACH-AgEAAAAB_wJAALAEACGAA0AAsAQAIQEAAADzAgAgAQAAAPMCACAJzwIAAK8EADDQAgAA9gIAENECAACvBAAw1QJAAJMEACHiAgEAoQQAIf0CAQChBAAh_gIBAKEEACH_AkAAsAQAIYADQACwBAAhAv8CAACaBQAggAMAAJoFACADAAAA9gIAIAEAAPcCADACAADzAgAgAwAAAPYCACABAAD3AgAwAgAA8wIAIAMAAAD2AgAgAQAA9wIAMAIAAPMCACAG1QJAAAAAAeICAQAAAAH9AgEAAAAB_gIBAAAAAf8CQAAAAAGAA0AAAAABARwAAPsCACAG1QJAAAAAAeICAQAAAAH9AgEAAAAB_gIBAAAAAf8CQAAAAAGAA0AAAAABARwAAP0CADABHAAA_QIAMAbVAkAA6QQAIeICAQDqBAAh_QIBAOoEACH-AgEA6gQAIf8CQACqBQAhgANAAKoFACECAAAA8wIAIBwAAIADACAG1QJAAOkEACHiAgEA6gQAIf0CAQDqBAAh_gIBAOoEACH_AkAAqgUAIYADQACqBQAhAgAAAPYCACAcAACCAwAgAgAAAPYCACAcAACCAwAgAwAAAPMCACAjAAD7AgAgJAAAgAMAIAEAAADzAgAgAQAAAPYCACAFBAAApwUAICsAAKkFACAsAACoBQAg_wIAAJoFACCAAwAAmgUAIAnPAgAAqwQAMNACAACJAwAQ0QIAAKsEADDVAkAAhQQAIeICAQCDBAAh_QIBAIMEACH-AgEAgwQAIf8CQACsBAAhgANAAKwEACEDAAAA9gIAIAEAAIgDADAoAACJAwAgAwAAAPYCACABAAD3AgAwAgAA8wIAIAXPAgAAqAQAMNACAACPAwAQ0QIAAKgEADD7AgIAAAAB_AIAAKoEACABAAAAjAMAIAEAAACMAwAgBc8CAACoBAAw0AIAAI8DABDRAgAAqAQAMPsCAgCpBAAh_AIAAKoEACAAAwAAAI8DACABAACQAwAwAgAAjAMAIAMAAACPAwAgAQAAkAMAMAIAAIwDACADAAAAjwMAIAEAAJADADACAACMAwAgAvsCAgAAAAH8AoAAAAABARwAAJQDACAC-wICAAAAAfwCgAAAAAEBHAAAlgMAMAEcAACWAwAwAvsCAgDoBAAh_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_AwAQ0QIAAIIEADDSAgEAgwQAIdMCAQCDBAAh1AICAIQEACHVAkAAhQQAIdYCQACFBAAhAwAAAC4AIAEAAP4DADAoAAD_AwAgAwAAAC4AIAEAAC8AMAIAADAAIAjPAgAAggQAMNACAAD_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_AIAAKoEACAI1wICAAAAAdgCAgAAAATZAgIAAAAE2gICAAAAAdsCAgAAAAHcAgIAAAAB3QICAAAAAd4CAgCHBAAhDNcCgAAAAAHaAoAAAAAB2wKAAAAAAdwCgAAAAAHdAoAAAAAB3gKAAAAAAfUCAQAAAAH2AgEAAAAB9wIBAAAAAfgCgAAAAAH5AoAAAAAB-gKAAAAAAQnPAgAAqwQAMNACAACJAwAQ0QIAAKsEADDVAkAAhQQAIeICAQCDBAAh_QIBAIMEACH-AgEAgwQAIf8CQACsBAAhgANAAKwEACELBAAAmgQAICsAAK4EACAsAACuBAAg1wJAAAAAAdgCQAAAAAXZAkAAAAAF2gJAAAAAAdsCQAAAAAHcAkAAAAAB3QJAAAAAAd4CQACtBAAhCwQAAJoEACArAACuBAAgLAAArgQAINcCQAAAAAHYAkAAAAAF2QJAAAAABdoCQAAAAAHbAkAAAAAB3AJAAAAAAd0CQAAAAAHeAkAArQQAIQjXAkAAAAAB2AJAAAAABdkCQAAAAAXaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAK4EACEJzwIAAK8EADDQAgAA9gIAENECAACvBAAw1QJAAJMEACHiAgEAoQQAIf0CAQChBAAh_gIBAKEEACH_AkAAsAQAIYADQACwBAAhCNcCQAAAAAHYAkAAAAAF2QJAAAAABdoCQAAAAAHbAkAAAAAB3AJAAAAAAd0CQAAAAAHeAkAArgQAIQrPAgAAsQQAMNACAADwAgAQ0QIAALEEADDVAkAAhQQAIeICAQCDBAAh5wIBAJcEACHrAgEAlwQAIYEDAQCDBAAhggMgAI4EACGDA0AArAQAIQbPAgAAsgQAMNACAADYAgAQ0QIAALIEADDUAgIAhAQAIeICAgCEBAAhhAMBAIMEACEHDQAAtAQAIM8CAACzBAAw0AIAAMUCABDRAgAAswQAMNQCAgCpBAAh4gICAKkEACGEAwEAoQQAIQPoAgAAJAAg6QIAACQAIOoCAAAkACAGzwIAALUEADDQAgAAvwIAENECAAC1BAAw4gICAIQEACGEAwEAgwQAIYUDAgCEBAAhBw0AALQEACDPAgAAtgQAMNACAACsAgAQ0QIAALYEADDiAgIAqQQAIYQDAQChBAAhhQMCAKkEACEHzwIAALcEADDQAgAApgIAENECAAC3BAAw0wIBAIMEACHVAkAAhQQAIdYCQACFBAAh5wIBAIMEACEGzwIAALgEADDQAgAAkAIAENECAAC4BAAw1AICAIQEACHiAgIAhAQAIYQDAQCDBAAhBw0AALQEACDPAgAAuQQAMNACAAD9AQAQ0QIAALkEADDUAgIAqQQAIeICAgCpBAAhhAMBAKEEACEJzwIAALoEADDQAgAA9wEAENECAAC6BAAw4gIBAIMEACH7AgIAhAQAIYQDAQCDBAAhhgMBAIMEACGHAwIAhAQAIYgDAgCEBAAhCc8CAAC7BAAw0AIAAOEBABDRAgAAuwQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIf8CQACFBAAhhQMBAIMEACGJAwEAgwQAIQnPAgAAvAQAMNACAADOAQAQ0QIAALwEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACH_AkAAkwQAIYUDAQChBAAhiQMBAKEEACEFzwIAAL0EADDQAgAAyAEAENECAAC9BAAw4gICAIQEACGEAwEAgwQAIQYDAAC_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_wJAAJMEACGTAwEAoQQAIZQDAQCiBAAhlQMBAKIEACESBQAA3QQAIAcAAN4EACAIAADfBAAgCwAA4AQAIAwAAOEEACARAADRBAAgEwAA4gQAIM8CAADcBAAw0AIAAAUAENECAADcBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh6wIBAKEEACHsAgIAowQAIYQDAQChBAAhpAMgAMYEACGlAwEAogQAIQgDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYQDAQChBAAhqAMAAAMAIKkDAAADACAD6AIAAAkAIOkCAAAJACDqAgAACQAgA-gCAAANACDpAgAADQAg6gIAAA0AIAPoAgAAEQAg6QIAABEAIOoCAAARACAD6AIAABsAIOkCAAAbACDqAgAAGwAgCwYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIagDAAA7ACCpAwAAOwAgAAAAAAAFrQMCAAAAAbMDAgAAAAG0AwIAAAABtQMCAAAAAbYDAgAAAAEBrQNAAAAAAQGtAwEAAAABBSMAAPkHACAkAAD_BwAgqgMAAPoHACCrAwAA_gcAILADAAA0ACAFIwAA9wcAICQAAPwHACCqAwAA-AcAIKsDAAD7BwAgsAMAACYAIAMjAAD5BwAgqgMAAPoHACCwAwAANAAgAyMAAPcHACCqAwAA-AcAILADAAAmACAAAAAAAAGtAyAAAAABBSMAAPEHACAkAAD1BwAgqgMAAPIHACCrAwAA9AcAILADAAC-AwAgCyMAAPcEADAkAAD8BAAwqgMAAPgEADCrAwAA-QQAMKwDAAD6BAAgrQMAAPsEADCuAwAA-wQAMK8DAAD7BAAwsAMAAPsEADCxAwAA_QQAMLIDAAD-BAAwBRUAAO4EACDTAgEAAAAB1AICAAAAAdUCQAAAAAHWAkAAAAABAgAAADAAICMAAIIFACADAAAAMAAgIwAAggUAICQAAIEFACABHAAA8wcAMAsUAADLBAAgFQAAzAQAIM8CAADKBAAw0AIAAC4AENECAADKBAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIaYDAADJBAAgAgAAADAAIBwAAIEFACACAAAA_wQAIBwAAIAFACAIzwIAAP4EADDQAgAA_wQAENECAAD-BAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIQjPAgAA_gQAMNACAAD_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_wJAAAAAAZMDAQAAAAGUAwEAAAABlQMBAAAAAQIAAAALACAjAAD0BgAgAwAAAAsAICMAAPQGACAkAADzBgAgARwAAMEHADAMBgAAlAQAIM8CAADbBAAw0AIAAAkAENECAADbBAAw1QJAAJMEACHWAkAAkwQAIeICAQAAAAHnAgEAoQQAIf8CQACTBAAhkwMBAAAAAZQDAQCiBAAhlQMBAKIEACECAAAACwAgHAAA8wYAIAIAAADxBgAgHAAA8gYAIAvPAgAA8AYAMNACAADxBgAQ0QIAAPAGADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIf8CQACTBAAhkwMBAKEEACGUAwEAogQAIZUDAQCiBAAhC88CAADwBgAw0AIAAPEGABDRAgAA8AYAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAh_wJAAJMEACGTAwEAoQQAIZQDAQCiBAAhlQMBAKIEACEH1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh_wJAAOkEACGTAwEA6gQAIZQDAQCgBQAhlQMBAKAFACEH1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh_wJAAOkEACGTAwEA6gQAIZQDAQCgBQAhlQMBAKAFACEH1QJAAAAAAdYCQAAAAAHiAgEAAAAB_wJAAAAAAZMDAQAAAAGUAwEAAAABlQMBAAAAAQ0HAAD2BgAgCAAA9wYAIAsAAPgGACAMAAD5BgAgEQAA-gYAIBMAAPsGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAABhAMBAAAAAaQDIAAAAAGlAwEAAAABBCMAAOkGADCqAwAA6gYAMKwDAADsBgAgsAMAAO0GADAEIwAA3QYAMKoDAADeBgAwrAMAAOAGACCwAwAA4QYAMAQjAADDBgAwqgMAAMQGADCsAwAAxgYAILADAADHBgAwBCMAALcGADCqAwAAuAYAMKwDAAC6BgAgsAMAALsGADAEIwAArgYAMKoDAACvBgAwrAMAALEGACCwAwAA0gUAMAMjAACpBgAgqgMAAKoGACCwAwAAvgMAIAQjAACYBgAwqgMAAJkGADCsAwAAmwYAILADAACcBgAwAAAAAAUjAAC8BwAgJAAAvwcAIKoDAAC9BwAgqwMAAL4HACCwAwAAAQAgAyMAALwHACCqAwAAvQcAILADAAABACAAAAAFIwAAtwcAICQAALoHACCqAwAAuAcAIKsDAAC5BwAgsAMAAAEAIAMjAAC3BwAgqgMAALgHACCwAwAAAQAgAAAABSMAALIHACAkAAC1BwAgqgMAALMHACCrAwAAtAcAILADAAATACADIwAAsgcAIKoDAACzBwAgsAMAABMAIAAAAAcjAACtBwAgJAAAsAcAIKoDAACuBwAgqwMAAK8HACCuAwAABQAgrwMAAAUAILADAAABACADIwAArQcAIKoDAACuBwAgsAMAAAEAIAAAAAAAByMAAKgHACAkAACrBwAgqgMAAKkHACCrAwAAqgcAIK4DAAADACCvAwAAAwAgsAMAALMBACADIwAAqAcAIKoDAACpBwAgsAMAALMBACACBgAAmAUAIBIAAJkFACAAAg0AAJoHACATAACZBwAgBQ4AAJ0HACAPAACeBwAgEAAAnwcAIBEAAKAHACAWAACaBwAgAQ0AAOIFACABDQAA4gUAIAENAADiBQAgAAUGAACYBQAgCgAAogcAIOcCAACaBQAg6wIAAJoFACCjAwAAmgUAIAABAwAA_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_QEAIBwAAOEHACDUAgIA6AQAIeICAgDoBAAhhAMBAOoEACED1AICAOgEACHiAgIA6AQAIYQDAQDqBAAhAwAAAKwCACAjAADWBwAgJAAA5AcAIAUAAACsAgAgHAAA5AcAIOICAgDoBAAhhAMBAOoEACGFAwIA6AQAIQPiAgIA6AQAIYQDAQDqBAAhhQMCAOgEACEF4gIBAAAAAfsCAgAAAAGEAwEAAAABhgMBAAAAAYgDAgAAAAEOBQAAmAcAIAcAAPYGACAIAAD3BgAgCwAA-AYAIBEAAPoGACATAAD7BgAg1QJAAAAAAdYCQAAAAAHiAgEAAAAB6wIBAAAAAewCAgAAAAGEAwEAAAABpAMgAAAAAaUDAQAAAAECAAAAAQAgIwAA5gcAIAMAAAAFACAjAADmBwAgJAAA6gcAIBAAAAAFACAFAACXBwAgBwAAowYAIAgAAKQGACALAAClBgAgEQAApwYAIBMAAKgGACAcAADqBwAg1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh6wIBAOoEACHsAgIAoQUAIYQDAQDqBAAhpAMgAPQEACGlAwEAoAUAIQ4FAACXBwAgBwAAowYAIAgAAKQGACALAAClBgAgEQAApwYAIBMAAKgGACDVAkAA6QQAIdYCQADpBAAh4gIBAOoEACHrAgEA6gQAIewCAgChBQAhhAMBAOoEACGkAyAA9AQAIaUDAQCgBQAhDgUAAJgHACAHAAD2BgAgCAAA9wYAIAsAAPgGACAMAAD5BgAgEQAA-gYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhAMBAAAAAaQDIAAAAAGlAwEAAAABAgAAAAEAICMAAOsHACAG1QJAAAAAAdYCQAAAAAHiAgEAAAAB5AICAAAAAeUCAgAAAAHmAiAAAAABAwAAAAUAICMAAOsHACAkAADwBwAgEAAAAAUAIAUAAJcHACAHAACjBgAgCAAApAYAIAsAAKUGACAMAACmBgAgEQAApwYAIBwAAPAHACDVAkAA6QQAIdYCQADpBAAh4gIBAOoEACHrAgEA6gQAIewCAgChBQAhhAMBAOoEACGkAyAA9AQAIaUDAQCgBQAhDgUAAJcHACAHAACjBgAgCAAApAYAIAsAAKUGACAMAACmBgAgEQAApwYAINUCQADpBAAh1gJAAOkEACHiAgEA6gQAIesCAQDqBAAh7AICAKEFACGEAwEA6gQAIaQDIAD0BAAhpQMBAKAFACEFBgAAlgUAINUCQAAAAAHWAkAAAAAB4gIBAAAAAecCAQAAAAECAAAAvgMAICMAAPEHACAE0wIBAAAAAdQCAgAAAAHVAkAAAAAB1gJAAAAAAQMAAAA7ACAjAADxBwAgJAAA9gcAIAcAAAA7ACAGAACIBQAgHAAA9gcAINUCQADpBAAh1gJAAOkEACHiAgEA6gQAIecCAQDqBAAhBQYAAIgFACDVAkAA6QQAIdYCQADpBAAh4gIBAOoEACHnAgEA6gQAIQoOAADdBQAgDwAA8wUAIBAAAN4FACARAADfBQAg4gIBAAAAAfsCAgAAAAGEAwEAAAABhgMBAAAAAYcDAgAAAAGIAwIAAAABAgAAACYAICMAAPcHACAIEwAAgwUAINUCQAAAAAHWAkAAAAAB4gIBAAAAAeMCAQAAAAHkAgIAAAAB5QICAAAAAeYCIAAAAAECAAAANAAgIwAA-QcAIAMAAAAkACAjAAD3BwAgJAAA_QcAIAwAAAAkACAOAADBBQAgDwAA8QUAIBAAAMIFACARAADDBQAgHAAA_QcAIOICAQDqBAAh-wICAOgEACGEAwEA6gQAIYYDAQDqBAAhhwMCAOgEACGIAwIA6AQAIQoOAADBBQAgDwAA8QUAIBAAAMIFACARAADDBQAg4gIBAOoEACH7AgIA6AQAIYQDAQDqBAAhhgMBAOoEACGHAwIA6AQAIYgDAgDoBAAhAwAAADIAICMAAPkHACAkAACACAAgCgAAADIAIBMAAPUEACAcAACACAAg1QJAAOkEACHWAkAA6QQAIeICAQDqBAAh4wIBAOoEACHkAgIA6AQAIeUCAgDoBAAh5gIgAPQEACEIEwAA9QQAINUCQADpBAAh1gJAAOkEACHiAgEA6gQAIeMCAQDqBAAh5AICAOgEACHlAgIA6AQAIeYCIAD0BAAhCAQAGAUEAgcMBAgQBQsUBgweCREjChM8FAIDBwEEAAMBAwgAAQYAAQEGAAEDBAAIBhUBChkHAQkABgEKGgABBh8BAgYAARUACwYEABcOAAwPAA4QABARLQoWMRICBAANDScLAQ0oAAIEAA8NKQsBDSoAAgQAEQ0rCwENLAACFAATFQALAwQAFg03EhMAFAMEABUGAAESNRMBEjYAAQ04AAIROQAWOgAFBz0ACD4ACz8ADEAAEUEAAAEFSwIBBVECBQQAHSkAHioAHysAICwAIQAAAAAABQQAHSkAHioAHysAICwAIQEGYwEBBmkBAwQAJisAJywAKAAAAAMEACYrACcsACgBCQAGAQkABgMEAC0rAC4sAC8AAAADBAAtKwAuLAAvAQYAAQEGAAEDBAA0KwA1LAA2AAAAAwQANCsANSwANgEGAAEBBgABAwQAOysAPCwAPQAAAAMEADsrADwsAD0AAAUEAEIpAEMqAEQrAEUsAEYAAAAAAAUEAEIpAEMqAEQrAEUsAEYAAAADBABMKwBNLABOAAAAAwQATCsATSwATgMOAAwPAA4QABADDgAMDwAOEAAQBQQAUykAVCoAVSsAViwAVwAAAAAABQQAUykAVCoAVSsAViwAVwAABQQAXCkAXSoAXisAXywAYAAAAAAABQQAXCkAXSoAXisAXywAYAIGAAEVAAsCBgABFQALAwQAZSsAZiwAZwAAAAMEAGUrAGYsAGcAAAUEAGwpAG0qAG4rAG8sAHAAAAAAAAUEAGwpAG0qAG4rAG8sAHAAAAUEAHUpAHYqAHcrAHgsAHkAAAAAAAUEAHUpAHYqAHcrAHgsAHkBBuUCAQEG6wIBAwQAfisAfywAgAEAAAADBAB-KwB_LACAAQAAAAMEAIYBKwCHASwAiAEAAAADBACGASsAhwEsAIgBAAAABQQAjgEpAI8BKgCQASsAkQEsAJIBAAAAAAAFBACOASkAjwEqAJABKwCRASwAkgEAAAAFBACYASkAmQEqAJoBKwCbASwAnAEAAAAAAAUEAJgBKQCZASoAmgErAJsBLACcAQEGAAEBBgABAwQAoQErAKIBLACjAQAAAAMEAKEBKwCiASwAowEBEwAUARMAFAUEAKgBKQCpASoAqgErAKsBLACsAQAAAAAABQQAqAEpAKkBKgCqASsAqwEsAKwBAhQAExUACwIUABMVAAsFBACxASkAsgEqALMBKwC0ASwAtQEAAAAAAAUEALEBKQCyASoAswErALQBLAC1ARcCARhCARlDARpEARtFAR1HAR5JGR9KGiBNASFPGSJQGyVSASZTASdUGS1XHC5YIi9ZBjBaBjFbBjJcBjNdBjRfBjVhGTZiIzdlBjhnGTloJDpqBjtrBjxsGT1vJT5wKT9xB0ByB0FzB0J0B0N1B0R3B0V5GUZ6Kkd8B0h-GUl_K0qAAQdLgQEHTIIBGU2FASxOhgEwT4cBBFCIAQRRiQEEUooBBFOLAQRUjQEEVY8BGVaQATFXkgEEWJQBGVmVATJalgEEW5cBBFyYARldmwEzXpwBN1-dAQVgngEFYZ8BBWKgAQVjoQEFZKMBBWWlARlmpgE4Z6gBBWiqARlpqwE5aqwBBWutAQVsrgEZbbEBOm6yAT5vtAECcLUBAnG3AQJyuAECc7kBAnS7AQJ1vQEZdr4BP3fAAQJ4wgEZecMBQHrEAQJ7xQECfMYBGX3JAUF-ygFHf8wBSIABzQFIgQHQAUiCAdEBSIMB0gFIhAHUAUiFAdYBGYYB1wFJhwHZAUiIAdsBGYkB3AFKigHdAUiLAd4BSIwB3wEZjQHiAUuOAeMBT48B5AELkAHlAQuRAeYBC5IB5wELkwHoAQuUAeoBC5UB7AEZlgHtAVCXAe8BC5gB8QEZmQHyAVGaAfMBC5sB9AELnAH1ARmdAfgBUp4B-QFYnwH7ARCgAfwBEKEB_wEQogGAAhCjAYECEKQBgwIQpQGFAhmmAYYCWacBiAIQqAGKAhmpAYsCWqoBjAIQqwGNAhCsAY4CGa0BkQJbrgGSAmGvAZMCCrABlAIKsQGVAgqyAZYCCrMBlwIKtAGZAgq1AZsCGbYBnAJitwGeAgq4AaACGbkBoQJjugGiAgq7AaMCCrwBpAIZvQGnAmS-AagCaL8BqgIMwAGrAgzBAa4CDMIBrwIMwwGwAgzEAbICDMUBtAIZxgG1AmnHAbcCDMgBuQIZyQG6AmrKAbsCDMsBvAIMzAG9AhnNAcACa84BwQJxzwHDAg7QAcQCDtEBxwIO0gHIAg7TAckCDtQBywIO1QHNAhnWAc4CctcB0AIO2AHSAhnZAdMCc9oB1AIO2wHVAg7cAdYCGd0B2QJ03gHaAnrfAdsCCeAB3AIJ4QHdAgniAd4CCeMB3wIJ5AHhAgnlAeMCGeYB5AJ75wHnAgnoAekCGekB6gJ86gHsAgnrAe0CCewB7gIZ7QHxAn3uAfICgQHvAfQCggHwAfUCggHxAfgCggHyAfkCggHzAfoCggH0AfwCggH1Af4CGfYB_wKDAfcBgQOCAfgBgwMZ-QGEA4QB-gGFA4IB-wGGA4IB_AGHAxn9AYoDhQH-AYsDiQH_AY0DigGAAo4DigGBApEDigGCApIDigGDApMDigGEApUDigGFApcDGYYCmAOLAYcCmgOKAYgCnAMZiQKdA4wBigKeA4oBiwKfA4oBjAKgAxmNAqMDjQGOAqQDkwGPAqYDlAGQAqcDlAGRAqoDlAGSAqsDlAGTAqwDlAGUAq4DlAGVArADGZYCsQOVAZcCswOUAZgCtQMZmQK2A5YBmgK3A5QBmwK4A5QBnAK5AxmdArwDlwGeAr0DnQGfAr8DFKACwAMUoQLCAxSiAsMDFKMCxAMUpALGAxSlAsgDGaYCyQOeAacCywMUqALNAxmpAs4DnwGqAs8DFKsC0AMUrALRAxmtAtQDoAGuAtUDpAGvAtYDE7AC1wMTsQLYAxOyAtkDE7MC2gMTtALcAxO1At4DGbYC3wOlAbcC4QMTuALjAxm5AuQDpgG6AuUDE7sC5gMTvALnAxm9AuoDpwG-AusDrQG_AuwDEsAC7QMSwQLuAxLCAu8DEsMC8AMSxALyAxLFAvQDGcYC9QOuAccC9wMSyAL5AxnJAvoDrwHKAvsDEssC_AMSzAL9AxnNAoAEsAHOAoEEtgE" + strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"users\",\"_count\",\"role\",\"user\",\"sessions\",\"accounts\",\"profile\",\"addresses\",\"profiles\",\"licenseKeys\",\"proclamations\",\"solfeggioFrequency\",\"category\",\"group\",\"userProclamations\",\"blocks\",\"schedule\",\"block\",\"proclamation\",\"blockProclamations\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Profile.findUnique\",\"Profile.findUniqueOrThrow\",\"Profile.findFirst\",\"Profile.findFirstOrThrow\",\"Profile.findMany\",\"Profile.createOne\",\"Profile.createMany\",\"Profile.createManyAndReturn\",\"Profile.updateOne\",\"Profile.updateMany\",\"Profile.updateManyAndReturn\",\"Profile.upsertOne\",\"Profile.deleteOne\",\"Profile.deleteMany\",\"Profile.groupBy\",\"Profile.aggregate\",\"Address.findUnique\",\"Address.findUniqueOrThrow\",\"Address.findFirst\",\"Address.findFirstOrThrow\",\"Address.findMany\",\"Address.createOne\",\"Address.createMany\",\"Address.createManyAndReturn\",\"Address.updateOne\",\"Address.updateMany\",\"Address.updateManyAndReturn\",\"Address.upsertOne\",\"Address.deleteOne\",\"Address.deleteMany\",\"Address.groupBy\",\"Address.aggregate\",\"Session.findUnique\",\"Session.findUniqueOrThrow\",\"Session.findFirst\",\"Session.findFirstOrThrow\",\"Session.findMany\",\"Session.createOne\",\"Session.createMany\",\"Session.createManyAndReturn\",\"Session.updateOne\",\"Session.updateMany\",\"Session.updateManyAndReturn\",\"Session.upsertOne\",\"Session.deleteOne\",\"Session.deleteMany\",\"Session.groupBy\",\"Session.aggregate\",\"Account.findUnique\",\"Account.findUniqueOrThrow\",\"Account.findFirst\",\"Account.findFirstOrThrow\",\"Account.findMany\",\"Account.createOne\",\"Account.createMany\",\"Account.createManyAndReturn\",\"Account.updateOne\",\"Account.updateMany\",\"Account.updateManyAndReturn\",\"Account.upsertOne\",\"Account.deleteOne\",\"Account.deleteMany\",\"Account.groupBy\",\"Account.aggregate\",\"UserRole.findUnique\",\"UserRole.findUniqueOrThrow\",\"UserRole.findFirst\",\"UserRole.findFirstOrThrow\",\"UserRole.findMany\",\"UserRole.createOne\",\"UserRole.createMany\",\"UserRole.createManyAndReturn\",\"UserRole.updateOne\",\"UserRole.updateMany\",\"UserRole.updateManyAndReturn\",\"UserRole.upsertOne\",\"UserRole.deleteOne\",\"UserRole.deleteMany\",\"UserRole.groupBy\",\"UserRole.aggregate\",\"Verification.findUnique\",\"Verification.findUniqueOrThrow\",\"Verification.findFirst\",\"Verification.findFirstOrThrow\",\"Verification.findMany\",\"Verification.createOne\",\"Verification.createMany\",\"Verification.createManyAndReturn\",\"Verification.updateOne\",\"Verification.updateMany\",\"Verification.updateManyAndReturn\",\"Verification.upsertOne\",\"Verification.deleteOne\",\"Verification.deleteMany\",\"Verification.groupBy\",\"Verification.aggregate\",\"Proclamation.findUnique\",\"Proclamation.findUniqueOrThrow\",\"Proclamation.findFirst\",\"Proclamation.findFirstOrThrow\",\"Proclamation.findMany\",\"Proclamation.createOne\",\"Proclamation.createMany\",\"Proclamation.createManyAndReturn\",\"Proclamation.updateOne\",\"Proclamation.updateMany\",\"Proclamation.updateManyAndReturn\",\"Proclamation.upsertOne\",\"Proclamation.deleteOne\",\"Proclamation.deleteMany\",\"Proclamation.groupBy\",\"Proclamation.aggregate\",\"ProclamationGroup.findUnique\",\"ProclamationGroup.findUniqueOrThrow\",\"ProclamationGroup.findFirst\",\"ProclamationGroup.findFirstOrThrow\",\"ProclamationGroup.findMany\",\"ProclamationGroup.createOne\",\"ProclamationGroup.createMany\",\"ProclamationGroup.createManyAndReturn\",\"ProclamationGroup.updateOne\",\"ProclamationGroup.updateMany\",\"ProclamationGroup.updateManyAndReturn\",\"ProclamationGroup.upsertOne\",\"ProclamationGroup.deleteOne\",\"ProclamationGroup.deleteMany\",\"ProclamationGroup.groupBy\",\"ProclamationGroup.aggregate\",\"UserProclamations.findUnique\",\"UserProclamations.findUniqueOrThrow\",\"UserProclamations.findFirst\",\"UserProclamations.findFirstOrThrow\",\"UserProclamations.findMany\",\"UserProclamations.createOne\",\"UserProclamations.createMany\",\"UserProclamations.createManyAndReturn\",\"UserProclamations.updateOne\",\"UserProclamations.updateMany\",\"UserProclamations.updateManyAndReturn\",\"UserProclamations.upsertOne\",\"UserProclamations.deleteOne\",\"UserProclamations.deleteMany\",\"UserProclamations.groupBy\",\"UserProclamations.aggregate\",\"SolfeggioFrequency.findUnique\",\"SolfeggioFrequency.findUniqueOrThrow\",\"SolfeggioFrequency.findFirst\",\"SolfeggioFrequency.findFirstOrThrow\",\"SolfeggioFrequency.findMany\",\"SolfeggioFrequency.createOne\",\"SolfeggioFrequency.createMany\",\"SolfeggioFrequency.createManyAndReturn\",\"SolfeggioFrequency.updateOne\",\"SolfeggioFrequency.updateMany\",\"SolfeggioFrequency.updateManyAndReturn\",\"SolfeggioFrequency.upsertOne\",\"SolfeggioFrequency.deleteOne\",\"SolfeggioFrequency.deleteMany\",\"SolfeggioFrequency.groupBy\",\"SolfeggioFrequency.aggregate\",\"ProclamationCategory.findUnique\",\"ProclamationCategory.findUniqueOrThrow\",\"ProclamationCategory.findFirst\",\"ProclamationCategory.findFirstOrThrow\",\"ProclamationCategory.findMany\",\"ProclamationCategory.createOne\",\"ProclamationCategory.createMany\",\"ProclamationCategory.createManyAndReturn\",\"ProclamationCategory.updateOne\",\"ProclamationCategory.updateMany\",\"ProclamationCategory.updateManyAndReturn\",\"ProclamationCategory.upsertOne\",\"ProclamationCategory.deleteOne\",\"ProclamationCategory.deleteMany\",\"ProclamationCategory.groupBy\",\"ProclamationCategory.aggregate\",\"LicenseKey.findUnique\",\"LicenseKey.findUniqueOrThrow\",\"LicenseKey.findFirst\",\"LicenseKey.findFirstOrThrow\",\"LicenseKey.findMany\",\"LicenseKey.createOne\",\"LicenseKey.createMany\",\"LicenseKey.createManyAndReturn\",\"LicenseKey.updateOne\",\"LicenseKey.updateMany\",\"LicenseKey.updateManyAndReturn\",\"LicenseKey.upsertOne\",\"LicenseKey.deleteOne\",\"LicenseKey.deleteMany\",\"LicenseKey.groupBy\",\"LicenseKey.aggregate\",\"ApiKey.findUnique\",\"ApiKey.findUniqueOrThrow\",\"ApiKey.findFirst\",\"ApiKey.findFirstOrThrow\",\"ApiKey.findMany\",\"ApiKey.createOne\",\"ApiKey.createMany\",\"ApiKey.createManyAndReturn\",\"ApiKey.updateOne\",\"ApiKey.updateMany\",\"ApiKey.updateManyAndReturn\",\"ApiKey.upsertOne\",\"ApiKey.deleteOne\",\"ApiKey.deleteMany\",\"ApiKey.groupBy\",\"ApiKey.aggregate\",\"ColorPalette.findUnique\",\"ColorPalette.findUniqueOrThrow\",\"ColorPalette.findFirst\",\"ColorPalette.findFirstOrThrow\",\"ColorPalette.findMany\",\"ColorPalette.createOne\",\"ColorPalette.createMany\",\"ColorPalette.createManyAndReturn\",\"ColorPalette.updateOne\",\"ColorPalette.updateMany\",\"ColorPalette.updateManyAndReturn\",\"ColorPalette.upsertOne\",\"ColorPalette.deleteOne\",\"ColorPalette.deleteMany\",\"ColorPalette.groupBy\",\"ColorPalette.aggregate\",\"AuditLog.findUnique\",\"AuditLog.findUniqueOrThrow\",\"AuditLog.findFirst\",\"AuditLog.findFirstOrThrow\",\"AuditLog.findMany\",\"AuditLog.createOne\",\"AuditLog.createMany\",\"AuditLog.createManyAndReturn\",\"AuditLog.updateOne\",\"AuditLog.updateMany\",\"AuditLog.updateManyAndReturn\",\"AuditLog.upsertOne\",\"AuditLog.deleteOne\",\"AuditLog.deleteMany\",\"AuditLog.groupBy\",\"AuditLog.aggregate\",\"Schedule.findUnique\",\"Schedule.findUniqueOrThrow\",\"Schedule.findFirst\",\"Schedule.findFirstOrThrow\",\"Schedule.findMany\",\"Schedule.createOne\",\"Schedule.createMany\",\"Schedule.createManyAndReturn\",\"Schedule.updateOne\",\"Schedule.updateMany\",\"Schedule.updateManyAndReturn\",\"Schedule.upsertOne\",\"Schedule.deleteOne\",\"Schedule.deleteMany\",\"Schedule.groupBy\",\"Schedule.aggregate\",\"Block.findUnique\",\"Block.findUniqueOrThrow\",\"Block.findFirst\",\"Block.findFirstOrThrow\",\"Block.findMany\",\"Block.createOne\",\"Block.createMany\",\"Block.createManyAndReturn\",\"Block.updateOne\",\"Block.updateMany\",\"Block.updateManyAndReturn\",\"Block.upsertOne\",\"Block.deleteOne\",\"Block.deleteMany\",\"Block.groupBy\",\"Block.aggregate\",\"BlockProclamations.findUnique\",\"BlockProclamations.findUniqueOrThrow\",\"BlockProclamations.findFirst\",\"BlockProclamations.findFirstOrThrow\",\"BlockProclamations.findMany\",\"BlockProclamations.createOne\",\"BlockProclamations.createMany\",\"BlockProclamations.createManyAndReturn\",\"BlockProclamations.updateOne\",\"BlockProclamations.updateMany\",\"BlockProclamations.updateManyAndReturn\",\"BlockProclamations.upsertOne\",\"BlockProclamations.deleteOne\",\"BlockProclamations.deleteMany\",\"BlockProclamations.groupBy\",\"BlockProclamations.aggregate\",\"AND\",\"OR\",\"NOT\",\"blockId\",\"proclamationId\",\"sortOrder\",\"createdAt\",\"updatedAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"id\",\"scheduleId\",\"startMinutes\",\"endMinutes\",\"dynamicAudio\",\"userId\",\"every\",\"some\",\"none\",\"email\",\"roleId\",\"roleName\",\"page\",\"action\",\"entity\",\"entityId\",\"before\",\"after\",\"ip\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"solfeggioFrequencyId\",\"colors\",\"keyPrefix\",\"keyHash\",\"expiresAt\",\"revokedAt\",\"key\",\"active\",\"lastValidatedAt\",\"systemName\",\"systemNameLower\",\"name\",\"value\",\"content\",\"categoryId\",\"groupId\",\"identifier\",\"accountId\",\"providerId\",\"accessToken\",\"refreshToken\",\"idToken\",\"accessTokenExpiresAt\",\"refreshTokenExpiresAt\",\"scope\",\"password\",\"token\",\"ipAddress\",\"userAgent\",\"profileId\",\"street\",\"city\",\"state\",\"postalCode\",\"country\",\"type\",\"isDefaultMailing\",\"isDefaultBilling\",\"isDefaultShipping\",\"firstName\",\"lastName\",\"phoneNumber\",\"ghlContactId\",\"emailVerified\",\"image\",\"blockId_proclamationId\",\"userId_proclamationId\",\"userId_systemNameLower\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "gQi2AbACEgUAAN4EACAHAADfBAAgCAAA4AQAIAsAAOEEACAMAADiBAAgEQAA0QQAIBMAAOMEACDPAgAA3QQAMNACAAAFABDRAgAA3QQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB6wIBAAAAAewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhAQAAAAEAIAYDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYYDAQChBAAhAQAAAAMAIBIFAADeBAAgBwAA3wQAIAgAAOAEACALAADhBAAgDAAA4gQAIBEAANEEACATAADjBAAgzwIAAN0EADDQAgAABQAQ0QIAAN0EADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHrAgEAoQQAIewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhCQUAAKQHACAHAAClBwAgCAAApgcAIAsAAKcHACAMAACoBwAgEQAAoQcAIBMAAJoHACDsAgAAmwUAIKcDAACbBQAgAwAAAAUAIAEAAAYAMAIAAAEAIAEAAAAFACAMBgAAlAQAIM8CAADcBAAw0AIAAAkAENECAADcBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACH_AkAAkwQAIZUDAQChBAAhlgMBAKIEACGXAwEAogQAIQMGAACZBQAglgMAAJsFACCXAwAAmwUAIAwGAACUBAAgzwIAANwEADDQAgAACQAQ0QIAANwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAecCAQChBAAh_wJAAJMEACGVAwEAAAABlgMBAKIEACGXAwEAogQAIQMAAAAJACABAAAKADACAAALACARBgAAlAQAIM8CAADbBAAw0AIAAA0AENECAADbBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACGMAwEAoQQAIY0DAQChBAAhjgMBAKIEACGPAwEAogQAIZADAQCiBAAhkQNAALAEACGSA0AAsAQAIZMDAQCiBAAhlAMBAKIEACEIBgAAmQUAII4DAACbBQAgjwMAAJsFACCQAwAAmwUAIJEDAACbBQAgkgMAAJsFACCTAwAAmwUAIJQDAACbBQAgEQYAAJQEACDPAgAA2wQAMNACAAANABDRAgAA2wQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB5wIBAKEEACGMAwEAoQQAIY0DAQChBAAhjgMBAKIEACGPAwEAogQAIZADAQCiBAAhkQNAALAEACGSA0AAsAQAIZMDAQCiBAAhlAMBAKIEACEDAAAADQAgAQAADgAwAgAADwAgDAYAANYEACAKAADaBAAgzwIAANkEADDQAgAAEQAQ0QIAANkEADDiAgEAoQQAIecCAQCiBAAh6wIBAKIEACGiAwEAoQQAIaMDAQChBAAhpAMBAKEEACGlAwEAogQAIQUGAACZBQAgCgAAowcAIOcCAACbBQAg6wIAAJsFACClAwAAmwUAIAwGAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAAAAAecCAQCiBAAh6wIBAKIEACGiAwEAoQQAIaMDAQChBAAhpAMBAKEEACGlAwEAogQAIQMAAAARACABAAASADACAAATACABAAAABQAgDwkAANgEACDPAgAA1wQAMNACAAAWABDRAgAA1wQAMOICAQChBAAhmAMBAKEEACGZAwEAoQQAIZoDAQChBAAhmwMBAKEEACGcAwEAoQQAIZ0DAQChBAAhngMBAKEEACGfAyAAxgQAIaADIADGBAAhoQMgAMYEACEBCQAAogcAIA8JAADYBAAgzwIAANcEADDQAgAAFgAQ0QIAANcEADDiAgEAAAABmAMBAKEEACGZAwEAoQQAIZoDAQChBAAhmwMBAKEEACGcAwEAoQQAIZ0DAQChBAAhngMBAKEEACGfAyAAxgQAIaADIADGBAAhoQMgAMYEACEDA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_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABARwAAIwBACAI1QJAAAAAAdYCQAAAAAHiAgEAAAAB5wIBAAAAAf8CQAAAAAGVAwEAAAABlgMBAAAAAZcDAQAAAAEBHAAAjgEAMAEcAACOAQAwCQYAAIcHACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIf8CQADqBAAhlQMBAOsEACGWAwEAoQUAIZcDAQChBQAhAgAAAAsAIBwAAJEBACAI1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh5wIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQIAAAAJACAcAACTAQAgAgAAAAkAIBwAAJMBACADAAAACwAgIwAAjAEAICQAAJEBACABAAAACwAgAQAAAAkAIAUEAACEBwAgKwAAhgcAICwAAIUHACCWAwAAmwUAIJcDAACbBQAgC88CAADBBAAw0AIAAJoBABDRAgAAwQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAh_wJAAIUEACGVAwEAgwQAIZYDAQCXBAAhlwMBAJcEACEDAAAACQAgAQAAmQEAMCgAAJoBACADAAAACQAgAQAACgAwAgAACwAgAQAAAA8AIAEAAAAPACADAAAADQAgAQAADgAwAgAADwAgAwAAAA0AIAEAAA4AMAIAAA8AIAMAAAANACABAAAOADACAAAPACAOBgAAgwcAINUCQAAAAAHWAkAAAAAB4gIBAAAAAecCAQAAAAGMAwEAAAABjQMBAAAAAY4DAQAAAAGPAwEAAAABkAMBAAAAAZEDQAAAAAGSA0AAAAABkwMBAAAAAZQDAQAAAAEBHAAAogEAIA3VAkAAAAAB1gJAAAAAAeICAQAAAAHnAgEAAAABjAMBAAAAAY0DAQAAAAGOAwEAAAABjwMBAAAAAZADAQAAAAGRA0AAAAABkgNAAAAAAZMDAQAAAAGUAwEAAAABARwAAKQBADABHAAApAEAMA4GAACCBwAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh5wIBAOsEACGMAwEA6wQAIY0DAQDrBAAhjgMBAKEFACGPAwEAoQUAIZADAQChBQAhkQNAAKsFACGSA0AAqwUAIZMDAQChBQAhlAMBAKEFACECAAAADwAgHAAApwEAIA3VAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIYwDAQDrBAAhjQMBAOsEACGOAwEAoQUAIY8DAQChBQAhkAMBAKEFACGRA0AAqwUAIZIDQACrBQAhkwMBAKEFACGUAwEAoQUAIQIAAAANACAcAACpAQAgAgAAAA0AIBwAAKkBACADAAAADwAgIwAAogEAICQAAKcBACABAAAADwAgAQAAAA0AIAoEAAD_BgAgKwAAgQcAICwAAIAHACCOAwAAmwUAII8DAACbBQAgkAMAAJsFACCRAwAAmwUAIJIDAACbBQAgkwMAAJsFACCUAwAAmwUAIBDPAgAAwAQAMNACAACwAQAQ0QIAAMAEADDVAkAAhQQAIdYCQACFBAAh4gIBAIMEACHnAgEAgwQAIYwDAQCDBAAhjQMBAIMEACGOAwEAlwQAIY8DAQCXBAAhkAMBAJcEACGRA0AArAQAIZIDQACsBAAhkwMBAJcEACGUAwEAlwQAIQMAAAANACABAACvAQAwKAAAsAEAIAMAAAANACABAAAOADACAAAPACAGAwAAvwQAIM8CAAC-BAAw0AIAAAMAENECAAC-BAAw4gICAAAAAYYDAQChBAAhAQAAALMBACABAAAAswEAIAEDAAD-BgAgAwAAAAMAIAEAALYBADACAACzAQAgAwAAAAMAIAEAALYBADACAACzAQAgAwAAAAMAIAEAALYBADACAACzAQAgAwMAAP0GACDiAgIAAAABhgMBAAAAAQEcAAC6AQAgAuICAgAAAAGGAwEAAAABARwAALwBADABHAAAvAEAMAMDAACYBgAg4gICAOkEACGGAwEA6wQAIQIAAACzAQAgHAAAvwEAIALiAgIA6QQAIYYDAQDrBAAhAgAAAAMAIBwAAMEBACACAAAAAwAgHAAAwQEAIAMAAACzAQAgIwAAugEAICQAAL8BACABAAAAswEAIAEAAAADACAFBAAAkwYAICkAAJQGACAqAACXBgAgKwAAlgYAICwAAJUGACAFzwIAAL0EADDQAgAAyAEAENECAAC9BAAw4gICAIQEACGGAwEAgwQAIQMAAAADACABAADHAQAwKAAAyAEAIAMAAAADACABAAC2AQAwAgAAswEAIAnPAgAAvAQAMNACAADOAQAQ0QIAALwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAf8CQACTBAAhhwMBAKEEACGLAwEAoQQAIQEAAADLAQAgAQAAAMsBACAJzwIAALwEADDQAgAAzgEAENECAAC8BAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh_wJAAJMEACGHAwEAoQQAIYsDAQChBAAhAAMAAADOAQAgAQAAzwEAMAIAAMsBACADAAAAzgEAIAEAAM8BADACAADLAQAgAwAAAM4BACABAADPAQAwAgAAywEAIAbVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABhwMBAAAAAYsDAQAAAAEBHAAA0wEAIAbVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABhwMBAAAAAYsDAQAAAAEBHAAA1QEAMAEcAADVAQAwBtUCQADqBAAh1gJAAOoEACHiAgEA6wQAIf8CQADqBAAhhwMBAOsEACGLAwEA6wQAIQIAAADLAQAgHAAA2AEAIAbVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIYcDAQDrBAAhiwMBAOsEACECAAAAzgEAIBwAANoBACACAAAAzgEAIBwAANoBACADAAAAywEAICMAANMBACAkAADYAQAgAQAAAMsBACABAAAAzgEAIAMEAACQBgAgKwAAkgYAICwAAJEGACAJzwIAALsEADDQAgAA4QEAENECAAC7BAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh_wJAAIUEACGHAwEAgwQAIYsDAQCDBAAhAwAAAM4BACABAADgAQAwKAAA4QEAIAMAAADOAQAgAQAAzwEAMAIAAMsBACABAAAAJgAgAQAAACYAIAMAAAAkACABAAAlADACAAAmACADAAAAJAAgAQAAJQAwAgAAJgAgAwAAACQAIAEAACUAMAIAACYAIAsOAADeBQAgDwAA9AUAIBAAAN8FACARAADgBQAgFgAA4QUAIOICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQEcAADpAQAgBuICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQEcAADrAQAwARwAAOsBADALDgAAwgUAIA8AAPIFACAQAADDBQAgEQAAxAUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACECAAAAJgAgHAAA7gEAIAbiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACECAAAAJAAgHAAA8AEAIAIAAAAkACAcAADwAQAgAwAAACYAICMAAOkBACAkAADuAQAgAQAAACYAIAEAAAAkACAFBAAAiwYAICkAAIwGACAqAACPBgAgKwAAjgYAICwAAI0GACAJzwIAALoEADDQAgAA9wEAENECAAC6BAAw4gIBAIMEACH7AgIAhAQAIYYDAQCDBAAhiAMBAIMEACGJAwIAhAQAIYoDAgCEBAAhAwAAACQAIAEAAPYBADAoAAD3AQAgAwAAACQAIAEAACUAMAIAACYAIAcNAAC0BAAgzwIAALkEADDQAgAA_QEAENECAAC5BAAw1AICAKkEACHiAgIAAAABhgMBAAAAAQEAAAD6AQAgAQAAAPoBACAHDQAAtAQAIM8CAAC5BAAw0AIAAP0BABDRAgAAuQQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQENAADjBQAgAwAAAP0BACABAAD-AQAwAgAA-gEAIAMAAAD9AQAgAQAA_gEAMAIAAPoBACADAAAA_QEAIAEAAP4BADACAAD6AQAgBA0AAIoGACDUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAggIAIAPUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAhAIAMAEcAACEAgAwBA0AAIAGACDUAgIA6QQAIeICAgDpBAAhhgMBAOsEACECAAAA-gEAIBwAAIcCACAD1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhAgAAAP0BACAcAACJAgAgAgAAAP0BACAcAACJAgAgAwAAAPoBACAjAACCAgAgJAAAhwIAIAEAAAD6AQAgAQAAAP0BACAFBAAA-wUAICkAAPwFACAqAAD_BQAgKwAA_gUAICwAAP0FACAGzwIAALgEADDQAgAAkAIAENECAAC4BAAw1AICAIQEACHiAgIAhAQAIYYDAQCDBAAhAwAAAP0BACABAACPAgAwKAAAkAIAIAMAAAD9AQAgAQAA_gEAMAIAAPoBACABAAAAIgAgAQAAACIAIAMAAAAgACABAAAhADACAAAiACADAAAAIAAgAQAAIQAwAgAAIgAgAwAAACAAIAEAACEAMAIAACIAIAYGAADcBQAgFQAA-gUAINMCAQAAAAHVAkAAAAAB1gJAAAAAAecCAQAAAAEBHAAAmAIAIATTAgEAAAAB1QJAAAAAAdYCQAAAAAHnAgEAAAABARwAAJoCADABHAAAmgIAMAYGAADaBQAgFQAA-QUAINMCAQDrBAAh1QJAAOoEACHWAkAA6gQAIecCAQDrBAAhAgAAACIAIBwAAJ0CACAE0wIBAOsEACHVAkAA6gQAIdYCQADqBAAh5wIBAOsEACECAAAAIAAgHAAAnwIAIAIAAAAgACAcAACfAgAgAwAAACIAICMAAJgCACAkAACdAgAgAQAAACIAIAEAAAAgACADBAAA9gUAICsAAPgFACAsAAD3BQAgB88CAAC3BAAw0AIAAKYCABDRAgAAtwQAMNMCAQCDBAAh1QJAAIUEACHWAkAAhQQAIecCAQCDBAAhAwAAACAAIAEAAKUCADAoAACmAgAgAwAAACAAIAEAACEAMAIAACIAIAcNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAAAAAYYDAQAAAAGHAwIAAAABAQAAAKkCACABAAAAqQIAIAcNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAKkEACGGAwEAoQQAIYcDAgCpBAAhAQ0AAOMFACADAAAArAIAIAEAAK0CADACAACpAgAgAwAAAKwCACABAACtAgAwAgAAqQIAIAMAAACsAgAgAQAArQIAMAIAAKkCACAEDQAA9QUAIOICAgAAAAGGAwEAAAABhwMCAAAAAQEcAACxAgAgA-ICAgAAAAGGAwEAAAABhwMCAAAAAQEcAACzAgAwARwAALMCADAEDQAA6QUAIOICAgDpBAAhhgMBAOsEACGHAwIA6QQAIQIAAACpAgAgHAAAtgIAIAPiAgIA6QQAIYYDAQDrBAAhhwMCAOkEACECAAAArAIAIBwAALgCACACAAAArAIAIBwAALgCACADAAAAqQIAICMAALECACAkAAC2AgAgAQAAAKkCACABAAAArAIAIAUEAADkBQAgKQAA5QUAICoAAOgFACArAADnBQAgLAAA5gUAIAbPAgAAtQQAMNACAAC_AgAQ0QIAALUEADDiAgIAhAQAIYYDAQCDBAAhhwMCAIQEACEDAAAArAIAIAEAAL4CADAoAAC_AgAgAwAAAKwCACABAACtAgAwAgAAqQIAIAcNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAAAABhgMBAAAAAQEAAADCAgAgAQAAAMICACAHDQAAtAQAIM8CAACzBAAw0AIAAMUCABDRAgAAswQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQENAADjBQAgAwAAAMUCACABAADGAgAwAgAAwgIAIAMAAADFAgAgAQAAxgIAMAIAAMICACADAAAAxQIAIAEAAMYCADACAADCAgAgBA0AAOIFACDUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAygIAIAPUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAzAIAMAEcAADMAgAwBA0AALYFACDUAgIA6QQAIeICAgDpBAAhhgMBAOsEACECAAAAwgIAIBwAAM8CACAD1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhAgAAAMUCACAcAADRAgAgAgAAAMUCACAcAADRAgAgAwAAAMICACAjAADKAgAgJAAAzwIAIAEAAADCAgAgAQAAAMUCACAFBAAAsQUAICkAALIFACAqAAC1BQAgKwAAtAUAICwAALMFACAGzwIAALIEADDQAgAA2AIAENECAACyBAAw1AICAIQEACHiAgIAhAQAIYYDAQCDBAAhAwAAAMUCACABAADXAgAwKAAA2AIAIAMAAADFAgAgAQAAxgIAMAIAAMICACABAAAAHQAgAQAAAB0AIAMAAAAbACABAAAcADACAAAdACADAAAAGwAgAQAAHAAwAgAAHQAgAwAAABsAIAEAABwAMAIAAB0AIAoGAACwBQAg1QJAAAAAAeICAQAAAAHnAgEAAAAB6wIBAAAAAYEDAQAAAAGCAyAAAAABgwNAAAAAAYQDAQAAAAGFAwEAAAABARwAAOACACAJ1QJAAAAAAeICAQAAAAHnAgEAAAAB6wIBAAAAAYEDAQAAAAGCAyAAAAABgwNAAAAAAYQDAQAAAAGFAwEAAAABARwAAOICADABHAAA4gIAMAEAAAAFACAKBgAArwUAINUCQADqBAAh4gIBAOsEACHnAgEAoQUAIesCAQChBQAhgQMBAOsEACGCAyAA9QQAIYMDQACrBQAhhAMBAKEFACGFAwEAoQUAIQIAAAAdACAcAADmAgAgCdUCQADqBAAh4gIBAOsEACHnAgEAoQUAIesCAQChBQAhgQMBAOsEACGCAyAA9QQAIYMDQACrBQAhhAMBAKEFACGFAwEAoQUAIQIAAAAbACAcAADoAgAgAgAAABsAIBwAAOgCACABAAAABQAgAwAAAB0AICMAAOACACAkAADmAgAgAQAAAB0AIAEAAAAbACAIBAAArAUAICsAAK4FACAsAACtBQAg5wIAAJsFACDrAgAAmwUAIIMDAACbBQAghAMAAJsFACCFAwAAmwUAIAzPAgAAsQQAMNACAADwAgAQ0QIAALEEADDVAkAAhQQAIeICAQCDBAAh5wIBAJcEACHrAgEAlwQAIYEDAQCDBAAhggMgAI4EACGDA0AArAQAIYQDAQCXBAAhhQMBAJcEACEDAAAAGwAgAQAA7wIAMCgAAPACACADAAAAGwAgAQAAHAAwAgAAHQAgCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAAAAAf0CAQChBAAh_gIBAAAAAf8CQACwBAAhgANAALAEACEBAAAA8wIAIAEAAADzAgAgCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAKEEACH9AgEAoQQAIf4CAQChBAAh_wJAALAEACGAA0AAsAQAIQL_AgAAmwUAIIADAACbBQAgAwAAAPYCACABAAD3AgAwAgAA8wIAIAMAAAD2AgAgAQAA9wIAMAIAAPMCACADAAAA9gIAIAEAAPcCADACAADzAgAgBtUCQAAAAAHiAgEAAAAB_QIBAAAAAf4CAQAAAAH_AkAAAAABgANAAAAAAQEcAAD7AgAgBtUCQAAAAAHiAgEAAAAB_QIBAAAAAf4CAQAAAAH_AkAAAAABgANAAAAAAQEcAAD9AgAwARwAAP0CADAG1QJAAOoEACHiAgEA6wQAIf0CAQDrBAAh_gIBAOsEACH_AkAAqwUAIYADQACrBQAhAgAAAPMCACAcAACAAwAgBtUCQADqBAAh4gIBAOsEACH9AgEA6wQAIf4CAQDrBAAh_wJAAKsFACGAA0AAqwUAIQIAAAD2AgAgHAAAggMAIAIAAAD2AgAgHAAAggMAIAMAAADzAgAgIwAA-wIAICQAAIADACABAAAA8wIAIAEAAAD2AgAgBQQAAKgFACArAACqBQAgLAAAqQUAIP8CAACbBQAggAMAAJsFACAJzwIAAKsEADDQAgAAiQMAENECAACrBAAw1QJAAIUEACHiAgEAgwQAIf0CAQCDBAAh_gIBAIMEACH_AkAArAQAIYADQACsBAAhAwAAAPYCACABAACIAwAwKAAAiQMAIAMAAAD2AgAgAQAA9wIAMAIAAPMCACAFzwIAAKgEADDQAgAAjwMAENECAACoBAAw-wICAAAAAfwCAACqBAAgAQAAAIwDACABAAAAjAMAIAXPAgAAqAQAMNACAACPAwAQ0QIAAKgEADD7AgIAqQQAIfwCAACqBAAgAAMAAACPAwAgAQAAkAMAMAIAAIwDACADAAAAjwMAIAEAAJADADACAACMAwAgAwAAAI8DACABAACQAwAwAgAAjAMAIAL7AgIAAAAB_AKAAAAAAQEcAACUAwAgAvsCAgAAAAH8AoAAAAABARwAAJYDADABHAAAlgMAMAL7AgIA6QQAIfwCgAAAAAECAAAAjAMAIBwAAJkDACAC-wICAOkEACH8AoAAAAABAgAAAI8DACAcAACbAwAgAgAAAI8DACAcAACbAwAgAwAAAIwDACAjAACUAwAgJAAAmQMAIAEAAACMAwAgAQAAAI8DACAFBAAAowUAICkAAKQFACAqAACnBQAgKwAApgUAICwAAKUFACAFzwIAAKUEADDQAgAAogMAENECAAClBAAw-wICAIQEACH8AgAApgQAIAMAAACPAwAgAQAAoQMAMCgAAKIDACADAAAAjwMAIAEAAJADADACAACMAwAgEM8CAACgBAAw0AIAAKgDABDRAgAAoAQAMNUCQACTBAAh4gIBAAAAAecCAQCiBAAh6wIBAKIEACHsAgIAowQAIe0CAQCiBAAh7gIBAKEEACHvAgEAoQQAIfACAQCiBAAh8QIBAKIEACHyAgAApAQAIPMCAACkBAAg9AIBAKIEACEBAAAApQMAIAEAAAClAwAgEM8CAACgBAAw0AIAAKgDABDRAgAAoAQAMNUCQACTBAAh4gIBAKEEACHnAgEAogQAIesCAQCiBAAh7AICAKMEACHtAgEAogQAIe4CAQChBAAh7wIBAKEEACHwAgEAogQAIfECAQCiBAAh8gIAAKQEACDzAgAApAQAIPQCAQCiBAAhCecCAACbBQAg6wIAAJsFACDsAgAAmwUAIO0CAACbBQAg8AIAAJsFACDxAgAAmwUAIPICAACbBQAg8wIAAJsFACD0AgAAmwUAIAMAAACoAwAgAQAAqQMAMAIAAKUDACADAAAAqAMAIAEAAKkDADACAAClAwAgAwAAAKgDACABAACpAwAwAgAApQMAIA3VAkAAAAAB4gIBAAAAAecCAQAAAAHrAgEAAAAB7AICAAAAAe0CAQAAAAHuAgEAAAAB7wIBAAAAAfACAQAAAAHxAgEAAAAB8gKAAAAAAfMCgAAAAAH0AgEAAAABARwAAK0DACAN1QJAAAAAAeICAQAAAAHnAgEAAAAB6wIBAAAAAewCAgAAAAHtAgEAAAAB7gIBAAAAAe8CAQAAAAHwAgEAAAAB8QIBAAAAAfICgAAAAAHzAoAAAAAB9AIBAAAAAQEcAACvAwAwARwAAK8DADAN1QJAAOoEACHiAgEA6wQAIecCAQChBQAh6wIBAKEFACHsAgIAogUAIe0CAQChBQAh7gIBAOsEACHvAgEA6wQAIfACAQChBQAh8QIBAKEFACHyAoAAAAAB8wKAAAAAAfQCAQChBQAhAgAAAKUDACAcAACyAwAgDdUCQADqBAAh4gIBAOsEACHnAgEAoQUAIesCAQChBQAh7AICAKIFACHtAgEAoQUAIe4CAQDrBAAh7wIBAOsEACHwAgEAoQUAIfECAQChBQAh8gKAAAAAAfMCgAAAAAH0AgEAoQUAIQIAAACoAwAgHAAAtAMAIAIAAACoAwAgHAAAtAMAIAMAAAClAwAgIwAArQMAICQAALIDACABAAAApQMAIAEAAACoAwAgDgQAAJwFACApAACdBQAgKgAAoAUAICsAAJ8FACAsAACeBQAg5wIAAJsFACDrAgAAmwUAIOwCAACbBQAg7QIAAJsFACDwAgAAmwUAIPECAACbBQAg8gIAAJsFACDzAgAAmwUAIPQCAACbBQAgEM8CAACWBAAw0AIAALsDABDRAgAAlgQAMNUCQACFBAAh4gIBAIMEACHnAgEAlwQAIesCAQCXBAAh7AICAJgEACHtAgEAlwQAIe4CAQCDBAAh7wIBAIMEACHwAgEAlwQAIfECAQCXBAAh8gIAAJkEACDzAgAAmQQAIPQCAQCXBAAhAwAAAKgDACABAAC6AwAwKAAAuwMAIAMAAACoAwAgAQAAqQMAMAIAAKUDACAJBgAAlAQAIBIAAJUEACDPAgAAkgQAMNACAAA7ABDRAgAAkgQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB5wIBAAAAAQEAAAC-AwAgAQAAAL4DACACBgAAmQUAIBIAAJoFACADAAAAOwAgAQAAwQMAMAIAAL4DACADAAAAOwAgAQAAwQMAMAIAAL4DACADAAAAOwAgAQAAwQMAMAIAAL4DACAGBgAAlwUAIBIAAJgFACDVAkAAAAAB1gJAAAAAAeICAQAAAAHnAgEAAAABARwAAMUDACAE1QJAAAAAAdYCQAAAAAHiAgEAAAAB5wIBAAAAAQEcAADHAwAwARwAAMcDADAGBgAAiQUAIBIAAIoFACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIQIAAAC-AwAgHAAAygMAIATVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIQIAAAA7ACAcAADMAwAgAgAAADsAIBwAAMwDACADAAAAvgMAICMAAMUDACAkAADKAwAgAQAAAL4DACABAAAAOwAgAwQAAIYFACArAACIBQAgLAAAhwUAIAfPAgAAkQQAMNACAADTAwAQ0QIAAJEEADDVAkAAhQQAIdYCQACFBAAh4gIBAIMEACHnAgEAgwQAIQMAAAA7ACABAADSAwAwKAAA0wMAIAMAAAA7ACABAADBAwAwAgAAvgMAIAEAAAA0ACABAAAANAAgAwAAADIAIAEAADMAMAIAADQAIAMAAAAyACABAAAzADACAAA0ACADAAAAMgAgAQAAMwAwAgAANAAgCQ0AAIUFACATAACEBQAg1QJAAAAAAdYCQAAAAAHiAgEAAAAB4wIBAAAAAeQCAgAAAAHlAgIAAAAB5gIgAAAAAQEcAADbAwAgB9UCQAAAAAHWAkAAAAAB4gIBAAAAAeMCAQAAAAHkAgIAAAAB5QICAAAAAeYCIAAAAAEBHAAA3QMAMAEcAADdAwAwCQ0AAPcEACATAAD2BAAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh4wIBAOsEACHkAgIA6QQAIeUCAgDpBAAh5gIgAPUEACECAAAANAAgHAAA4AMAIAfVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHjAgEA6wQAIeQCAgDpBAAh5QICAOkEACHmAiAA9QQAIQIAAAAyACAcAADiAwAgAgAAADIAIBwAAOIDACADAAAANAAgIwAA2wMAICQAAOADACABAAAANAAgAQAAADIAIAUEAADwBAAgKQAA8QQAICoAAPQEACArAADzBAAgLAAA8gQAIArPAgAAjQQAMNACAADpAwAQ0QIAAI0EADDVAkAAhQQAIdYCQACFBAAh4gIBAIMEACHjAgEAgwQAIeQCAgCEBAAh5QICAIQEACHmAiAAjgQAIQMAAAAyACABAADoAwAwKAAA6QMAIAMAAAAyACABAAAzADACAAA0ACABAAAAMAAgAQAAADAAIAMAAAAuACABAAAvADACAAAwACADAAAALgAgAQAALwAwAgAAMAAgAwAAAC4AIAEAAC8AMAIAADAAIAcUAADuBAAgFQAA7wQAINICAQAAAAHTAgEAAAAB1AICAAAAAdUCQAAAAAHWAkAAAAABARwAAPEDACAF0gIBAAAAAdMCAQAAAAHUAgIAAAAB1QJAAAAAAdYCQAAAAAEBHAAA8wMAMAEcAADzAwAwBxQAAOwEACAVAADtBAAg0gIBAOsEACHTAgEA6wQAIdQCAgDpBAAh1QJAAOoEACHWAkAA6gQAIQIAAAAwACAcAAD2AwAgBdICAQDrBAAh0wIBAOsEACHUAgIA6QQAIdUCQADqBAAh1gJAAOoEACECAAAALgAgHAAA-AMAIAIAAAAuACAcAAD4AwAgAwAAADAAICMAAPEDACAkAAD2AwAgAQAAADAAIAEAAAAuACAFBAAA5AQAICkAAOUEACAqAADoBAAgKwAA5wQAICwAAOYEACAIzwIAAIIEADDQAgAA_wMAENECAACCBAAw0gIBAIMEACHTAgEAgwQAIdQCAgCEBAAh1QJAAIUEACHWAkAAhQQAIQMAAAAuACABAAD-AwAwKAAA_wMAIAMAAAAuACABAAAvADACAAAwACAIzwIAAIIEADDQAgAA_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_AIAAKYEACAPBAAAhwQAICsAAKcEACAsAACnBAAg1wKAAAAAAdoCgAAAAAHbAoAAAAAB3AKAAAAAAd0CgAAAAAHeAoAAAAAB9QIBAAAAAfYCAQAAAAH3AgEAAAAB-AKAAAAAAfkCgAAAAAH6AoAAAAABDNcCgAAAAAHaAoAAAAAB2wKAAAAAAdwCgAAAAAHdAoAAAAAB3gKAAAAAAfUCAQAAAAH2AgEAAAAB9wIBAAAAAfgCgAAAAAH5AoAAAAAB-gKAAAAAAQXPAgAAqAQAMNACAACPAwAQ0QIAAKgEADD7AgIAqQQAIfwCAACqBAAgCNcCAgAAAAHYAgIAAAAE2QICAAAABNoCAgAAAAHbAgIAAAAB3AICAAAAAd0CAgAAAAHeAgIAhwQAIQzXAoAAAAAB2gKAAAAAAdsCgAAAAAHcAoAAAAAB3QKAAAAAAd4CgAAAAAH1AgEAAAAB9gIBAAAAAfcCAQAAAAH4AoAAAAAB-QKAAAAAAfoCgAAAAAEJzwIAAKsEADDQAgAAiQMAENECAACrBAAw1QJAAIUEACHiAgEAgwQAIf0CAQCDBAAh_gIBAIMEACH_AkAArAQAIYADQACsBAAhCwQAAJoEACArAACuBAAgLAAArgQAINcCQAAAAAHYAkAAAAAF2QJAAAAABdoCQAAAAAHbAkAAAAAB3AJAAAAAAd0CQAAAAAHeAkAArQQAIQsEAACaBAAgKwAArgQAICwAAK4EACDXAkAAAAAB2AJAAAAABdkCQAAAAAXaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAK0EACEI1wJAAAAAAdgCQAAAAAXZAkAAAAAF2gJAAAAAAdsCQAAAAAHcAkAAAAAB3QJAAAAAAd4CQACuBAAhCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAKEEACH9AgEAoQQAIf4CAQChBAAh_wJAALAEACGAA0AAsAQAIQjXAkAAAAAB2AJAAAAABdkCQAAAAAXaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAK4EACEMzwIAALEEADDQAgAA8AIAENECAACxBAAw1QJAAIUEACHiAgEAgwQAIecCAQCXBAAh6wIBAJcEACGBAwEAgwQAIYIDIACOBAAhgwNAAKwEACGEAwEAlwQAIYUDAQCXBAAhBs8CAACyBAAw0AIAANgCABDRAgAAsgQAMNQCAgCEBAAh4gICAIQEACGGAwEAgwQAIQcNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhA-gCAAAkACDpAgAAJAAg6gIAACQAIAbPAgAAtQQAMNACAAC_AgAQ0QIAALUEADDiAgIAhAQAIYYDAQCDBAAhhwMCAIQEACEHDQAAtAQAIM8CAAC2BAAw0AIAAKwCABDRAgAAtgQAMOICAgCpBAAhhgMBAKEEACGHAwIAqQQAIQfPAgAAtwQAMNACAACmAgAQ0QIAALcEADDTAgEAgwQAIdUCQACFBAAh1gJAAIUEACHnAgEAgwQAIQbPAgAAuAQAMNACAACQAgAQ0QIAALgEADDUAgIAhAQAIeICAgCEBAAhhgMBAIMEACEHDQAAtAQAIM8CAAC5BAAw0AIAAP0BABDRAgAAuQQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQnPAgAAugQAMNACAAD3AQAQ0QIAALoEADDiAgEAgwQAIfsCAgCEBAAhhgMBAIMEACGIAwEAgwQAIYkDAgCEBAAhigMCAIQEACEJzwIAALsEADDQAgAA4QEAENECAAC7BAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh_wJAAIUEACGHAwEAgwQAIYsDAQCDBAAhCc8CAAC8BAAw0AIAAM4BABDRAgAAvAQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIf8CQACTBAAhhwMBAKEEACGLAwEAoQQAIQXPAgAAvQQAMNACAADIAQAQ0QIAAL0EADDiAgIAhAQAIYYDAQCDBAAhBgMAAL8EACDPAgAAvgQAMNACAAADABDRAgAAvgQAMOICAgCpBAAhhgMBAKEEACED6AIAAAUAIOkCAAAFACDqAgAABQAgEM8CAADABAAw0AIAALABABDRAgAAwAQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAhjAMBAIMEACGNAwEAgwQAIY4DAQCXBAAhjwMBAJcEACGQAwEAlwQAIZEDQACsBAAhkgNAAKwEACGTAwEAlwQAIZQDAQCXBAAhC88CAADBBAAw0AIAAJoBABDRAgAAwQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAh_wJAAIUEACGVAwEAgwQAIZYDAQCXBAAhlwMBAJcEACEOzwIAAMIEADDQAgAAhAEAENECAADCBAAw4gIBAIMEACGYAwEAgwQAIZkDAQCDBAAhmgMBAIMEACGbAwEAgwQAIZwDAQCDBAAhnQMBAIMEACGeAwEAgwQAIZ8DIACOBAAhoAMgAI4EACGhAyAAjgQAIQrPAgAAwwQAMNACAABuABDRAgAAwwQAMOICAQCDBAAh5wIBAJcEACHrAgEAlwQAIaIDAQCDBAAhowMBAIMEACGkAwEAgwQAIaUDAQCXBAAhC88CAADEBAAw0AIAAFYAENECAADEBAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh6wIBAIMEACHsAgIAmAQAIYYDAQCDBAAhpgMgAI4EACGnAwEAlwQAIQwNAADIBAAgEwAAxwQAIM8CAADFBAAw0AIAADIAENECAADFBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh4wIBAKEEACHkAgIAqQQAIeUCAgCpBAAh5gIgAMYEACEC1wIgAAAAAd4CIACQBAAhCwYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIasDAAA7ACCsAwAAOwAgA-gCAAAuACDpAgAALgAg6gIAAC4AIALSAgEAAAAB0wIBAAAAAQoUAADLBAAgFQAAzAQAIM8CAADKBAAw0AIAAC4AENECAADKBAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIQ4NAADIBAAgEwAAxwQAIM8CAADFBAAw0AIAADIAENECAADFBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh4wIBAKEEACHkAgIAqQQAIeUCAgCpBAAh5gIgAMYEACGrAwAAMgAgrAMAADIAIBAOAADOBAAgDwAAzwQAIBAAANAEACARAADRBAAgFgAAyAQAIM8CAADNBAAw0AIAACQAENECAADNBAAw4gIBAKEEACH7AgIAqQQAIYYDAQChBAAhiAMBAKEEACGJAwIAqQQAIYoDAgCpBAAhqwMAACQAIKwDAAAkACAODgAAzgQAIA8AAM8EACAQAADQBAAgEQAA0QQAIBYAAMgEACDPAgAAzQQAMNACAAAkABDRAgAAzQQAMOICAQChBAAh-wICAKkEACGGAwEAoQQAIYgDAQChBAAhiQMCAKkEACGKAwIAqQQAIQkNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAKkEACGGAwEAoQQAIYcDAgCpBAAhqwMAAKwCACCsAwAArAIAIAkNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhqwMAAMUCACCsAwAAxQIAIAkNAAC0BAAgzwIAALkEADDQAgAA_QEAENECAAC5BAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhqwMAAP0BACCsAwAA_QEAIAPoAgAAIAAg6QIAACAAIOoCAAAgACAC0wIBAAAAAecCAQAAAAEJBgAAlAQAIBUAAMwEACDPAgAA0wQAMNACAAAgABDRAgAA0wQAMNMCAQChBAAh1QJAAJMEACHWAkAAkwQAIecCAQChBAAhAucCAQAAAAGFAwEAAAABDQYAANYEACDPAgAA1QQAMNACAAAbABDRAgAA1QQAMNUCQACTBAAh4gIBAKEEACHnAgEAogQAIesCAQCiBAAhgQMBAKEEACGCAyAAxgQAIYMDQACwBAAhhAMBAKIEACGFAwEAogQAIRQFAADeBAAgBwAA3wQAIAgAAOAEACALAADhBAAgDAAA4gQAIBEAANEEACATAADjBAAgzwIAAN0EADDQAgAABQAQ0QIAAN0EADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHrAgEAoQQAIewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhqwMAAAUAIKwDAAAFACAPCQAA2AQAIM8CAADXBAAw0AIAABYAENECAADXBAAw4gIBAKEEACGYAwEAoQQAIZkDAQChBAAhmgMBAKEEACGbAwEAoQQAIZwDAQChBAAhnQMBAKEEACGeAwEAoQQAIZ8DIADGBAAhoAMgAMYEACGhAyAAxgQAIQ4GAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAKEEACHnAgEAogQAIesCAQCiBAAhogMBAKEEACGjAwEAoQQAIaQDAQChBAAhpQMBAKIEACGrAwAAEQAgrAMAABEAIAwGAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAKEEACHnAgEAogQAIesCAQCiBAAhogMBAKEEACGjAwEAoQQAIaQDAQChBAAhpQMBAKIEACED6AIAABYAIOkCAAAWACDqAgAAFgAgEQYAAJQEACDPAgAA2wQAMNACAAANABDRAgAA2wQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAhjAMBAKEEACGNAwEAoQQAIY4DAQCiBAAhjwMBAKIEACGQAwEAogQAIZEDQACwBAAhkgNAALAEACGTAwEAogQAIZQDAQCiBAAhDAYAAJQEACDPAgAA3AQAMNACAAAJABDRAgAA3AQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAh_wJAAJMEACGVAwEAoQQAIZYDAQCiBAAhlwMBAKIEACESBQAA3gQAIAcAAN8EACAIAADgBAAgCwAA4QQAIAwAAOIEACARAADRBAAgEwAA4wQAIM8CAADdBAAw0AIAAAUAENECAADdBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh6wIBAKEEACHsAgIAowQAIYYDAQChBAAhpgMgAMYEACGnAwEAogQAIQgDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYYDAQChBAAhqwMAAAMAIKwDAAADACAD6AIAAAkAIOkCAAAJACDqAgAACQAgA-gCAAANACDpAgAADQAg6gIAAA0AIAPoAgAAEQAg6QIAABEAIOoCAAARACAD6AIAABsAIOkCAAAbACDqAgAAGwAgCwYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIasDAAA7ACCsAwAAOwAgAAAAAAAFsAMCAAAAAbYDAgAAAAG3AwIAAAABuAMCAAAAAbkDAgAAAAEBsANAAAAAAQGwAwEAAAABBSMAAPoHACAkAACACAAgrQMAAPsHACCuAwAA_wcAILMDAAA0ACAFIwAA-AcAICQAAP0HACCtAwAA-QcAIK4DAAD8BwAgswMAACYAIAMjAAD6BwAgrQMAAPsHACCzAwAANAAgAyMAAPgHACCtAwAA-QcAILMDAAAmACAAAAAAAAGwAyAAAAABBSMAAPIHACAkAAD2BwAgrQMAAPMHACCuAwAA9QcAILMDAAC-AwAgCyMAAPgEADAkAAD9BAAwrQMAAPkEADCuAwAA-gQAMK8DAAD7BAAgsAMAAPwEADCxAwAA_AQAMLIDAAD8BAAwswMAAPwEADC0AwAA_gQAMLUDAAD_BAAwBRUAAO8EACDTAgEAAAAB1AICAAAAAdUCQAAAAAHWAkAAAAABAgAAADAAICMAAIMFACADAAAAMAAgIwAAgwUAICQAAIIFACABHAAA9AcAMAsUAADLBAAgFQAAzAQAIM8CAADKBAAw0AIAAC4AENECAADKBAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIagDAADJBAAgAgAAADAAIBwAAIIFACACAAAAgAUAIBwAAIEFACAIzwIAAP8EADDQAgAAgAUAENECAAD_BAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIQjPAgAA_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-BQAw0AIAAL8FABDRAgAAvgUAMOICAQChBAAh-wICAKkEACGGAwEAoQQAIYgDAQChBAAhiQMCAKkEACGKAwIAqQQAIQXiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYoDAgDpBAAhCQ4AAMIFACAQAADDBQAgEQAAxAUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYoDAgDpBAAhBSMAANcHACAkAADkBwAgrQMAANgHACCuAwAA4wcAILMDAACpAgAgBSMAANUHACAkAADhBwAgrQMAANYHACCuAwAA4AcAILMDAAD6AQAgCyMAAM8FADAkAADUBQAwrQMAANAFADCuAwAA0QUAMK8DAADSBQAgsAMAANMFADCxAwAA0wUAMLIDAADTBQAwswMAANMFADC0AwAA1QUAMLUDAADWBQAwCyMAAMYFADAkAADKBQAwrQMAAMcFADCuAwAAyAUAMK8DAADJBQAgsAMAAPwEADCxAwAA_AQAMLIDAAD8BAAwswMAAPwEADC0AwAAywUAMLUDAAD_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_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_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABAgAAAAsAICMAAPUGACADAAAACwAgIwAA9QYAICQAAPQGACABHAAAwgcAMAwGAACUBAAgzwIAANwEADDQAgAACQAQ0QIAANwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAecCAQChBAAh_wJAAJMEACGVAwEAAAABlgMBAKIEACGXAwEAogQAIQIAAAALACAcAAD0BgAgAgAAAPIGACAcAADzBgAgC88CAADxBgAw0AIAAPIGABDRAgAA8QYAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAh_wJAAJMEACGVAwEAoQQAIZYDAQCiBAAhlwMBAKIEACELzwIAAPEGADDQAgAA8gYAENECAADxBgAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACH_AkAAkwQAIZUDAQChBAAhlgMBAKIEACGXAwEAogQAIQfVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQfVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQfVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABDQcAAPcGACAIAAD4BgAgCwAA-QYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAGGAwEAAAABpgMgAAAAAacDAQAAAAEEIwAA6gYAMK0DAADrBgAwrwMAAO0GACCzAwAA7gYAMAQjAADeBgAwrQMAAN8GADCvAwAA4QYAILMDAADiBgAwBCMAAMQGADCtAwAAxQYAMK8DAADHBgAgswMAAMgGADAEIwAAuAYAMK0DAAC5BgAwrwMAALsGACCzAwAAvAYAMAQjAACvBgAwrQMAALAGADCvAwAAsgYAILMDAADTBQAwAyMAAKoGACCtAwAAqwYAILMDAAC-AwAgBCMAAJkGADCtAwAAmgYAMK8DAACcBgAgswMAAJ0GADAAAAAABSMAAL0HACAkAADABwAgrQMAAL4HACCuAwAAvwcAILMDAAABACADIwAAvQcAIK0DAAC-BwAgswMAAAEAIAAAAAUjAAC4BwAgJAAAuwcAIK0DAAC5BwAgrgMAALoHACCzAwAAAQAgAyMAALgHACCtAwAAuQcAILMDAAABACAAAAAFIwAAswcAICQAALYHACCtAwAAtAcAIK4DAAC1BwAgswMAABMAIAMjAACzBwAgrQMAALQHACCzAwAAEwAgAAAAByMAAK4HACAkAACxBwAgrQMAAK8HACCuAwAAsAcAILEDAAAFACCyAwAABQAgswMAAAEAIAMjAACuBwAgrQMAAK8HACCzAwAAAQAgAAAAAAAHIwAAqQcAICQAAKwHACCtAwAAqgcAIK4DAACrBwAgsQMAAAMAILIDAAADACCzAwAAswEAIAMjAACpBwAgrQMAAKoHACCzAwAAswEAIAIGAACZBQAgEgAAmgUAIAACDQAAmwcAIBMAAJoHACAFDgAAngcAIA8AAJ8HACAQAACgBwAgEQAAoQcAIBYAAJsHACABDQAA4wUAIAENAADjBQAgAQ0AAOMFACAABQYAAJkFACAKAACjBwAg5wIAAJsFACDrAgAAmwUAIKUDAACbBQAgAAEDAAD-BgAgAAAAAALiAgIAAAABhgMBAAAAAQIAAACzAQAgIwAAqQcAIAMAAAADACAjAACpBwAgJAAArQcAIAQAAAADACAcAACtBwAg4gICAOkEACGGAwEA6wQAIQLiAgIA6QQAIYYDAQDrBAAhDgUAAJkHACAHAAD3BgAgCAAA-AYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhgMBAAAAAaYDIAAAAAGnAwEAAAABAgAAAAEAICMAAK4HACADAAAABQAgIwAArgcAICQAALIHACAQAAAABQAgBQAAmAcAIAcAAKQGACAIAAClBgAgDAAApwYAIBEAAKgGACATAACpBgAgHAAAsgcAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEOBQAAmAcAIAcAAKQGACAIAAClBgAgDAAApwYAIBEAAKgGACATAACpBgAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQgGAACSBwAg4gIBAAAAAecCAQAAAAHrAgEAAAABogMBAAAAAaMDAQAAAAGkAwEAAAABpQMBAAAAAQIAAAATACAjAACzBwAgAwAAABEAICMAALMHACAkAAC3BwAgCgAAABEAIAYAAJEHACAcAAC3BwAg4gIBAOsEACHnAgEAoQUAIesCAQChBQAhogMBAOsEACGjAwEA6wQAIaQDAQDrBAAhpQMBAKEFACEIBgAAkQcAIOICAQDrBAAh5wIBAKEFACHrAgEAoQUAIaIDAQDrBAAhowMBAOsEACGkAwEA6wQAIaUDAQChBQAhDgUAAJkHACAIAAD4BgAgCwAA-QYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhgMBAAAAAaYDIAAAAAGnAwEAAAABAgAAAAEAICMAALgHACADAAAABQAgIwAAuAcAICQAALwHACAQAAAABQAgBQAAmAcAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACATAACpBgAgHAAAvAcAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEOBQAAmAcAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACATAACpBgAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQ4FAACZBwAgBwAA9wYAIAsAAPkGACAMAAD6BgAgEQAA-wYAIBMAAPwGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAAB7AICAAAAAYYDAQAAAAGmAyAAAAABpwMBAAAAAQIAAAABACAjAAC9BwAgAwAAAAUAICMAAL0HACAkAADBBwAgEAAAAAUAIAUAAJgHACAHAACkBgAgCwAApgYAIAwAAKcGACARAACoBgAgEwAAqQYAIBwAAMEHACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHrAgEA6wQAIewCAgCiBQAhhgMBAOsEACGmAyAA9QQAIacDAQChBQAhDgUAAJgHACAHAACkBgAgCwAApgYAIAwAAKcGACARAACoBgAgEwAAqQYAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEH1QJAAAAAAdYCQAAAAAHiAgEAAAAB_wJAAAAAAZUDAQAAAAGWAwEAAAABlwMBAAAAAQzVAkAAAAAB1gJAAAAAAeICAQAAAAGMAwEAAAABjQMBAAAAAY4DAQAAAAGPAwEAAAABkAMBAAAAAZEDQAAAAAGSA0AAAAABkwMBAAAAAZQDAQAAAAEK4gIBAAAAAZkDAQAAAAGaAwEAAAABmwMBAAAAAZwDAQAAAAGdAwEAAAABngMBAAAAAZ8DIAAAAAGgAyAAAAABoQMgAAAAAQbiAgEAAAAB6wIBAAAAAaIDAQAAAAGjAwEAAAABpAMBAAAAAaUDAQAAAAEI1QJAAAAAAeICAQAAAAHrAgEAAAABgQMBAAAAAYIDIAAAAAGDA0AAAAABhAMBAAAAAYUDAQAAAAED0wIBAAAAAdUCQAAAAAHWAkAAAAABB9UCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAGGAwEAAAABpgMgAAAAAacDAQAAAAEF4gIBAAAAAfsCAgAAAAGGAwEAAAABiAMBAAAAAYkDAgAAAAEKDgAA3gUAIA8AAPQFACAQAADfBQAgFgAA4QUAIOICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQIAAAAmACAjAADKBwAgAwAAACQAICMAAMoHACAkAADOBwAgDAAAACQAIA4AAMIFACAPAADyBQAgEAAAwwUAIBYAAMUFACAcAADOBwAg4gIBAOsEACH7AgIA6QQAIYYDAQDrBAAhiAMBAOsEACGJAwIA6QQAIYoDAgDpBAAhCg4AAMIFACAPAADyBQAgEAAAwwUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACED1AICAAAAAeICAgAAAAGGAwEAAAABAgAAAMICACAjAADPBwAgAwAAAMUCACAjAADPBwAgJAAA0wcAIAUAAADFAgAgHAAA0wcAINQCAgDpBAAh4gICAOkEACGGAwEA6wQAIQPUAgIA6QQAIeICAgDpBAAhhgMBAOsEACEF4gIBAAAAAYYDAQAAAAGIAwEAAAABiQMCAAAAAYoDAgAAAAED1AICAAAAAeICAgAAAAGGAwEAAAABAgAAAPoBACAjAADVBwAgA-ICAgAAAAGGAwEAAAABhwMCAAAAAQIAAACpAgAgIwAA1wcAIA4FAACZBwAgBwAA9wYAIAgAAPgGACALAAD5BgAgDAAA-gYAIBMAAPwGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAAB7AICAAAAAYYDAQAAAAGmAyAAAAABpwMBAAAAAQIAAAABACAjAADZBwAgAwAAAAUAICMAANkHACAkAADdBwAgEAAAAAUAIAUAAJgHACAHAACkBgAgCAAApQYAIAsAAKYGACAMAACnBgAgEwAAqQYAIBwAAN0HACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHrAgEA6wQAIewCAgCiBQAhhgMBAOsEACGmAyAA9QQAIacDAQChBQAhDgUAAJgHACAHAACkBgAgCAAApQYAIAsAAKYGACAMAACnBgAgEwAAqQYAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACED1QJAAAAAAdYCQAAAAAHnAgEAAAABBNICAQAAAAHUAgIAAAAB1QJAAAAAAdYCQAAAAAEDAAAA_QEAICMAANUHACAkAADiBwAgBQAAAP0BACAcAADiBwAg1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhA9QCAgDpBAAh4gICAOkEACGGAwEA6wQAIQMAAACsAgAgIwAA1wcAICQAAOUHACAFAAAArAIAIBwAAOUHACDiAgIA6QQAIYYDAQDrBAAhhwMCAOkEACED4gICAOkEACGGAwEA6wQAIYcDAgDpBAAhBeICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGKAwIAAAABDgUAAJkHACAHAAD3BgAgCAAA-AYAIAsAAPkGACARAAD7BgAgEwAA_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-AAs_AAxAABFBAAABBUsCAQVRAgUEAB0pAB4qAB8rACAsACEAAAAAAAUEAB0pAB4qAB8rACAsACEBBmMBAQZpAQMEACYrACcsACgAAAADBAAmKwAnLAAoAQkABgEJAAYDBAAtKwAuLAAvAAAAAwQALSsALiwALwEGAAEBBgABAwQANCsANSwANgAAAAMEADQrADUsADYBBgABAQYAAQMEADsrADwsAD0AAAADBAA7KwA8LAA9AAAFBABCKQBDKgBEKwBFLABGAAAAAAAFBABCKQBDKgBEKwBFLABGAAAAAwQATCsATSwATgAAAAMEAEwrAE0sAE4DDgAMDwAOEAAQAw4ADA8ADhAAEAUEAFMpAFQqAFUrAFYsAFcAAAAAAAUEAFMpAFQqAFUrAFYsAFcAAAUEAFwpAF0qAF4rAF8sAGAAAAAAAAUEAFwpAF0qAF4rAF8sAGACBgABFQALAgYAARUACwMEAGUrAGYsAGcAAAADBABlKwBmLABnAAAFBABsKQBtKgBuKwBvLABwAAAAAAAFBABsKQBtKgBuKwBvLABwAAAFBAB1KQB2KgB3KwB4LAB5AAAAAAAFBAB1KQB2KgB3KwB4LAB5AQblAgEBBusCAQMEAH4rAH8sAIABAAAAAwQAfisAfywAgAEAAAADBACGASsAhwEsAIgBAAAAAwQAhgErAIcBLACIAQAAAAUEAI4BKQCPASoAkAErAJEBLACSAQAAAAAABQQAjgEpAI8BKgCQASsAkQEsAJIBAAAABQQAmAEpAJkBKgCaASsAmwEsAJwBAAAAAAAFBACYASkAmQEqAJoBKwCbASwAnAEBBgABAQYAAQMEAKEBKwCiASwAowEAAAADBAChASsAogEsAKMBARMAFAETABQFBACoASkAqQEqAKoBKwCrASwArAEAAAAAAAUEAKgBKQCpASoAqgErAKsBLACsAQIUABMVAAsCFAATFQALBQQAsQEpALIBKgCzASsAtAEsALUBAAAAAAAFBACxASkAsgEqALMBKwC0ASwAtQEXAgEYQgEZQwEaRAEbRQEdRwEeSRkfShogTQEhTxkiUBslUgEmUwEnVBktVxwuWCIvWQYwWgYxWwYyXAYzXQY0XwY1YRk2YiM3ZQY4Zxk5aCQ6agY7awY8bBk9byU-cCk_cQdAcgdBcwdCdAdDdQdEdwdFeRlGeipHfAdIfhlJfytKgAEHS4EBB0yCARlNhQEsToYBME-HAQRQiAEEUYkBBFKKAQRTiwEEVI0BBFWPARlWkAExV5IBBFiUARlZlQEyWpYBBFuXAQRcmAEZXZsBM16cATdfnQEFYJ4BBWGfAQVioAEFY6EBBWSjAQVlpQEZZqYBOGeoAQVoqgEZaasBOWqsAQVrrQEFbK4BGW2xATpusgE-b7QBAnC1AQJxtwECcrgBAnO5AQJ0uwECdb0BGXa-AT93wAECeMIBGXnDAUB6xAECe8UBAnzGARl9yQFBfsoBR3_MAUiAAc0BSIEB0AFIggHRAUiDAdIBSIQB1AFIhQHWARmGAdcBSYcB2QFIiAHbARmJAdwBSooB3QFIiwHeAUiMAd8BGY0B4gFLjgHjAU-PAeQBC5AB5QELkQHmAQuSAecBC5MB6AELlAHqAQuVAewBGZYB7QFQlwHvAQuYAfEBGZkB8gFRmgHzAQubAfQBC5wB9QEZnQH4AVKeAfkBWJ8B-wEQoAH8ARChAf8BEKIBgAIQowGBAhCkAYMCEKUBhQIZpgGGAlmnAYgCEKgBigIZqQGLAlqqAYwCEKsBjQIQrAGOAhmtAZECW64BkgJhrwGTAgqwAZQCCrEBlQIKsgGWAgqzAZcCCrQBmQIKtQGbAhm2AZwCYrcBngIKuAGgAhm5AaECY7oBogIKuwGjAgq8AaQCGb0BpwJkvgGoAmi_AaoCDMABqwIMwQGuAgzCAa8CDMMBsAIMxAGyAgzFAbQCGcYBtQJpxwG3AgzIAbkCGckBugJqygG7AgzLAbwCDMwBvQIZzQHAAmvOAcECcc8BwwIO0AHEAg7RAccCDtIByAIO0wHJAg7UAcsCDtUBzQIZ1gHOAnLXAdACDtgB0gIZ2QHTAnPaAdQCDtsB1QIO3AHWAhndAdkCdN4B2gJ63wHbAgngAdwCCeEB3QIJ4gHeAgnjAd8CCeQB4QIJ5QHjAhnmAeQCe-cB5wIJ6AHpAhnpAeoCfOoB7AIJ6wHtAgnsAe4CGe0B8QJ97gHyAoEB7wH0AoIB8AH1AoIB8QH4AoIB8gH5AoIB8wH6AoIB9AH8AoIB9QH-Ahn2Af8CgwH3AYEDggH4AYMDGfkBhAOEAfoBhQOCAfsBhgOCAfwBhwMZ_QGKA4UB_gGLA4kB_wGNA4oBgAKOA4oBgQKRA4oBggKSA4oBgwKTA4oBhAKVA4oBhQKXAxmGApgDiwGHApoDigGIApwDGYkCnQOMAYoCngOKAYsCnwOKAYwCoAMZjQKjA40BjgKkA5MBjwKmA5QBkAKnA5QBkQKqA5QBkgKrA5QBkwKsA5QBlAKuA5QBlQKwAxmWArEDlQGXArMDlAGYArUDGZkCtgOWAZoCtwOUAZsCuAOUAZwCuQMZnQK8A5cBngK9A50BnwK_AxSgAsADFKECwgMUogLDAxSjAsQDFKQCxgMUpQLIAxmmAskDngGnAssDFKgCzQMZqQLOA58BqgLPAxSrAtADFKwC0QMZrQLUA6ABrgLVA6QBrwLWAxOwAtcDE7EC2AMTsgLZAxOzAtoDE7QC3AMTtQLeAxm2At8DpQG3AuEDE7gC4wMZuQLkA6YBugLlAxO7AuYDE7wC5wMZvQLqA6cBvgLrA60BvwLsAxLAAu0DEsEC7gMSwgLvAxLDAvADEsQC8gMSxQL0AxnGAvUDrgHHAvcDEsgC-QMZyQL6A68BygL7AxLLAvwDEswC_QMZzQKABLABzgKBBLYB" } async function decodeBase64AsWasm(wasmBase64: string): Promise { diff --git a/src/lib/generated/prisma/internal/prismaNamespace.ts b/src/lib/generated/prisma/internal/prismaNamespace.ts index 2dc3566..9d918be 100644 --- a/src/lib/generated/prisma/internal/prismaNamespace.ts +++ b/src/lib/generated/prisma/internal/prismaNamespace.ts @@ -2020,7 +2020,9 @@ export const LicenseKeyScalarFieldEnum = { createdAt: 'createdAt', lastValidatedAt: 'lastValidatedAt', userId: 'userId', - email: 'email' + email: 'email', + systemName: 'systemName', + systemNameLower: 'systemNameLower' } as const export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum] diff --git a/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts index aa94cb6..fe7def9 100644 --- a/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -241,7 +241,9 @@ export const LicenseKeyScalarFieldEnum = { createdAt: 'createdAt', lastValidatedAt: 'lastValidatedAt', userId: 'userId', - email: 'email' + email: 'email', + systemName: 'systemName', + systemNameLower: 'systemNameLower' } as const export type LicenseKeyScalarFieldEnum = (typeof LicenseKeyScalarFieldEnum)[keyof typeof LicenseKeyScalarFieldEnum] diff --git a/src/lib/generated/prisma/models/LicenseKey.ts b/src/lib/generated/prisma/models/LicenseKey.ts index 3bba913..4a423ca 100644 --- a/src/lib/generated/prisma/models/LicenseKey.ts +++ b/src/lib/generated/prisma/models/LicenseKey.ts @@ -32,6 +32,8 @@ export type LicenseKeyMinAggregateOutputType = { lastValidatedAt: Date | null userId: string | null email: string | null + systemName: string | null + systemNameLower: string | null } export type LicenseKeyMaxAggregateOutputType = { @@ -42,6 +44,8 @@ export type LicenseKeyMaxAggregateOutputType = { lastValidatedAt: Date | null userId: string | null email: string | null + systemName: string | null + systemNameLower: string | null } export type LicenseKeyCountAggregateOutputType = { @@ -52,6 +56,8 @@ export type LicenseKeyCountAggregateOutputType = { lastValidatedAt: number userId: number email: number + systemName: number + systemNameLower: number _all: number } @@ -64,6 +70,8 @@ export type LicenseKeyMinAggregateInputType = { lastValidatedAt?: true userId?: true email?: true + systemName?: true + systemNameLower?: true } export type LicenseKeyMaxAggregateInputType = { @@ -74,6 +82,8 @@ export type LicenseKeyMaxAggregateInputType = { lastValidatedAt?: true userId?: true email?: true + systemName?: true + systemNameLower?: true } export type LicenseKeyCountAggregateInputType = { @@ -84,6 +94,8 @@ export type LicenseKeyCountAggregateInputType = { lastValidatedAt?: true userId?: true email?: true + systemName?: true + systemNameLower?: true _all?: true } @@ -167,6 +179,8 @@ export type LicenseKeyGroupByOutputType = { lastValidatedAt: Date | null userId: string | null email: string | null + systemName: string | null + systemNameLower: string | null _count: LicenseKeyCountAggregateOutputType | null _min: LicenseKeyMinAggregateOutputType | null _max: LicenseKeyMaxAggregateOutputType | null @@ -198,6 +212,8 @@ export type LicenseKeyWhereInput = { lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null user?: Prisma.XOR | null } @@ -209,12 +225,15 @@ export type LicenseKeyOrderByWithRelationInput = { lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder email?: Prisma.SortOrderInput | Prisma.SortOrder + systemName?: Prisma.SortOrderInput | Prisma.SortOrder + systemNameLower?: Prisma.SortOrderInput | Prisma.SortOrder user?: Prisma.UserOrderByWithRelationInput } export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{ id?: string key?: string + userId_systemNameLower?: Prisma.LicenseKeyUserIdSystemNameLowerCompoundUniqueInput AND?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[] OR?: Prisma.LicenseKeyWhereInput[] NOT?: Prisma.LicenseKeyWhereInput | Prisma.LicenseKeyWhereInput[] @@ -223,8 +242,10 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{ lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null user?: Prisma.XOR | null -}, "id" | "key"> +}, "id" | "key" | "userId_systemNameLower"> export type LicenseKeyOrderByWithAggregationInput = { id?: Prisma.SortOrder @@ -234,6 +255,8 @@ export type LicenseKeyOrderByWithAggregationInput = { lastValidatedAt?: Prisma.SortOrderInput | Prisma.SortOrder userId?: Prisma.SortOrderInput | Prisma.SortOrder email?: Prisma.SortOrderInput | Prisma.SortOrder + systemName?: Prisma.SortOrderInput | Prisma.SortOrder + systemNameLower?: Prisma.SortOrderInput | Prisma.SortOrder _count?: Prisma.LicenseKeyCountOrderByAggregateInput _max?: Prisma.LicenseKeyMaxOrderByAggregateInput _min?: Prisma.LicenseKeyMinOrderByAggregateInput @@ -250,6 +273,8 @@ export type LicenseKeyScalarWhereWithAggregatesInput = { lastValidatedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"LicenseKey"> | Date | string | null userId?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null email?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null + systemName?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null + systemNameLower?: Prisma.StringNullableWithAggregatesFilter<"LicenseKey"> | string | null } export type LicenseKeyCreateInput = { @@ -259,6 +284,8 @@ export type LicenseKeyCreateInput = { createdAt?: Date | string lastValidatedAt?: Date | string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput } @@ -270,6 +297,8 @@ export type LicenseKeyUncheckedCreateInput = { lastValidatedAt?: Date | string | null userId?: string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null } export type LicenseKeyUpdateInput = { @@ -279,6 +308,8 @@ export type LicenseKeyUpdateInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput } @@ -290,6 +321,8 @@ export type LicenseKeyUncheckedUpdateInput = { lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } export type LicenseKeyCreateManyInput = { @@ -300,6 +333,8 @@ export type LicenseKeyCreateManyInput = { lastValidatedAt?: Date | string | null userId?: string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null } export type LicenseKeyUpdateManyMutationInput = { @@ -309,6 +344,8 @@ export type LicenseKeyUpdateManyMutationInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } export type LicenseKeyUncheckedUpdateManyInput = { @@ -319,6 +356,8 @@ export type LicenseKeyUncheckedUpdateManyInput = { lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } export type LicenseKeyListRelationFilter = { @@ -331,6 +370,11 @@ export type LicenseKeyOrderByRelationAggregateInput = { _count?: Prisma.SortOrder } +export type LicenseKeyUserIdSystemNameLowerCompoundUniqueInput = { + userId: string + systemNameLower: string +} + export type LicenseKeyCountOrderByAggregateInput = { id?: Prisma.SortOrder key?: Prisma.SortOrder @@ -339,6 +383,8 @@ export type LicenseKeyCountOrderByAggregateInput = { lastValidatedAt?: Prisma.SortOrder userId?: Prisma.SortOrder email?: Prisma.SortOrder + systemName?: Prisma.SortOrder + systemNameLower?: Prisma.SortOrder } export type LicenseKeyMaxOrderByAggregateInput = { @@ -349,6 +395,8 @@ export type LicenseKeyMaxOrderByAggregateInput = { lastValidatedAt?: Prisma.SortOrder userId?: Prisma.SortOrder email?: Prisma.SortOrder + systemName?: Prisma.SortOrder + systemNameLower?: Prisma.SortOrder } export type LicenseKeyMinOrderByAggregateInput = { @@ -359,6 +407,8 @@ export type LicenseKeyMinOrderByAggregateInput = { lastValidatedAt?: Prisma.SortOrder userId?: Prisma.SortOrder email?: Prisma.SortOrder + systemName?: Prisma.SortOrder + systemNameLower?: Prisma.SortOrder } export type LicenseKeyCreateNestedManyWithoutUserInput = { @@ -410,6 +460,8 @@ export type LicenseKeyCreateWithoutUserInput = { createdAt?: Date | string lastValidatedAt?: Date | string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null } export type LicenseKeyUncheckedCreateWithoutUserInput = { @@ -419,6 +471,8 @@ export type LicenseKeyUncheckedCreateWithoutUserInput = { createdAt?: Date | string lastValidatedAt?: Date | string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null } export type LicenseKeyCreateOrConnectWithoutUserInput = { @@ -458,6 +512,8 @@ export type LicenseKeyScalarWhereInput = { lastValidatedAt?: Prisma.DateTimeNullableFilter<"LicenseKey"> | Date | string | null userId?: Prisma.StringNullableFilter<"LicenseKey"> | string | null email?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null + systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null } export type LicenseKeyCreateManyUserInput = { @@ -467,6 +523,8 @@ export type LicenseKeyCreateManyUserInput = { createdAt?: Date | string lastValidatedAt?: Date | string | null email?: string | null + systemName?: string | null + systemNameLower?: string | null } export type LicenseKeyUpdateWithoutUserInput = { @@ -476,6 +534,8 @@ export type LicenseKeyUpdateWithoutUserInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } export type LicenseKeyUncheckedUpdateWithoutUserInput = { @@ -485,6 +545,8 @@ export type LicenseKeyUncheckedUpdateWithoutUserInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } export type LicenseKeyUncheckedUpdateManyWithoutUserInput = { @@ -494,6 +556,8 @@ export type LicenseKeyUncheckedUpdateManyWithoutUserInput = { createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } @@ -506,6 +570,8 @@ export type LicenseKeySelect }, ExtArgs["result"]["licenseKey"]> @@ -517,6 +583,8 @@ export type LicenseKeySelectCreateManyAndReturn }, ExtArgs["result"]["licenseKey"]> @@ -528,6 +596,8 @@ export type LicenseKeySelectUpdateManyAndReturn }, ExtArgs["result"]["licenseKey"]> @@ -539,9 +609,11 @@ export type LicenseKeySelectScalar = { lastValidatedAt?: boolean userId?: boolean email?: boolean + systemName?: boolean + systemNameLower?: boolean } -export type LicenseKeyOmit = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "createdAt" | "lastValidatedAt" | "userId" | "email", ExtArgs["result"]["licenseKey"]> +export type LicenseKeyOmit = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "createdAt" | "lastValidatedAt" | "userId" | "email" | "systemName" | "systemNameLower", ExtArgs["result"]["licenseKey"]> export type LicenseKeyInclude = { user?: boolean | Prisma.LicenseKey$userArgs } @@ -565,6 +637,8 @@ export type $LicenseKeyPayload composites: {} } @@ -996,6 +1070,8 @@ export interface LicenseKeyFieldRefs { readonly lastValidatedAt: Prisma.FieldRef<"LicenseKey", 'DateTime'> readonly userId: Prisma.FieldRef<"LicenseKey", 'String'> readonly email: Prisma.FieldRef<"LicenseKey", 'String'> + readonly systemName: Prisma.FieldRef<"LicenseKey", 'String'> + readonly systemNameLower: Prisma.FieldRef<"LicenseKey", 'String'> } From 8412dfb69c1ae7c005c219d0aabb961264cd18d2 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 09:51:54 -0500 Subject: [PATCH 05/25] Portal: System Name PUT endpoint and license-validation status Add PUT /api/v2/licenses/[licenseKey]/system-name (200/400 invalid_name/ 404 unknown_license/409 name_taken, idempotent) and extend POST /api/v2/license with an optional systemName and additive systemNameStatus (ok/unchanged/conflict/invalid). Validation always succeeds; an omitted field never changes or clears a name and empty/whitespace is invalid. Adds server and helper tests covering trimming, bounds, case-insensitive uniqueness, cross-account reuse, 404, and idempotency. --- src/lib/server/system-name.spec.ts | 115 ++++++++++++++++++ src/lib/server/system-name.ts | 73 +++++++++++ src/lib/server/v2-serializers.spec.ts | 8 +- src/lib/server/v2-serializers.ts | 13 +- src/routes/api/v2/license/+server.ts | 22 +++- src/routes/api/v2/license/server.spec.ts | 93 +++++++++++++- .../[licenseKey]/system-name/+server.ts | 40 ++++++ .../[licenseKey]/system-name/server.spec.ts | 108 ++++++++++++++++ 8 files changed, 463 insertions(+), 9 deletions(-) create mode 100644 src/lib/server/system-name.spec.ts create mode 100644 src/lib/server/system-name.ts create mode 100644 src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts create mode 100644 src/routes/api/v2/licenses/[licenseKey]/system-name/server.spec.ts diff --git a/src/lib/server/system-name.spec.ts b/src/lib/server/system-name.spec.ts new file mode 100644 index 0000000..d40cba9 --- /dev/null +++ b/src/lib/server/system-name.spec.ts @@ -0,0 +1,115 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('$lib/db', () => ({ + prisma: { + licenseKey: { findFirst: vi.fn(), update: vi.fn() } + } +})); + +import { prisma } from '$lib/db'; +import { applySystemName, normalizeSystemName, SYSTEM_NAME_MAX_LENGTH } from './system-name'; + +const keyFindFirst = prisma.licenseKey.findFirst as ReturnType; +const keyUpdate = prisma.licenseKey.update as ReturnType; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('normalizeSystemName', () => { + it('trims and returns a case-folded copy', () => { + expect(normalizeSystemName(' Living Room ')).toEqual({ + ok: true, + name: 'Living Room', + lower: 'living room' + }); + }); + + it('accepts the length bounds', () => { + expect(normalizeSystemName('a'.repeat(SYSTEM_NAME_MAX_LENGTH)).ok).toBe(true); + expect(normalizeSystemName('a')).toEqual({ ok: true, name: 'a', lower: 'a' }); + }); + + it('rejects empty, whitespace, and out-of-range values', () => { + expect(normalizeSystemName('')).toEqual({ ok: false }); + expect(normalizeSystemName(' ')).toEqual({ ok: false }); + expect(normalizeSystemName('a'.repeat(SYSTEM_NAME_MAX_LENGTH + 1))).toEqual({ ok: false }); + }); + + it('rejects non-strings', () => { + expect(normalizeSystemName(undefined)).toEqual({ ok: false }); + expect(normalizeSystemName(null)).toEqual({ ok: false }); + expect(normalizeSystemName(42)).toEqual({ ok: false }); + }); +}); + +describe('applySystemName', () => { + it('reports unchanged without writing when identical', async () => { + const status = await applySystemName( + { id: 'k1', userId: 'u1', systemName: 'Living Room' }, + 'Living Room', + 'living room' + ); + + expect(status).toBe('unchanged'); + expect(keyFindFirst).not.toHaveBeenCalled(); + expect(keyUpdate).not.toHaveBeenCalled(); + }); + + it('reports conflict when another license on the account holds the name', async () => { + keyFindFirst.mockResolvedValue({ id: 'other' }); + + const status = await applySystemName( + { id: 'k1', userId: 'u1', systemName: null }, + 'Den', + 'den' + ); + + expect(status).toBe('conflict'); + expect(keyUpdate).not.toHaveBeenCalled(); + }); + + it('writes the name and its case-folded copy when free', async () => { + keyFindFirst.mockResolvedValue(null); + keyUpdate.mockResolvedValue({}); + + const status = await applySystemName( + { id: 'k1', userId: 'u1', systemName: null }, + 'Den', + 'den' + ); + + expect(status).toBe('ok'); + expect(keyUpdate).toHaveBeenCalledWith({ + where: { id: 'k1' }, + data: { systemName: 'Den', systemNameLower: 'den' } + }); + }); + + it('skips the uniqueness check for a license with no account', async () => { + keyUpdate.mockResolvedValue({}); + + const status = await applySystemName( + { id: 'k1', userId: null, systemName: null }, + 'Den', + 'den' + ); + + expect(status).toBe('ok'); + expect(keyFindFirst).not.toHaveBeenCalled(); + expect(keyUpdate).toHaveBeenCalled(); + }); + + it('maps a unique-constraint violation to conflict', async () => { + keyFindFirst.mockResolvedValue(null); + keyUpdate.mockRejectedValue({ code: 'P2002' }); + + const status = await applySystemName( + { id: 'k1', userId: 'u1', systemName: null }, + 'Den', + 'den' + ); + + expect(status).toBe('conflict'); + }); +}); diff --git a/src/lib/server/system-name.ts b/src/lib/server/system-name.ts new file mode 100644 index 0000000..6c45d9e --- /dev/null +++ b/src/lib/server/system-name.ts @@ -0,0 +1,73 @@ +// System Name helpers. A System Name is a human-readable label for a License +// Key, set only by the Kiosk and displayed read-only by the Portal. It is never +// a reference or foreign key. Uniqueness is per account (the owning User) and +// case-insensitive when non-null, enforced by the unique index on +// (userId, systemNameLower). + +import { prisma } from '$lib/db'; + +export const SYSTEM_NAME_MAX_LENGTH = 40; + +export type SystemNameStatus = 'ok' | 'unchanged' | 'conflict' | 'invalid'; + +export type NormalizedName = { ok: true; name: string; lower: string } | { ok: false }; + +/** Trim and validate a caller-supplied System Name. Stored as typed (trimmed); + * `lower` is the case-folded copy used for uniqueness. */ +export function normalizeSystemName(raw: unknown): NormalizedName { + if (typeof raw !== 'string') return { ok: false }; + const name = raw.trim(); + if (name.length < 1 || name.length > SYSTEM_NAME_MAX_LENGTH) return { ok: false }; + return { ok: true, name, lower: name.toLowerCase() }; +} + +type SystemNameLicense = { + id: string; + userId: string | null; + systemName: string | null; +}; + +function isUniqueViolation(error: unknown): boolean { + return ( + typeof error === 'object' && error !== null && (error as { code?: string }).code === 'P2002' + ); +} + +/** + * Apply a normalized name to a license. Returns the resulting status: + * - `unchanged` when the stored name is already identical + * - `conflict` when another license on the same account holds the name + * - `ok` when the name was written + * A license with no owning account (legacy, userId null) is written without a + * uniqueness check; per-account uniqueness cannot apply without an account. + */ +export async function applySystemName( + license: SystemNameLicense, + name: string, + lower: string +): Promise { + if (license.systemName === name) return 'unchanged'; + + if (license.userId) { + const conflict = await prisma.licenseKey.findFirst({ + where: { + userId: license.userId, + systemNameLower: lower, + id: { not: license.id } + }, + select: { id: true } + }); + if (conflict) return 'conflict'; + } + + try { + await prisma.licenseKey.update({ + where: { id: license.id }, + data: { systemName: name, systemNameLower: lower } + }); + return 'ok'; + } catch (error) { + if (isUniqueViolation(error)) return 'conflict'; + throw error; + } +} diff --git a/src/lib/server/v2-serializers.spec.ts b/src/lib/server/v2-serializers.spec.ts index 2e3a102..0b94e3a 100644 --- a/src/lib/server/v2-serializers.spec.ts +++ b/src/lib/server/v2-serializers.spec.ts @@ -139,14 +139,16 @@ describe('serializeGroup', () => { }); describe('serializeLicense', () => { - it('returns only key and email_address (levels removed)', () => { + it('returns key, email_address, and the advisory systemNameStatus', () => { const result = serializeLicense({ key: 'XXXX-XXXX-XXXX-XXXX', - emailAddress: 'user@example.com' + emailAddress: 'user@example.com', + systemNameStatus: 'unchanged' }); expect(result).toEqual({ key: 'XXXX-XXXX-XXXX-XXXX', - email_address: 'user@example.com' + email_address: 'user@example.com', + systemNameStatus: 'unchanged' }); expect(Object.keys(result)).not.toContain('subscription_level'); }); diff --git a/src/lib/server/v2-serializers.ts b/src/lib/server/v2-serializers.ts index 5700851..28276f3 100644 --- a/src/lib/server/v2-serializers.ts +++ b/src/lib/server/v2-serializers.ts @@ -119,13 +119,20 @@ export function serializeGroup(g: { id: number; name: string; sortOrder: number export type V2License = { key: string; email_address: string; + systemNameStatus: string; }; -/** Build the POST /license response. */ -export function serializeLicense(opts: { key: string; emailAddress: string }): V2License { +/** Build the POST /license response. `systemNameStatus` is advisory: the Kiosk + * treats "ok"/"unchanged" as synced and reverts on "conflict"/"invalid". */ +export function serializeLicense(opts: { + key: string; + emailAddress: string; + systemNameStatus: string; +}): V2License { return { key: opts.key, - email_address: opts.emailAddress + email_address: opts.emailAddress, + systemNameStatus: opts.systemNameStatus }; } diff --git a/src/routes/api/v2/license/+server.ts b/src/routes/api/v2/license/+server.ts index b21644f..c7b9acc 100644 --- a/src/routes/api/v2/license/+server.ts +++ b/src/routes/api/v2/license/+server.ts @@ -4,6 +4,11 @@ import { prisma } from '$lib/db'; import { clientIp, jsonOk, rateLimited, readJsonBody } from '$lib/server/v2-helpers'; import { rateLimit } from '$lib/server/v2-rate-limit'; import { serializeLicense } from '$lib/server/v2-serializers'; +import { + applySystemName, + normalizeSystemName, + type SystemNameStatus +} from '$lib/server/system-name'; const LICENSE_KEY_RE = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/; @@ -50,6 +55,20 @@ export const POST: RequestHandler = async ({ request }) => { throw error(400, 'key does not belong to specified user'); } + // Optional System Name (Kiosk is the only writer). Validation always + // succeeds; a non-ok name status is advisory. An omitted field never + // changes or clears the stored name; empty/whitespace is invalid, never a + // clear. + let systemNameStatus: SystemNameStatus = 'unchanged'; + if ('systemName' in body) { + const parsed = normalizeSystemName(body.systemName); + if (!parsed.ok) { + systemNameStatus = 'invalid'; + } else { + systemNameStatus = await applySystemName(storedKey, parsed.name, parsed.lower); + } + } + // Record that this active key was validated. A failure to record the // timestamp should not fail an otherwise valid validation. try { @@ -64,7 +83,8 @@ export const POST: RequestHandler = async ({ request }) => { return jsonOk( serializeLicense({ key: storedKey.key, - emailAddress: email + emailAddress: email, + systemNameStatus }) ); }; diff --git a/src/routes/api/v2/license/server.spec.ts b/src/routes/api/v2/license/server.spec.ts index 3fde419..cf63304 100644 --- a/src/routes/api/v2/license/server.spec.ts +++ b/src/routes/api/v2/license/server.spec.ts @@ -43,7 +43,8 @@ const storedKey = { id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP', active: true, - userId: 'user-1' + userId: 'user-1', + systemName: null }; describe('POST /api/v2/license', () => { @@ -64,7 +65,8 @@ describe('POST /api/v2/license', () => { const body = (await res.json()) as Record; expect(body).toEqual({ key: storedKey.key, - email_address: 'user@example.com' + email_address: 'user@example.com', + systemNameStatus: 'unchanged' }); expect(Object.keys(body)).not.toContain('subscription_level'); expect(mockKeyUpdate).toHaveBeenCalledWith({ @@ -147,3 +149,90 @@ describe('POST /api/v2/license', () => { expect(mockKeyFindFirst).not.toHaveBeenCalled(); }); }); + +describe('POST /api/v2/license systemName handling', () => { + beforeEach(() => { + vi.clearAllMocks(); + _clearRateLimits(); + mockKeyFindFirst.mockResolvedValue(storedKey); + mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' }); + mockKeyUpdate.mockResolvedValue({}); + }); + + async function post(...args: unknown[]) { + const body: Record = { + license_key: storedKey.key, + email_address: 'user@example.com' + }; + if (args.length > 0) body.systemName = args[0]; + const res = await POST({ request: licenseRequest(body) } as POSTEvent); + return res; + } + + it('leaves the name untouched and reports unchanged when omitted', async () => { + const res = await post(); + + expect((await res.json()).systemNameStatus).toBe('unchanged'); + expect(mockKeyUpdate).toHaveBeenCalledTimes(1); + expect(mockKeyUpdate).toHaveBeenCalledWith({ + where: { id: storedKey.id }, + data: { lastValidatedAt: expect.any(Date) } + }); + }); + + it('sets a trimmed name, case-folded for uniqueness (ok)', async () => { + mockKeyFindFirst.mockResolvedValueOnce(storedKey).mockResolvedValueOnce(null); + + const res = await post(' Living Room '); + + expect((await res.json()).systemNameStatus).toBe('ok'); + expect(mockKeyUpdate).toHaveBeenCalledWith({ + where: { id: storedKey.id }, + data: { systemName: 'Living Room', systemNameLower: 'living room' } + }); + }); + + it('reports conflict without writing when another key holds the name', async () => { + mockKeyFindFirst.mockResolvedValueOnce(storedKey).mockResolvedValueOnce({ id: 'other' }); + + const res = await post('Living Room'); + + expect((await res.json()).systemNameStatus).toBe('conflict'); + expect(mockKeyUpdate).toHaveBeenCalledTimes(1); + }); + + it('treats differing casing as a conflict', async () => { + mockKeyFindFirst.mockResolvedValueOnce(storedKey).mockResolvedValueOnce({ id: 'other' }); + + const res = await post('living room'); + + expect((await res.json()).systemNameStatus).toBe('conflict'); + }); + + it('allows the same name in a different account', async () => { + mockKeyFindFirst.mockResolvedValueOnce(storedKey).mockResolvedValueOnce(null); + + const res = await post('Living Room'); + + expect((await res.json()).systemNameStatus).toBe('ok'); + }); + + it('reports invalid without writing for empty, whitespace, too long, or non-string', async () => { + for (const bad of ['', ' ', 'a'.repeat(41), 42]) { + const res = await post(bad); + expect((await res.json()).systemNameStatus).toBe('invalid'); + } + // Only the lastValidatedAt stamp is written; no name writes. + for (const call of mockKeyUpdate.mock.calls) { + expect(call[0].data).toEqual({ lastValidatedAt: expect.any(Date) }); + } + }); + + it('validation still succeeds (200) on conflict', async () => { + mockKeyFindFirst.mockResolvedValueOnce(storedKey).mockResolvedValueOnce({ id: 'other' }); + + const res = await post('Living Room'); + + expect(res.status).toBe(200); + }); +}); diff --git a/src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts b/src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts new file mode 100644 index 0000000..f6cdc46 --- /dev/null +++ b/src/routes/api/v2/licenses/[licenseKey]/system-name/+server.ts @@ -0,0 +1,40 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { prisma } from '$lib/db'; +import { clientIp, jsonOk, rateLimited, readJsonBody } from '$lib/server/v2-helpers'; +import { rateLimit } from '$lib/server/v2-rate-limit'; +import { applySystemName, normalizeSystemName } from '$lib/server/system-name'; + +/** + * Kiosk-facing. Sets the System Name for a License Key. The Kiosk is the only + * writer; the Portal never calls this. Idempotent: re-sending the current name + * returns 200 without a write. Responds 400 `invalid_name`, 404 + * `unknown_license`, or 409 `name_taken`. + */ +export const PUT: RequestHandler = async ({ request, params }) => { + const ip = clientIp(request); + const rl = rateLimit({ key: `system-name:${ip}`, limit: 10, windowMs: 60_000 }); + if (!rl.ok) { + return rateLimited(rl.retryAfterMs); + } + + const licenseKey = (params.licenseKey ?? '').trim(); + const body = (await readJsonBody(request)) as Record; + + const parsed = normalizeSystemName(body.systemName); + if (!parsed.ok) { + throw error(400, 'invalid_name'); + } + + const license = await prisma.licenseKey.findUnique({ where: { key: licenseKey } }); + if (!license) { + throw error(404, 'unknown_license'); + } + + const status = await applySystemName(license, parsed.name, parsed.lower); + if (status === 'conflict') { + throw error(409, 'name_taken'); + } + + return jsonOk({ systemName: parsed.name, systemNameStatus: 'ok' }); +}; diff --git a/src/routes/api/v2/licenses/[licenseKey]/system-name/server.spec.ts b/src/routes/api/v2/licenses/[licenseKey]/system-name/server.spec.ts new file mode 100644 index 0000000..d2949fe --- /dev/null +++ b/src/routes/api/v2/licenses/[licenseKey]/system-name/server.spec.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { isHttpError } from '@sveltejs/kit'; +import { _clearRateLimits } from '$lib/server/v2-rate-limit'; + +vi.mock('$lib/db', () => ({ + prisma: { + licenseKey: { findUnique: vi.fn(), findFirst: vi.fn(), update: vi.fn() } + } +})); + +import { prisma } from '$lib/db'; +import { PUT } from './+server'; + +type PUTEvent = Parameters[0]; + +const mockFindUnique = prisma.licenseKey.findUnique as ReturnType; +const mockFindFirst = prisma.licenseKey.findFirst as ReturnType; +const mockUpdate = prisma.licenseKey.update as ReturnType; + +const KEY = 'ABCD-EFGH-IJKL-MNOP'; + +const storedKey = { + id: 'key-1', + key: KEY, + active: true, + userId: 'user-1', + systemName: null as string | null +}; + +function putRequest(body: unknown): Request { + return new Request(`http://localhost/api/v2/licenses/${KEY}/system-name`, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }); +} + +function event(body: unknown): PUTEvent { + return { request: putRequest(body), params: { licenseKey: KEY } } as PUTEvent; +} + +async function expectHttpError(promise: unknown, status: number, message: string) { + try { + await promise; + } catch (e) { + expect(isHttpError(e)).toBe(true); + const err = e as { status: number; body: { message: string } }; + expect(err.status).toBe(status); + expect(err.body.message).toBe(message); + return; + } + throw new Error('expected handler to throw an HttpError'); +} + +beforeEach(() => { + vi.clearAllMocks(); + _clearRateLimits(); +}); + +describe('PUT /api/v2/licenses/[licenseKey]/system-name', () => { + it('sets the name and returns it', async () => { + mockFindUnique.mockResolvedValue(storedKey); + mockFindFirst.mockResolvedValue(null); + mockUpdate.mockResolvedValue({}); + + const res = await PUT(event({ systemName: ' Living Room ' })); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ systemName: 'Living Room', systemNameStatus: 'ok' }); + expect(mockUpdate).toHaveBeenCalledWith({ + where: { id: 'key-1' }, + data: { systemName: 'Living Room', systemNameLower: 'living room' } + }); + }); + + it('is idempotent when the name is unchanged', async () => { + mockFindUnique.mockResolvedValue({ ...storedKey, systemName: 'Living Room' }); + + const res = await PUT(event({ systemName: 'Living Room' })); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ systemName: 'Living Room', systemNameStatus: 'ok' }); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('rejects an invalid name', async () => { + await expectHttpError(PUT(event({ systemName: ' ' })), 400, 'invalid_name'); + await expectHttpError(PUT(event({ systemName: '' })), 400, 'invalid_name'); + await expectHttpError(PUT(event({ systemName: 'a'.repeat(41) })), 400, 'invalid_name'); + await expectHttpError(PUT(event({})), 400, 'invalid_name'); + expect(mockFindUnique).not.toHaveBeenCalled(); + }); + + it('returns 404 for an unknown license', async () => { + mockFindUnique.mockResolvedValue(null); + + await expectHttpError(PUT(event({ systemName: 'Den' })), 404, 'unknown_license'); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('returns 409 when another license on the account holds the name', async () => { + mockFindUnique.mockResolvedValue(storedKey); + mockFindFirst.mockResolvedValue({ id: 'other' }); + + await expectHttpError(PUT(event({ systemName: 'Den' })), 409, 'name_taken'); + expect(mockUpdate).not.toHaveBeenCalled(); + }); +}); From 3c49f425d809533fdf822eb4c107591b70d525fa Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 09:51:58 -0500 Subject: [PATCH 06/25] Portal: display System Name read-only on dashboard and admin lists Show the System Name where license keys are listed, falling back to an 'Unnamed system' indication when unset. No rename UI or mutation path. --- src/routes/(protected)/(user)/dashboard/+page.svelte | 11 +++++++++++ .../(protected)/admin/license-keys/+page.svelte | 1 + 2 files changed, 12 insertions(+) diff --git a/src/routes/(protected)/(user)/dashboard/+page.svelte b/src/routes/(protected)/(user)/dashboard/+page.svelte index f0b0166..83e33cc 100644 --- a/src/routes/(protected)/(user)/dashboard/+page.svelte +++ b/src/routes/(protected)/(user)/dashboard/+page.svelte @@ -44,6 +44,7 @@ class="border-b border-slate-200 bg-slate-50 text-xs tracking-wide text-slate-500 uppercase dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400" > + System Name Key Status Created @@ -53,6 +54,13 @@ {#each data.licenseKeys as key (key.key)} + + {#if key.systemName} + {key.systemName} + {:else} + Unnamed system + {/if} + {key.key} {#if key.active} @@ -104,6 +112,9 @@ {/if}
+ + {key.systemName ?? 'Unnamed system'} + Created {formatDate(key.createdAt)} diff --git a/src/routes/(protected)/admin/license-keys/+page.svelte b/src/routes/(protected)/admin/license-keys/+page.svelte index e79d2b1..3c09763 100644 --- a/src/routes/(protected)/admin/license-keys/+page.svelte +++ b/src/routes/(protected)/admin/license-keys/+page.svelte @@ -14,6 +14,7 @@ const columns: DataTableColumn[] = [ { label: 'Key', value: (r) => r.key }, + { label: 'System Name', value: (r) => (r.systemName as string | null) ?? 'Unnamed system' }, { label: 'User', value: (r) => (r.user as Record | undefined)?.name ?? '' From 25b5eeb14477a6b66a112bff386f5967af6c65a0 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 09:59:17 -0500 Subject: [PATCH 07/25] docs: document System Name PUT endpoint and validation fields in OpenAPI --- docs/openapi.yaml | 87 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3afea31..a7a7a2a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -309,6 +309,78 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + /api/v2/licenses/{licenseKey}/system-name: + put: + tags: [License] + summary: Set a License Key's System Name (Kiosk-only) + description: | + Sets the human-readable System Name for a License Key. The Kiosk is the + only writer; the Portal displays the name read-only. The name is unique + per account, compared case-insensitively, when non-null. Idempotent: + re-sending the current name returns 200 without a write. + operationId: setSystemName + parameters: + - in: path + name: licenseKey + required: true + schema: + type: string + pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [systemName] + properties: + systemName: + type: string + minLength: 1 + maxLength: 40 + responses: + '200': + description: The stored name. + content: + application/json: + schema: + type: object + required: [systemName, systemNameStatus] + properties: + systemName: + type: string + systemNameStatus: + type: string + enum: [ok] + '400': + description: 'Invalid name (`invalid_name`); empty/whitespace is invalid.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '404': + description: 'Unknown license (`unknown_license`).' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '409': + description: 'Name already used on the account (`name_taken`).' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '429': + description: Rate limit exceeded (10 requests per minute per IP). + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before retrying. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' /api/v2/automations/create/account: post: tags: [Automations] @@ -505,9 +577,16 @@ components: type: string format: email maxLength: 254 + systemName: + type: string + minLength: 1 + maxLength: 40 + description: | + Optional. The Kiosk's System Name (trimmed). Omitted means the stored + name is left untouched; empty/whitespace is invalid, never a clear. LicenseResponse: type: object - required: [key, email_address] + required: [key, email_address, systemNameStatus] properties: key: type: string @@ -515,6 +594,12 @@ components: email_address: type: string format: email + systemNameStatus: + type: string + enum: [ok, unchanged, conflict, invalid] + description: | + Advisory outcome of the optional `systemName`. Validation itself + always succeeds. `unchanged` is returned when the field is omitted. ScheduleBlock: type: object required: From 138bf03ea9f432382a9e16ddbf70fbec1d8596bf Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:11:21 -0500 Subject: [PATCH 08/25] Portal: move Schedule to per-License-Key with daily/weekly and Block.day Adds Schedule.licenseKeyId (unique, nullable) and Schedule.type, makes the legacy userId optional/non-unique, adds Block.day and flips Block dynamicAudio default to true. The migration backfills each existing per-user schedule onto that account's oldest License Key as a daily schedule, preserves unmappable schedules (licenseKeyId NULL) with a NOTICE, and is reversible. Tested up/down against a scratch schema. --- .../migration.sql | 46 +++ prisma/schema.prisma | 44 ++- src/lib/generated/prisma/internal/class.ts | 8 +- .../prisma/internal/prismaNamespace.ts | 3 + .../prisma/internal/prismaNamespaceBrowser.ts | 3 + src/lib/generated/prisma/models/Block.ts | 41 +- src/lib/generated/prisma/models/LicenseKey.ts | 119 ++++++ src/lib/generated/prisma/models/Schedule.ts | 368 +++++++++++++++--- src/lib/generated/prisma/models/User.ts | 128 +++--- 9 files changed, 630 insertions(+), 130 deletions(-) create mode 100644 prisma/migrations/20260929130000_move_schedule_to_license_key/migration.sql diff --git a/prisma/migrations/20260929130000_move_schedule_to_license_key/migration.sql b/prisma/migrations/20260929130000_move_schedule_to_license_key/migration.sql new file mode 100644 index 0000000..c6338c1 --- /dev/null +++ b/prisma/migrations/20260929130000_move_schedule_to_license_key/migration.sql @@ -0,0 +1,46 @@ +-- Phase 3: move Schedule from per-user to per-License-Key, add weekly support. + +-- AlterTable: Schedule gains licenseKeyId + type, userId becomes optional. +ALTER TABLE "Schedule" ADD COLUMN "licenseKeyId" TEXT, +ADD COLUMN "type" TEXT; + +-- AlterTable: Block gains day; new Blocks default dynamicAudio true. +ALTER TABLE "Block" ADD COLUMN "day" TEXT; +ALTER TABLE "Block" ALTER COLUMN "dynamicAudio" SET DEFAULT true; + +-- DropIndex: userId is no longer the schedule identity. +DROP INDEX "Schedule_userId_key"; + +-- AlterTable: legacy owner becomes optional and non-unique. +ALTER TABLE "Schedule" ALTER COLUMN "userId" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Schedule_licenseKeyId_key" ON "Schedule"("licenseKeyId"); +CREATE INDEX "Schedule_userId_idx" ON "Schedule"("userId"); + +-- AddForeignKey +ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_licenseKeyId_fkey" FOREIGN KEY ("licenseKeyId") REFERENCES "LicenseKey"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Backfill: map each existing per-user Schedule to the account's oldest +-- License Key. Existing content was daily-only. +UPDATE "Schedule" s +SET "licenseKeyId" = lk."id", + "type" = COALESCE(s."type", 'daily') +FROM ( + SELECT DISTINCT ON ("userId") "id", "userId" + FROM "LicenseKey" + WHERE "userId" IS NOT NULL + ORDER BY "userId", "createdAt" ASC, "id" ASC +) lk +WHERE s."userId" = lk."userId" + AND s."licenseKeyId" IS NULL; + +-- Report (do not silently drop) schedules that cannot be mapped to a key. +DO $$ +DECLARE unmapped integer; +BEGIN + SELECT count(*) INTO unmapped FROM "Schedule" WHERE "licenseKeyId" IS NULL; + IF unmapped > 0 THEN + RAISE NOTICE 'Phase 3 migration: % Schedule row(s) could not be mapped to a License Key (no owning user/license); retained with licenseKeyId NULL.', unmapped; + END IF; +END $$; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 61f7185..bc70a88 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,7 +28,7 @@ model User { profiles Profile[] licenseKeys LicenseKey[] userProclamations UserProclamations[] - schedule Schedule? + schedules Schedule[] @@unique([email]) @@map("user") @@ -205,6 +205,7 @@ model LicenseKey { userId String? user User? @relation(fields: [userId], references: [id], onDelete: SetNull) email String? + schedule Schedule? // Human-readable label set only by the Kiosk (e.g. "Living Room"). Trimmed, // 1-40 chars. Never a reference or foreign key; the License Key is the only // identifier. `systemNameLower` is a case-folded copy used to enforce @@ -263,31 +264,42 @@ model AuditLog { @@map("AuditLog") } -// A user's daily listening schedule. Exactly one per user (enforced by the -// unique userId); contains 0-4 time Blocks. Times are wall-clock minutes from -// midnight (0-1440), interpreted by the kiosk in the user's configured -// timezone. The portal never stores a timezone. +// A listening schedule assigned to one License Key (at most one per key). +// `type` is "daily" | "weekly" | null (null = no schedule). Times are +// wall-clock minutes from midnight (0-1440), interpreted by the kiosk in its +// own configured timezone. The portal never stores a timezone. +// +// `userId` is a legacy owner retained while per-user schedules are migrated to +// per-license-key; new schedules set `licenseKeyId` and leave `userId` null. model Schedule { - id String @id @default(uuid()) - userId String @unique - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - blocks Block[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + licenseKeyId String? @unique + licenseKey LicenseKey? @relation(fields: [licenseKeyId], references: [id], onDelete: Cascade) + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + type String? + blocks Block[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) } -// A single non-overlapping time range within a Schedule, plus the ordered set -// of selected Proclamations to play during it. Overnight ranges are disallowed -// in v1, so startMinutes is always less than endMinutes. +// A single time range within a Schedule, plus the ordered set of selected +// Proclamations to play during it. `day` is null for daily schedules and +// "mon".."sun" for weekly schedules. Overnight (midnight-crossing) ranges are +// valid: an end clock time before the start belongs to the start day and spills +// into the next day. model Block { id String @id @default(uuid()) scheduleId String schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade) + day String? startMinutes Int endMinutes Int // When true, the kiosk enables dynamic audio for the duration of this block, - // overriding any audio selections made on the device. - dynamicAudio Boolean @default(false) + // overriding any audio selections made on the device. Portal default: true. + dynamicAudio Boolean @default(true) proclamations BlockProclamations[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/lib/generated/prisma/internal/class.ts b/src/lib/generated/prisma/internal/class.ts index 34c269c..b96cda1 100644 --- a/src/lib/generated/prisma/internal/class.ts +++ b/src/lib/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.8.0", "engineVersion": "3c6e192761c0362d496ed980de936e2f3cebcd3a", "activeProvider": "postgresql", - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/lib/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n roleId Int? @default(4) // New users default to \"User\"\n role UserRole? @relation(fields: [roleId], references: [id])\n sessions Session[]\n accounts Account[]\n profiles Profile[]\n licenseKeys LicenseKey[]\n userProclamations UserProclamations[]\n schedule Schedule?\n\n @@unique([email])\n @@map(\"user\")\n}\n\n// I chose to create a Profile table here because honestly, I didn't want to waste\n// a bunch of time trying to figure out how to customize the default Better-Auth model. \nmodel Profile {\n id String @id @default(uuid())\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n email String?\n firstName String\n lastName String\n phoneNumber String\n ghlContactId String? @map(\"ghl_contact_id\")\n addresses Address[]\n\n @@index([userId])\n @@index([email])\n}\n\nmodel Address {\n id String @id @default(uuid())\n profileId String\n profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n street String\n city String\n state String\n postalCode String\n country String\n type String @default(\"Mailing\")\n isDefaultMailing Boolean @default(false)\n isDefaultBilling Boolean @default(false)\n isDefaultShipping Boolean @default(false)\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel UserRole {\n id Int @id @default(autoincrement())\n name String\n users User[]\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Proclamation {\n id String @id @default(uuid())\n name String @unique\n content String\n solfeggioFrequencyId Int\n solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])\n categoryId Int\n category ProclamationCategory @relation(fields: [categoryId], references: [id])\n groupId Int\n group ProclamationGroup @relation(fields: [groupId], references: [id])\n userProclamations UserProclamations[]\n blockProclamations BlockProclamations[]\n // Legacy subscription-level FK. Retained in the database but unmanaged; the\n // FK constraint is dropped and the value is left NULL. Hidden from the client.\n subscriptionLevelId Int? @ignore\n}\n\nmodel ProclamationGroup {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\nmodel UserProclamations {\n userId String\n user User @relation(fields: [userId], references: [id])\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([userId, proclamationId])\n}\n\nmodel SolfeggioFrequency {\n id Int @id @default(autoincrement())\n name String @unique\n value Int @unique\n proclamations Proclamation[]\n}\n\nmodel ProclamationCategory {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\n// Archive of the former child sub-categories removed when the\n// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.\n// `@@ignore` keeps it out of the generated client while Prisma remains aware\n// of the table so `migrate dev` does not report it as an untracked table.\nmodel ProclamationCategoryLegacy {\n id Int @id\n name String\n parentId Int?\n sortOrder Int @default(0)\n\n @@map(\"ProclamationCategoryLegacy\")\n @@ignore\n}\n\n// Archive of the category->subscription-level mappings for the child\n// categories removed when the category hierarchy was flattened. Kept with\n// `@@ignore` so Prisma remains aware of the table but it is excluded from the\n// generated client and Prisma's own migrations.\nmodel ProclamationCategorySubscriptionLevelLegacy {\n categoryId Int\n subscriptionLevelId Int\n\n @@id([categoryId, subscriptionLevelId])\n @@map(\"ProclamationCategorySubscriptionLevelLegacy\")\n @@ignore\n}\n\nmodel LicenseKey {\n id String @id @default(uuid())\n key String @unique\n active Boolean\n createdAt DateTime @default(now())\n lastValidatedAt DateTime?\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: SetNull)\n email String?\n // Human-readable label set only by the Kiosk (e.g. \"Living Room\"). Trimmed,\n // 1-40 chars. Never a reference or foreign key; the License Key is the only\n // identifier. `systemNameLower` is a case-folded copy used to enforce\n // per-account, case-insensitive uniqueness when non-null.\n systemName String?\n systemNameLower String?\n // Legacy subscription level. Retained in the database but unmanaged; hidden\n // from the client and left NULL.\n level String? @ignore\n\n @@unique([userId, systemNameLower])\n @@index([userId])\n @@index([email])\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n keyPrefix String\n keyHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime?\n revokedAt DateTime?\n}\n\n// Portal-editable 7-color palette per canonical solfeggio frequency id (1-9).\n// `solfeggioFrequencyId` is the canonical id the kiosk uses (see\n// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.\nmodel ColorPalette {\n solfeggioFrequencyId Int @id\n colors Json // [\"#RRGGBB\", ... x7]\n\n @@map(\"ColorPalette\")\n}\n\n// Append-only audit trail of database mutations. Written centrally by the\n// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.\nmodel AuditLog {\n id String @id @default(uuid())\n createdAt DateTime @default(now())\n userId String?\n email String?\n roleId Int?\n roleName String?\n page String\n action String\n entity String?\n entityId String?\n before Json?\n after Json?\n ip String?\n\n @@index([createdAt])\n @@index([roleId])\n @@index([action])\n @@index([page])\n @@map(\"AuditLog\")\n}\n\n// A user's daily listening schedule. Exactly one per user (enforced by the\n// unique userId); contains 0-4 time Blocks. Times are wall-clock minutes from\n// midnight (0-1440), interpreted by the kiosk in the user's configured\n// timezone. The portal never stores a timezone.\nmodel Schedule {\n id String @id @default(uuid())\n userId String @unique\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n blocks Block[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\n// A single non-overlapping time range within a Schedule, plus the ordered set\n// of selected Proclamations to play during it. Overnight ranges are disallowed\n// in v1, so startMinutes is always less than endMinutes.\nmodel Block {\n id String @id @default(uuid())\n scheduleId String\n schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)\n startMinutes Int\n endMinutes Int\n // When true, the kiosk enables dynamic audio for the duration of this block,\n // overriding any audio selections made on the device.\n dynamicAudio Boolean @default(false)\n proclamations BlockProclamations[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([scheduleId])\n}\n\n// Join table: the Proclamations selected for a Block. Carries the Proclamation\n// content for playback via the API; the portal UI only ever reads\n// title/category/group.\nmodel BlockProclamations {\n blockId String\n block Block @relation(fields: [blockId], references: [id], onDelete: Cascade)\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([blockId, proclamationId])\n @@index([proclamationId])\n}\n", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/lib/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n roleId Int? @default(4) // New users default to \"User\"\n role UserRole? @relation(fields: [roleId], references: [id])\n sessions Session[]\n accounts Account[]\n profiles Profile[]\n licenseKeys LicenseKey[]\n userProclamations UserProclamations[]\n schedules Schedule[]\n\n @@unique([email])\n @@map(\"user\")\n}\n\n// I chose to create a Profile table here because honestly, I didn't want to waste\n// a bunch of time trying to figure out how to customize the default Better-Auth model. \nmodel Profile {\n id String @id @default(uuid())\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n email String?\n firstName String\n lastName String\n phoneNumber String\n ghlContactId String? @map(\"ghl_contact_id\")\n addresses Address[]\n\n @@index([userId])\n @@index([email])\n}\n\nmodel Address {\n id String @id @default(uuid())\n profileId String\n profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n street String\n city String\n state String\n postalCode String\n country String\n type String @default(\"Mailing\")\n isDefaultMailing Boolean @default(false)\n isDefaultBilling Boolean @default(false)\n isDefaultShipping Boolean @default(false)\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel UserRole {\n id Int @id @default(autoincrement())\n name String\n users User[]\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel Proclamation {\n id String @id @default(uuid())\n name String @unique\n content String\n solfeggioFrequencyId Int\n solfeggioFrequency SolfeggioFrequency @relation(fields: [solfeggioFrequencyId], references: [id])\n categoryId Int\n category ProclamationCategory @relation(fields: [categoryId], references: [id])\n groupId Int\n group ProclamationGroup @relation(fields: [groupId], references: [id])\n userProclamations UserProclamations[]\n blockProclamations BlockProclamations[]\n // Legacy subscription-level FK. Retained in the database but unmanaged; the\n // FK constraint is dropped and the value is left NULL. Hidden from the client.\n subscriptionLevelId Int? @ignore\n}\n\nmodel ProclamationGroup {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\nmodel UserProclamations {\n userId String\n user User @relation(fields: [userId], references: [id])\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([userId, proclamationId])\n}\n\nmodel SolfeggioFrequency {\n id Int @id @default(autoincrement())\n name String @unique\n value Int @unique\n proclamations Proclamation[]\n}\n\nmodel ProclamationCategory {\n id Int @id @default(autoincrement())\n name String @unique\n sortOrder Int @default(0)\n proclamations Proclamation[]\n}\n\n// Archive of the former child sub-categories removed when the\n// ProclamationCategory hierarchy was flattened. Kept for recovery/audit.\n// `@@ignore` keeps it out of the generated client while Prisma remains aware\n// of the table so `migrate dev` does not report it as an untracked table.\nmodel ProclamationCategoryLegacy {\n id Int @id\n name String\n parentId Int?\n sortOrder Int @default(0)\n\n @@map(\"ProclamationCategoryLegacy\")\n @@ignore\n}\n\n// Archive of the category->subscription-level mappings for the child\n// categories removed when the category hierarchy was flattened. Kept with\n// `@@ignore` so Prisma remains aware of the table but it is excluded from the\n// generated client and Prisma's own migrations.\nmodel ProclamationCategorySubscriptionLevelLegacy {\n categoryId Int\n subscriptionLevelId Int\n\n @@id([categoryId, subscriptionLevelId])\n @@map(\"ProclamationCategorySubscriptionLevelLegacy\")\n @@ignore\n}\n\nmodel LicenseKey {\n id String @id @default(uuid())\n key String @unique\n active Boolean\n createdAt DateTime @default(now())\n lastValidatedAt DateTime?\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: SetNull)\n email String?\n schedule Schedule?\n // Human-readable label set only by the Kiosk (e.g. \"Living Room\"). Trimmed,\n // 1-40 chars. Never a reference or foreign key; the License Key is the only\n // identifier. `systemNameLower` is a case-folded copy used to enforce\n // per-account, case-insensitive uniqueness when non-null.\n systemName String?\n systemNameLower String?\n // Legacy subscription level. Retained in the database but unmanaged; hidden\n // from the client and left NULL.\n level String? @ignore\n\n @@unique([userId, systemNameLower])\n @@index([userId])\n @@index([email])\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n keyPrefix String\n keyHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime?\n revokedAt DateTime?\n}\n\n// Portal-editable 7-color palette per canonical solfeggio frequency id (1-9).\n// `solfeggioFrequencyId` is the canonical id the kiosk uses (see\n// v2-serializers.ts), not the DB autoincrement id of `SolfeggioFrequency`.\nmodel ColorPalette {\n solfeggioFrequencyId Int @id\n colors Json // [\"#RRGGBB\", ... x7]\n\n @@map(\"ColorPalette\")\n}\n\n// Append-only audit trail of database mutations. Written centrally by the\n// Prisma extension in src/lib/db.ts; read by the admin Audit Log page.\nmodel AuditLog {\n id String @id @default(uuid())\n createdAt DateTime @default(now())\n userId String?\n email String?\n roleId Int?\n roleName String?\n page String\n action String\n entity String?\n entityId String?\n before Json?\n after Json?\n ip String?\n\n @@index([createdAt])\n @@index([roleId])\n @@index([action])\n @@index([page])\n @@map(\"AuditLog\")\n}\n\n// A listening schedule assigned to one License Key (at most one per key).\n// `type` is \"daily\" | \"weekly\" | null (null = no schedule). Times are\n// wall-clock minutes from midnight (0-1440), interpreted by the kiosk in its\n// own configured timezone. The portal never stores a timezone.\n//\n// `userId` is a legacy owner retained while per-user schedules are migrated to\n// per-license-key; new schedules set `licenseKeyId` and leave `userId` null.\nmodel Schedule {\n id String @id @default(uuid())\n licenseKeyId String? @unique\n licenseKey LicenseKey? @relation(fields: [licenseKeyId], references: [id], onDelete: Cascade)\n userId String?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n type String?\n blocks Block[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n}\n\n// A single time range within a Schedule, plus the ordered set of selected\n// Proclamations to play during it. `day` is null for daily schedules and\n// \"mon\"..\"sun\" for weekly schedules. Overnight (midnight-crossing) ranges are\n// valid: an end clock time before the start belongs to the start day and spills\n// into the next day.\nmodel Block {\n id String @id @default(uuid())\n scheduleId String\n schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)\n day String?\n startMinutes Int\n endMinutes Int\n // When true, the kiosk enables dynamic audio for the duration of this block,\n // overriding any audio selections made on the device. Portal default: true.\n dynamicAudio Boolean @default(true)\n proclamations BlockProclamations[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([scheduleId])\n}\n\n// Join table: the Proclamations selected for a Block. Carries the Proclamation\n// content for playback via the API; the portal UI only ever reads\n// title/category/group.\nmodel BlockProclamations {\n blockId String\n block Block @relation(fields: [blockId], references: [id], onDelete: Cascade)\n proclamationId String\n proclamation Proclamation @relation(fields: [proclamationId], references: [id])\n sortOrder Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@id([blockId, proclamationId])\n @@index([proclamationId])\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -32,10 +32,10 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"role\",\"kind\":\"object\",\"type\":\"UserRole\",\"relationName\":\"UserToUserRole\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"ProfileToUser\"},{\"name\":\"licenseKeys\",\"kind\":\"object\",\"type\":\"LicenseKey\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"ScheduleToUser\"}],\"dbName\":\"user\"},\"Profile\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProfileToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"phoneNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ghlContactId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"ghl_contact_id\"},{\"name\":\"addresses\",\"kind\":\"object\",\"type\":\"Address\",\"relationName\":\"AddressToProfile\"}],\"dbName\":null},\"Address\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profileId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profile\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AddressToProfile\"},{\"name\":\"street\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"city\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"postalCode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"country\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDefaultMailing\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultBilling\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultShipping\",\"kind\":\"scalar\",\"type\":\"Boolean\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"UserRole\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserRole\"}],\"dbName\":null},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Proclamation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"solfeggioFrequency\",\"kind\":\"object\",\"type\":\"SolfeggioFrequency\",\"relationName\":\"ProclamationToSolfeggioFrequency\"},{\"name\":\"categoryId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"category\",\"kind\":\"object\",\"type\":\"ProclamationCategory\",\"relationName\":\"ProclamationToProclamationCategory\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"ProclamationGroup\",\"relationName\":\"ProclamationToProclamationGroup\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"blockProclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockProclamationsToProclamation\"}],\"dbName\":null},\"ProclamationGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationGroup\"}],\"dbName\":null},\"UserProclamations\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SolfeggioFrequency\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToSolfeggioFrequency\"}],\"dbName\":null},\"ProclamationCategory\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationCategory\"}],\"dbName\":null},\"LicenseKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastValidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"systemName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"systemNameLower\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyPrefix\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ColorPalette\":{\"fields\":[{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":\"ColorPalette\"},\"AuditLog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"roleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"page\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entity\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"before\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"after\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"ip\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"AuditLog\"},\"Schedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ScheduleToUser\"},{\"name\":\"blocks\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Block\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"scheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"startMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"endMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"dynamicAudio\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"BlockProclamations\":{\"fields\":[{\"name\":\"blockId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"block\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"BlockProclamationsToProclamation\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"role\",\"kind\":\"object\",\"type\":\"UserRole\",\"relationName\":\"UserToUserRole\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"ProfileToUser\"},{\"name\":\"licenseKeys\",\"kind\":\"object\",\"type\":\"LicenseKey\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"schedules\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"ScheduleToUser\"}],\"dbName\":\"user\"},\"Profile\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProfileToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"phoneNumber\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ghlContactId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"ghl_contact_id\"},{\"name\":\"addresses\",\"kind\":\"object\",\"type\":\"Address\",\"relationName\":\"AddressToProfile\"}],\"dbName\":null},\"Address\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profileId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profile\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AddressToProfile\"},{\"name\":\"street\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"city\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"postalCode\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"country\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDefaultMailing\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultBilling\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"isDefaultShipping\",\"kind\":\"scalar\",\"type\":\"Boolean\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"UserRole\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"users\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserRole\"}],\"dbName\":null},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"Proclamation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"solfeggioFrequency\",\"kind\":\"object\",\"type\":\"SolfeggioFrequency\",\"relationName\":\"ProclamationToSolfeggioFrequency\"},{\"name\":\"categoryId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"category\",\"kind\":\"object\",\"type\":\"ProclamationCategory\",\"relationName\":\"ProclamationToProclamationCategory\"},{\"name\":\"groupId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"group\",\"kind\":\"object\",\"type\":\"ProclamationGroup\",\"relationName\":\"ProclamationToProclamationGroup\"},{\"name\":\"userProclamations\",\"kind\":\"object\",\"type\":\"UserProclamations\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"blockProclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockProclamationsToProclamation\"}],\"dbName\":null},\"ProclamationGroup\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationGroup\"}],\"dbName\":null},\"UserProclamations\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToUserProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SolfeggioFrequency\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToSolfeggioFrequency\"}],\"dbName\":null},\"ProclamationCategory\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"ProclamationToProclamationCategory\"}],\"dbName\":null},\"LicenseKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"lastValidatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"LicenseKeyToUser\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"LicenseKeyToSchedule\"},{\"name\":\"systemName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"systemNameLower\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyPrefix\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"keyHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"revokedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ColorPalette\":{\"fields\":[{\"name\":\"solfeggioFrequencyId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":\"ColorPalette\"},\"AuditLog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roleId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"roleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"page\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"action\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entity\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"before\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"after\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"ip\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":\"AuditLog\"},\"Schedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"licenseKeyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"licenseKey\",\"kind\":\"object\",\"type\":\"LicenseKey\",\"relationName\":\"LicenseKeyToSchedule\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ScheduleToUser\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"blocks\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Block\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"scheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"schedule\",\"kind\":\"object\",\"type\":\"Schedule\",\"relationName\":\"BlockToSchedule\"},{\"name\":\"day\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"startMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"endMinutes\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"dynamicAudio\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"proclamations\",\"kind\":\"object\",\"type\":\"BlockProclamations\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"BlockProclamations\":{\"fields\":[{\"name\":\"blockId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"block\",\"kind\":\"object\",\"type\":\"Block\",\"relationName\":\"BlockToBlockProclamations\"},{\"name\":\"proclamationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"proclamation\",\"kind\":\"object\",\"type\":\"Proclamation\",\"relationName\":\"BlockProclamationsToProclamation\"},{\"name\":\"sortOrder\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") config.parameterizationSchema = { - strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"users\",\"_count\",\"role\",\"user\",\"sessions\",\"accounts\",\"profile\",\"addresses\",\"profiles\",\"licenseKeys\",\"proclamations\",\"solfeggioFrequency\",\"category\",\"group\",\"userProclamations\",\"blocks\",\"schedule\",\"block\",\"proclamation\",\"blockProclamations\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Profile.findUnique\",\"Profile.findUniqueOrThrow\",\"Profile.findFirst\",\"Profile.findFirstOrThrow\",\"Profile.findMany\",\"Profile.createOne\",\"Profile.createMany\",\"Profile.createManyAndReturn\",\"Profile.updateOne\",\"Profile.updateMany\",\"Profile.updateManyAndReturn\",\"Profile.upsertOne\",\"Profile.deleteOne\",\"Profile.deleteMany\",\"Profile.groupBy\",\"Profile.aggregate\",\"Address.findUnique\",\"Address.findUniqueOrThrow\",\"Address.findFirst\",\"Address.findFirstOrThrow\",\"Address.findMany\",\"Address.createOne\",\"Address.createMany\",\"Address.createManyAndReturn\",\"Address.updateOne\",\"Address.updateMany\",\"Address.updateManyAndReturn\",\"Address.upsertOne\",\"Address.deleteOne\",\"Address.deleteMany\",\"Address.groupBy\",\"Address.aggregate\",\"Session.findUnique\",\"Session.findUniqueOrThrow\",\"Session.findFirst\",\"Session.findFirstOrThrow\",\"Session.findMany\",\"Session.createOne\",\"Session.createMany\",\"Session.createManyAndReturn\",\"Session.updateOne\",\"Session.updateMany\",\"Session.updateManyAndReturn\",\"Session.upsertOne\",\"Session.deleteOne\",\"Session.deleteMany\",\"Session.groupBy\",\"Session.aggregate\",\"Account.findUnique\",\"Account.findUniqueOrThrow\",\"Account.findFirst\",\"Account.findFirstOrThrow\",\"Account.findMany\",\"Account.createOne\",\"Account.createMany\",\"Account.createManyAndReturn\",\"Account.updateOne\",\"Account.updateMany\",\"Account.updateManyAndReturn\",\"Account.upsertOne\",\"Account.deleteOne\",\"Account.deleteMany\",\"Account.groupBy\",\"Account.aggregate\",\"UserRole.findUnique\",\"UserRole.findUniqueOrThrow\",\"UserRole.findFirst\",\"UserRole.findFirstOrThrow\",\"UserRole.findMany\",\"UserRole.createOne\",\"UserRole.createMany\",\"UserRole.createManyAndReturn\",\"UserRole.updateOne\",\"UserRole.updateMany\",\"UserRole.updateManyAndReturn\",\"UserRole.upsertOne\",\"UserRole.deleteOne\",\"UserRole.deleteMany\",\"UserRole.groupBy\",\"UserRole.aggregate\",\"Verification.findUnique\",\"Verification.findUniqueOrThrow\",\"Verification.findFirst\",\"Verification.findFirstOrThrow\",\"Verification.findMany\",\"Verification.createOne\",\"Verification.createMany\",\"Verification.createManyAndReturn\",\"Verification.updateOne\",\"Verification.updateMany\",\"Verification.updateManyAndReturn\",\"Verification.upsertOne\",\"Verification.deleteOne\",\"Verification.deleteMany\",\"Verification.groupBy\",\"Verification.aggregate\",\"Proclamation.findUnique\",\"Proclamation.findUniqueOrThrow\",\"Proclamation.findFirst\",\"Proclamation.findFirstOrThrow\",\"Proclamation.findMany\",\"Proclamation.createOne\",\"Proclamation.createMany\",\"Proclamation.createManyAndReturn\",\"Proclamation.updateOne\",\"Proclamation.updateMany\",\"Proclamation.updateManyAndReturn\",\"Proclamation.upsertOne\",\"Proclamation.deleteOne\",\"Proclamation.deleteMany\",\"Proclamation.groupBy\",\"Proclamation.aggregate\",\"ProclamationGroup.findUnique\",\"ProclamationGroup.findUniqueOrThrow\",\"ProclamationGroup.findFirst\",\"ProclamationGroup.findFirstOrThrow\",\"ProclamationGroup.findMany\",\"ProclamationGroup.createOne\",\"ProclamationGroup.createMany\",\"ProclamationGroup.createManyAndReturn\",\"ProclamationGroup.updateOne\",\"ProclamationGroup.updateMany\",\"ProclamationGroup.updateManyAndReturn\",\"ProclamationGroup.upsertOne\",\"ProclamationGroup.deleteOne\",\"ProclamationGroup.deleteMany\",\"ProclamationGroup.groupBy\",\"ProclamationGroup.aggregate\",\"UserProclamations.findUnique\",\"UserProclamations.findUniqueOrThrow\",\"UserProclamations.findFirst\",\"UserProclamations.findFirstOrThrow\",\"UserProclamations.findMany\",\"UserProclamations.createOne\",\"UserProclamations.createMany\",\"UserProclamations.createManyAndReturn\",\"UserProclamations.updateOne\",\"UserProclamations.updateMany\",\"UserProclamations.updateManyAndReturn\",\"UserProclamations.upsertOne\",\"UserProclamations.deleteOne\",\"UserProclamations.deleteMany\",\"UserProclamations.groupBy\",\"UserProclamations.aggregate\",\"SolfeggioFrequency.findUnique\",\"SolfeggioFrequency.findUniqueOrThrow\",\"SolfeggioFrequency.findFirst\",\"SolfeggioFrequency.findFirstOrThrow\",\"SolfeggioFrequency.findMany\",\"SolfeggioFrequency.createOne\",\"SolfeggioFrequency.createMany\",\"SolfeggioFrequency.createManyAndReturn\",\"SolfeggioFrequency.updateOne\",\"SolfeggioFrequency.updateMany\",\"SolfeggioFrequency.updateManyAndReturn\",\"SolfeggioFrequency.upsertOne\",\"SolfeggioFrequency.deleteOne\",\"SolfeggioFrequency.deleteMany\",\"SolfeggioFrequency.groupBy\",\"SolfeggioFrequency.aggregate\",\"ProclamationCategory.findUnique\",\"ProclamationCategory.findUniqueOrThrow\",\"ProclamationCategory.findFirst\",\"ProclamationCategory.findFirstOrThrow\",\"ProclamationCategory.findMany\",\"ProclamationCategory.createOne\",\"ProclamationCategory.createMany\",\"ProclamationCategory.createManyAndReturn\",\"ProclamationCategory.updateOne\",\"ProclamationCategory.updateMany\",\"ProclamationCategory.updateManyAndReturn\",\"ProclamationCategory.upsertOne\",\"ProclamationCategory.deleteOne\",\"ProclamationCategory.deleteMany\",\"ProclamationCategory.groupBy\",\"ProclamationCategory.aggregate\",\"LicenseKey.findUnique\",\"LicenseKey.findUniqueOrThrow\",\"LicenseKey.findFirst\",\"LicenseKey.findFirstOrThrow\",\"LicenseKey.findMany\",\"LicenseKey.createOne\",\"LicenseKey.createMany\",\"LicenseKey.createManyAndReturn\",\"LicenseKey.updateOne\",\"LicenseKey.updateMany\",\"LicenseKey.updateManyAndReturn\",\"LicenseKey.upsertOne\",\"LicenseKey.deleteOne\",\"LicenseKey.deleteMany\",\"LicenseKey.groupBy\",\"LicenseKey.aggregate\",\"ApiKey.findUnique\",\"ApiKey.findUniqueOrThrow\",\"ApiKey.findFirst\",\"ApiKey.findFirstOrThrow\",\"ApiKey.findMany\",\"ApiKey.createOne\",\"ApiKey.createMany\",\"ApiKey.createManyAndReturn\",\"ApiKey.updateOne\",\"ApiKey.updateMany\",\"ApiKey.updateManyAndReturn\",\"ApiKey.upsertOne\",\"ApiKey.deleteOne\",\"ApiKey.deleteMany\",\"ApiKey.groupBy\",\"ApiKey.aggregate\",\"ColorPalette.findUnique\",\"ColorPalette.findUniqueOrThrow\",\"ColorPalette.findFirst\",\"ColorPalette.findFirstOrThrow\",\"ColorPalette.findMany\",\"ColorPalette.createOne\",\"ColorPalette.createMany\",\"ColorPalette.createManyAndReturn\",\"ColorPalette.updateOne\",\"ColorPalette.updateMany\",\"ColorPalette.updateManyAndReturn\",\"ColorPalette.upsertOne\",\"ColorPalette.deleteOne\",\"ColorPalette.deleteMany\",\"ColorPalette.groupBy\",\"ColorPalette.aggregate\",\"AuditLog.findUnique\",\"AuditLog.findUniqueOrThrow\",\"AuditLog.findFirst\",\"AuditLog.findFirstOrThrow\",\"AuditLog.findMany\",\"AuditLog.createOne\",\"AuditLog.createMany\",\"AuditLog.createManyAndReturn\",\"AuditLog.updateOne\",\"AuditLog.updateMany\",\"AuditLog.updateManyAndReturn\",\"AuditLog.upsertOne\",\"AuditLog.deleteOne\",\"AuditLog.deleteMany\",\"AuditLog.groupBy\",\"AuditLog.aggregate\",\"Schedule.findUnique\",\"Schedule.findUniqueOrThrow\",\"Schedule.findFirst\",\"Schedule.findFirstOrThrow\",\"Schedule.findMany\",\"Schedule.createOne\",\"Schedule.createMany\",\"Schedule.createManyAndReturn\",\"Schedule.updateOne\",\"Schedule.updateMany\",\"Schedule.updateManyAndReturn\",\"Schedule.upsertOne\",\"Schedule.deleteOne\",\"Schedule.deleteMany\",\"Schedule.groupBy\",\"Schedule.aggregate\",\"Block.findUnique\",\"Block.findUniqueOrThrow\",\"Block.findFirst\",\"Block.findFirstOrThrow\",\"Block.findMany\",\"Block.createOne\",\"Block.createMany\",\"Block.createManyAndReturn\",\"Block.updateOne\",\"Block.updateMany\",\"Block.updateManyAndReturn\",\"Block.upsertOne\",\"Block.deleteOne\",\"Block.deleteMany\",\"Block.groupBy\",\"Block.aggregate\",\"BlockProclamations.findUnique\",\"BlockProclamations.findUniqueOrThrow\",\"BlockProclamations.findFirst\",\"BlockProclamations.findFirstOrThrow\",\"BlockProclamations.findMany\",\"BlockProclamations.createOne\",\"BlockProclamations.createMany\",\"BlockProclamations.createManyAndReturn\",\"BlockProclamations.updateOne\",\"BlockProclamations.updateMany\",\"BlockProclamations.updateManyAndReturn\",\"BlockProclamations.upsertOne\",\"BlockProclamations.deleteOne\",\"BlockProclamations.deleteMany\",\"BlockProclamations.groupBy\",\"BlockProclamations.aggregate\",\"AND\",\"OR\",\"NOT\",\"blockId\",\"proclamationId\",\"sortOrder\",\"createdAt\",\"updatedAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"id\",\"scheduleId\",\"startMinutes\",\"endMinutes\",\"dynamicAudio\",\"userId\",\"every\",\"some\",\"none\",\"email\",\"roleId\",\"roleName\",\"page\",\"action\",\"entity\",\"entityId\",\"before\",\"after\",\"ip\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"solfeggioFrequencyId\",\"colors\",\"keyPrefix\",\"keyHash\",\"expiresAt\",\"revokedAt\",\"key\",\"active\",\"lastValidatedAt\",\"systemName\",\"systemNameLower\",\"name\",\"value\",\"content\",\"categoryId\",\"groupId\",\"identifier\",\"accountId\",\"providerId\",\"accessToken\",\"refreshToken\",\"idToken\",\"accessTokenExpiresAt\",\"refreshTokenExpiresAt\",\"scope\",\"password\",\"token\",\"ipAddress\",\"userAgent\",\"profileId\",\"street\",\"city\",\"state\",\"postalCode\",\"country\",\"type\",\"isDefaultMailing\",\"isDefaultBilling\",\"isDefaultShipping\",\"firstName\",\"lastName\",\"phoneNumber\",\"ghlContactId\",\"emailVerified\",\"image\",\"blockId_proclamationId\",\"userId_proclamationId\",\"userId_systemNameLower\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), - graph: "gQi2AbACEgUAAN4EACAHAADfBAAgCAAA4AQAIAsAAOEEACAMAADiBAAgEQAA0QQAIBMAAOMEACDPAgAA3QQAMNACAAAFABDRAgAA3QQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB6wIBAAAAAewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhAQAAAAEAIAYDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYYDAQChBAAhAQAAAAMAIBIFAADeBAAgBwAA3wQAIAgAAOAEACALAADhBAAgDAAA4gQAIBEAANEEACATAADjBAAgzwIAAN0EADDQAgAABQAQ0QIAAN0EADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHrAgEAoQQAIewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhCQUAAKQHACAHAAClBwAgCAAApgcAIAsAAKcHACAMAACoBwAgEQAAoQcAIBMAAJoHACDsAgAAmwUAIKcDAACbBQAgAwAAAAUAIAEAAAYAMAIAAAEAIAEAAAAFACAMBgAAlAQAIM8CAADcBAAw0AIAAAkAENECAADcBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACH_AkAAkwQAIZUDAQChBAAhlgMBAKIEACGXAwEAogQAIQMGAACZBQAglgMAAJsFACCXAwAAmwUAIAwGAACUBAAgzwIAANwEADDQAgAACQAQ0QIAANwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAecCAQChBAAh_wJAAJMEACGVAwEAAAABlgMBAKIEACGXAwEAogQAIQMAAAAJACABAAAKADACAAALACARBgAAlAQAIM8CAADbBAAw0AIAAA0AENECAADbBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACGMAwEAoQQAIY0DAQChBAAhjgMBAKIEACGPAwEAogQAIZADAQCiBAAhkQNAALAEACGSA0AAsAQAIZMDAQCiBAAhlAMBAKIEACEIBgAAmQUAII4DAACbBQAgjwMAAJsFACCQAwAAmwUAIJEDAACbBQAgkgMAAJsFACCTAwAAmwUAIJQDAACbBQAgEQYAAJQEACDPAgAA2wQAMNACAAANABDRAgAA2wQAMNUCQACTBAAh1gJAAJMEACHiAgEAAAAB5wIBAKEEACGMAwEAoQQAIY0DAQChBAAhjgMBAKIEACGPAwEAogQAIZADAQCiBAAhkQNAALAEACGSA0AAsAQAIZMDAQCiBAAhlAMBAKIEACEDAAAADQAgAQAADgAwAgAADwAgDAYAANYEACAKAADaBAAgzwIAANkEADDQAgAAEQAQ0QIAANkEADDiAgEAoQQAIecCAQCiBAAh6wIBAKIEACGiAwEAoQQAIaMDAQChBAAhpAMBAKEEACGlAwEAogQAIQUGAACZBQAgCgAAowcAIOcCAACbBQAg6wIAAJsFACClAwAAmwUAIAwGAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAAAAAecCAQCiBAAh6wIBAKIEACGiAwEAoQQAIaMDAQChBAAhpAMBAKEEACGlAwEAogQAIQMAAAARACABAAASADACAAATACABAAAABQAgDwkAANgEACDPAgAA1wQAMNACAAAWABDRAgAA1wQAMOICAQChBAAhmAMBAKEEACGZAwEAoQQAIZoDAQChBAAhmwMBAKEEACGcAwEAoQQAIZ0DAQChBAAhngMBAKEEACGfAyAAxgQAIaADIADGBAAhoQMgAMYEACEBCQAAogcAIA8JAADYBAAgzwIAANcEADDQAgAAFgAQ0QIAANcEADDiAgEAAAABmAMBAKEEACGZAwEAoQQAIZoDAQChBAAhmwMBAKEEACGcAwEAoQQAIZ0DAQChBAAhngMBAKEEACGfAyAAxgQAIaADIADGBAAhoQMgAMYEACEDA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_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABARwAAIwBACAI1QJAAAAAAdYCQAAAAAHiAgEAAAAB5wIBAAAAAf8CQAAAAAGVAwEAAAABlgMBAAAAAZcDAQAAAAEBHAAAjgEAMAEcAACOAQAwCQYAAIcHACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIf8CQADqBAAhlQMBAOsEACGWAwEAoQUAIZcDAQChBQAhAgAAAAsAIBwAAJEBACAI1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh5wIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQIAAAAJACAcAACTAQAgAgAAAAkAIBwAAJMBACADAAAACwAgIwAAjAEAICQAAJEBACABAAAACwAgAQAAAAkAIAUEAACEBwAgKwAAhgcAICwAAIUHACCWAwAAmwUAIJcDAACbBQAgC88CAADBBAAw0AIAAJoBABDRAgAAwQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAh_wJAAIUEACGVAwEAgwQAIZYDAQCXBAAhlwMBAJcEACEDAAAACQAgAQAAmQEAMCgAAJoBACADAAAACQAgAQAACgAwAgAACwAgAQAAAA8AIAEAAAAPACADAAAADQAgAQAADgAwAgAADwAgAwAAAA0AIAEAAA4AMAIAAA8AIAMAAAANACABAAAOADACAAAPACAOBgAAgwcAINUCQAAAAAHWAkAAAAAB4gIBAAAAAecCAQAAAAGMAwEAAAABjQMBAAAAAY4DAQAAAAGPAwEAAAABkAMBAAAAAZEDQAAAAAGSA0AAAAABkwMBAAAAAZQDAQAAAAEBHAAAogEAIA3VAkAAAAAB1gJAAAAAAeICAQAAAAHnAgEAAAABjAMBAAAAAY0DAQAAAAGOAwEAAAABjwMBAAAAAZADAQAAAAGRA0AAAAABkgNAAAAAAZMDAQAAAAGUAwEAAAABARwAAKQBADABHAAApAEAMA4GAACCBwAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh5wIBAOsEACGMAwEA6wQAIY0DAQDrBAAhjgMBAKEFACGPAwEAoQUAIZADAQChBQAhkQNAAKsFACGSA0AAqwUAIZMDAQChBQAhlAMBAKEFACECAAAADwAgHAAApwEAIA3VAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIYwDAQDrBAAhjQMBAOsEACGOAwEAoQUAIY8DAQChBQAhkAMBAKEFACGRA0AAqwUAIZIDQACrBQAhkwMBAKEFACGUAwEAoQUAIQIAAAANACAcAACpAQAgAgAAAA0AIBwAAKkBACADAAAADwAgIwAAogEAICQAAKcBACABAAAADwAgAQAAAA0AIAoEAAD_BgAgKwAAgQcAICwAAIAHACCOAwAAmwUAII8DAACbBQAgkAMAAJsFACCRAwAAmwUAIJIDAACbBQAgkwMAAJsFACCUAwAAmwUAIBDPAgAAwAQAMNACAACwAQAQ0QIAAMAEADDVAkAAhQQAIdYCQACFBAAh4gIBAIMEACHnAgEAgwQAIYwDAQCDBAAhjQMBAIMEACGOAwEAlwQAIY8DAQCXBAAhkAMBAJcEACGRA0AArAQAIZIDQACsBAAhkwMBAJcEACGUAwEAlwQAIQMAAAANACABAACvAQAwKAAAsAEAIAMAAAANACABAAAOADACAAAPACAGAwAAvwQAIM8CAAC-BAAw0AIAAAMAENECAAC-BAAw4gICAAAAAYYDAQChBAAhAQAAALMBACABAAAAswEAIAEDAAD-BgAgAwAAAAMAIAEAALYBADACAACzAQAgAwAAAAMAIAEAALYBADACAACzAQAgAwAAAAMAIAEAALYBADACAACzAQAgAwMAAP0GACDiAgIAAAABhgMBAAAAAQEcAAC6AQAgAuICAgAAAAGGAwEAAAABARwAALwBADABHAAAvAEAMAMDAACYBgAg4gICAOkEACGGAwEA6wQAIQIAAACzAQAgHAAAvwEAIALiAgIA6QQAIYYDAQDrBAAhAgAAAAMAIBwAAMEBACACAAAAAwAgHAAAwQEAIAMAAACzAQAgIwAAugEAICQAAL8BACABAAAAswEAIAEAAAADACAFBAAAkwYAICkAAJQGACAqAACXBgAgKwAAlgYAICwAAJUGACAFzwIAAL0EADDQAgAAyAEAENECAAC9BAAw4gICAIQEACGGAwEAgwQAIQMAAAADACABAADHAQAwKAAAyAEAIAMAAAADACABAAC2AQAwAgAAswEAIAnPAgAAvAQAMNACAADOAQAQ0QIAALwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAf8CQACTBAAhhwMBAKEEACGLAwEAoQQAIQEAAADLAQAgAQAAAMsBACAJzwIAALwEADDQAgAAzgEAENECAAC8BAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh_wJAAJMEACGHAwEAoQQAIYsDAQChBAAhAAMAAADOAQAgAQAAzwEAMAIAAMsBACADAAAAzgEAIAEAAM8BADACAADLAQAgAwAAAM4BACABAADPAQAwAgAAywEAIAbVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABhwMBAAAAAYsDAQAAAAEBHAAA0wEAIAbVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABhwMBAAAAAYsDAQAAAAEBHAAA1QEAMAEcAADVAQAwBtUCQADqBAAh1gJAAOoEACHiAgEA6wQAIf8CQADqBAAhhwMBAOsEACGLAwEA6wQAIQIAAADLAQAgHAAA2AEAIAbVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIYcDAQDrBAAhiwMBAOsEACECAAAAzgEAIBwAANoBACACAAAAzgEAIBwAANoBACADAAAAywEAICMAANMBACAkAADYAQAgAQAAAMsBACABAAAAzgEAIAMEAACQBgAgKwAAkgYAICwAAJEGACAJzwIAALsEADDQAgAA4QEAENECAAC7BAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh_wJAAIUEACGHAwEAgwQAIYsDAQCDBAAhAwAAAM4BACABAADgAQAwKAAA4QEAIAMAAADOAQAgAQAAzwEAMAIAAMsBACABAAAAJgAgAQAAACYAIAMAAAAkACABAAAlADACAAAmACADAAAAJAAgAQAAJQAwAgAAJgAgAwAAACQAIAEAACUAMAIAACYAIAsOAADeBQAgDwAA9AUAIBAAAN8FACARAADgBQAgFgAA4QUAIOICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQEcAADpAQAgBuICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQEcAADrAQAwARwAAOsBADALDgAAwgUAIA8AAPIFACAQAADDBQAgEQAAxAUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACECAAAAJgAgHAAA7gEAIAbiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACECAAAAJAAgHAAA8AEAIAIAAAAkACAcAADwAQAgAwAAACYAICMAAOkBACAkAADuAQAgAQAAACYAIAEAAAAkACAFBAAAiwYAICkAAIwGACAqAACPBgAgKwAAjgYAICwAAI0GACAJzwIAALoEADDQAgAA9wEAENECAAC6BAAw4gIBAIMEACH7AgIAhAQAIYYDAQCDBAAhiAMBAIMEACGJAwIAhAQAIYoDAgCEBAAhAwAAACQAIAEAAPYBADAoAAD3AQAgAwAAACQAIAEAACUAMAIAACYAIAcNAAC0BAAgzwIAALkEADDQAgAA_QEAENECAAC5BAAw1AICAKkEACHiAgIAAAABhgMBAAAAAQEAAAD6AQAgAQAAAPoBACAHDQAAtAQAIM8CAAC5BAAw0AIAAP0BABDRAgAAuQQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQENAADjBQAgAwAAAP0BACABAAD-AQAwAgAA-gEAIAMAAAD9AQAgAQAA_gEAMAIAAPoBACADAAAA_QEAIAEAAP4BADACAAD6AQAgBA0AAIoGACDUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAggIAIAPUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAhAIAMAEcAACEAgAwBA0AAIAGACDUAgIA6QQAIeICAgDpBAAhhgMBAOsEACECAAAA-gEAIBwAAIcCACAD1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhAgAAAP0BACAcAACJAgAgAgAAAP0BACAcAACJAgAgAwAAAPoBACAjAACCAgAgJAAAhwIAIAEAAAD6AQAgAQAAAP0BACAFBAAA-wUAICkAAPwFACAqAAD_BQAgKwAA_gUAICwAAP0FACAGzwIAALgEADDQAgAAkAIAENECAAC4BAAw1AICAIQEACHiAgIAhAQAIYYDAQCDBAAhAwAAAP0BACABAACPAgAwKAAAkAIAIAMAAAD9AQAgAQAA_gEAMAIAAPoBACABAAAAIgAgAQAAACIAIAMAAAAgACABAAAhADACAAAiACADAAAAIAAgAQAAIQAwAgAAIgAgAwAAACAAIAEAACEAMAIAACIAIAYGAADcBQAgFQAA-gUAINMCAQAAAAHVAkAAAAAB1gJAAAAAAecCAQAAAAEBHAAAmAIAIATTAgEAAAAB1QJAAAAAAdYCQAAAAAHnAgEAAAABARwAAJoCADABHAAAmgIAMAYGAADaBQAgFQAA-QUAINMCAQDrBAAh1QJAAOoEACHWAkAA6gQAIecCAQDrBAAhAgAAACIAIBwAAJ0CACAE0wIBAOsEACHVAkAA6gQAIdYCQADqBAAh5wIBAOsEACECAAAAIAAgHAAAnwIAIAIAAAAgACAcAACfAgAgAwAAACIAICMAAJgCACAkAACdAgAgAQAAACIAIAEAAAAgACADBAAA9gUAICsAAPgFACAsAAD3BQAgB88CAAC3BAAw0AIAAKYCABDRAgAAtwQAMNMCAQCDBAAh1QJAAIUEACHWAkAAhQQAIecCAQCDBAAhAwAAACAAIAEAAKUCADAoAACmAgAgAwAAACAAIAEAACEAMAIAACIAIAcNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAAAAAYYDAQAAAAGHAwIAAAABAQAAAKkCACABAAAAqQIAIAcNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAKkEACGGAwEAoQQAIYcDAgCpBAAhAQ0AAOMFACADAAAArAIAIAEAAK0CADACAACpAgAgAwAAAKwCACABAACtAgAwAgAAqQIAIAMAAACsAgAgAQAArQIAMAIAAKkCACAEDQAA9QUAIOICAgAAAAGGAwEAAAABhwMCAAAAAQEcAACxAgAgA-ICAgAAAAGGAwEAAAABhwMCAAAAAQEcAACzAgAwARwAALMCADAEDQAA6QUAIOICAgDpBAAhhgMBAOsEACGHAwIA6QQAIQIAAACpAgAgHAAAtgIAIAPiAgIA6QQAIYYDAQDrBAAhhwMCAOkEACECAAAArAIAIBwAALgCACACAAAArAIAIBwAALgCACADAAAAqQIAICMAALECACAkAAC2AgAgAQAAAKkCACABAAAArAIAIAUEAADkBQAgKQAA5QUAICoAAOgFACArAADnBQAgLAAA5gUAIAbPAgAAtQQAMNACAAC_AgAQ0QIAALUEADDiAgIAhAQAIYYDAQCDBAAhhwMCAIQEACEDAAAArAIAIAEAAL4CADAoAAC_AgAgAwAAAKwCACABAACtAgAwAgAAqQIAIAcNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAAAABhgMBAAAAAQEAAADCAgAgAQAAAMICACAHDQAAtAQAIM8CAACzBAAw0AIAAMUCABDRAgAAswQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQENAADjBQAgAwAAAMUCACABAADGAgAwAgAAwgIAIAMAAADFAgAgAQAAxgIAMAIAAMICACADAAAAxQIAIAEAAMYCADACAADCAgAgBA0AAOIFACDUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAygIAIAPUAgIAAAAB4gICAAAAAYYDAQAAAAEBHAAAzAIAMAEcAADMAgAwBA0AALYFACDUAgIA6QQAIeICAgDpBAAhhgMBAOsEACECAAAAwgIAIBwAAM8CACAD1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhAgAAAMUCACAcAADRAgAgAgAAAMUCACAcAADRAgAgAwAAAMICACAjAADKAgAgJAAAzwIAIAEAAADCAgAgAQAAAMUCACAFBAAAsQUAICkAALIFACAqAAC1BQAgKwAAtAUAICwAALMFACAGzwIAALIEADDQAgAA2AIAENECAACyBAAw1AICAIQEACHiAgIAhAQAIYYDAQCDBAAhAwAAAMUCACABAADXAgAwKAAA2AIAIAMAAADFAgAgAQAAxgIAMAIAAMICACABAAAAHQAgAQAAAB0AIAMAAAAbACABAAAcADACAAAdACADAAAAGwAgAQAAHAAwAgAAHQAgAwAAABsAIAEAABwAMAIAAB0AIAoGAACwBQAg1QJAAAAAAeICAQAAAAHnAgEAAAAB6wIBAAAAAYEDAQAAAAGCAyAAAAABgwNAAAAAAYQDAQAAAAGFAwEAAAABARwAAOACACAJ1QJAAAAAAeICAQAAAAHnAgEAAAAB6wIBAAAAAYEDAQAAAAGCAyAAAAABgwNAAAAAAYQDAQAAAAGFAwEAAAABARwAAOICADABHAAA4gIAMAEAAAAFACAKBgAArwUAINUCQADqBAAh4gIBAOsEACHnAgEAoQUAIesCAQChBQAhgQMBAOsEACGCAyAA9QQAIYMDQACrBQAhhAMBAKEFACGFAwEAoQUAIQIAAAAdACAcAADmAgAgCdUCQADqBAAh4gIBAOsEACHnAgEAoQUAIesCAQChBQAhgQMBAOsEACGCAyAA9QQAIYMDQACrBQAhhAMBAKEFACGFAwEAoQUAIQIAAAAbACAcAADoAgAgAgAAABsAIBwAAOgCACABAAAABQAgAwAAAB0AICMAAOACACAkAADmAgAgAQAAAB0AIAEAAAAbACAIBAAArAUAICsAAK4FACAsAACtBQAg5wIAAJsFACDrAgAAmwUAIIMDAACbBQAghAMAAJsFACCFAwAAmwUAIAzPAgAAsQQAMNACAADwAgAQ0QIAALEEADDVAkAAhQQAIeICAQCDBAAh5wIBAJcEACHrAgEAlwQAIYEDAQCDBAAhggMgAI4EACGDA0AArAQAIYQDAQCXBAAhhQMBAJcEACEDAAAAGwAgAQAA7wIAMCgAAPACACADAAAAGwAgAQAAHAAwAgAAHQAgCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAAAAAf0CAQChBAAh_gIBAAAAAf8CQACwBAAhgANAALAEACEBAAAA8wIAIAEAAADzAgAgCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAKEEACH9AgEAoQQAIf4CAQChBAAh_wJAALAEACGAA0AAsAQAIQL_AgAAmwUAIIADAACbBQAgAwAAAPYCACABAAD3AgAwAgAA8wIAIAMAAAD2AgAgAQAA9wIAMAIAAPMCACADAAAA9gIAIAEAAPcCADACAADzAgAgBtUCQAAAAAHiAgEAAAAB_QIBAAAAAf4CAQAAAAH_AkAAAAABgANAAAAAAQEcAAD7AgAgBtUCQAAAAAHiAgEAAAAB_QIBAAAAAf4CAQAAAAH_AkAAAAABgANAAAAAAQEcAAD9AgAwARwAAP0CADAG1QJAAOoEACHiAgEA6wQAIf0CAQDrBAAh_gIBAOsEACH_AkAAqwUAIYADQACrBQAhAgAAAPMCACAcAACAAwAgBtUCQADqBAAh4gIBAOsEACH9AgEA6wQAIf4CAQDrBAAh_wJAAKsFACGAA0AAqwUAIQIAAAD2AgAgHAAAggMAIAIAAAD2AgAgHAAAggMAIAMAAADzAgAgIwAA-wIAICQAAIADACABAAAA8wIAIAEAAAD2AgAgBQQAAKgFACArAACqBQAgLAAAqQUAIP8CAACbBQAggAMAAJsFACAJzwIAAKsEADDQAgAAiQMAENECAACrBAAw1QJAAIUEACHiAgEAgwQAIf0CAQCDBAAh_gIBAIMEACH_AkAArAQAIYADQACsBAAhAwAAAPYCACABAACIAwAwKAAAiQMAIAMAAAD2AgAgAQAA9wIAMAIAAPMCACAFzwIAAKgEADDQAgAAjwMAENECAACoBAAw-wICAAAAAfwCAACqBAAgAQAAAIwDACABAAAAjAMAIAXPAgAAqAQAMNACAACPAwAQ0QIAAKgEADD7AgIAqQQAIfwCAACqBAAgAAMAAACPAwAgAQAAkAMAMAIAAIwDACADAAAAjwMAIAEAAJADADACAACMAwAgAwAAAI8DACABAACQAwAwAgAAjAMAIAL7AgIAAAAB_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_wMAENECAACCBAAw0gIBAIMEACHTAgEAgwQAIdQCAgCEBAAh1QJAAIUEACHWAkAAhQQAIQMAAAAuACABAAD-AwAwKAAA_wMAIAMAAAAuACABAAAvADACAAAwACAIzwIAAIIEADDQAgAA_wMAENECAACCBAAw0gIBAIMEACHTAgEAgwQAIdQCAgCEBAAh1QJAAIUEACHWAkAAhQQAIQ4EAACHBAAgKwAAjAQAICwAAIwEACDXAgEAAAAB2AIBAAAABNkCAQAAAATaAgEAAAAB2wIBAAAAAdwCAQAAAAHdAgEAAAAB3gIBAIsEACHfAgEAAAAB4AIBAAAAAeECAQAAAAENBAAAhwQAICkAAIoEACAqAACHBAAgKwAAhwQAICwAAIcEACDXAgIAAAAB2AICAAAABNkCAgAAAATaAgIAAAAB2wICAAAAAdwCAgAAAAHdAgIAAAAB3gICAIkEACELBAAAhwQAICsAAIgEACAsAACIBAAg1wJAAAAAAdgCQAAAAATZAkAAAAAE2gJAAAAAAdsCQAAAAAHcAkAAAAAB3QJAAAAAAd4CQACGBAAhCwQAAIcEACArAACIBAAgLAAAiAQAINcCQAAAAAHYAkAAAAAE2QJAAAAABNoCQAAAAAHbAkAAAAAB3AJAAAAAAd0CQAAAAAHeAkAAhgQAIQjXAgIAAAAB2AICAAAABNkCAgAAAATaAgIAAAAB2wICAAAAAdwCAgAAAAHdAgIAAAAB3gICAIcEACEI1wJAAAAAAdgCQAAAAATZAkAAAAAE2gJAAAAAAdsCQAAAAAHcAkAAAAAB3QJAAAAAAd4CQACIBAAhDQQAAIcEACApAACKBAAgKgAAhwQAICsAAIcEACAsAACHBAAg1wICAAAAAdgCAgAAAATZAgIAAAAE2gICAAAAAdsCAgAAAAHcAgIAAAAB3QICAAAAAd4CAgCJBAAhCNcCCAAAAAHYAggAAAAE2QIIAAAABNoCCAAAAAHbAggAAAAB3AIIAAAAAd0CCAAAAAHeAggAigQAIQ4EAACHBAAgKwAAjAQAICwAAIwEACDXAgEAAAAB2AIBAAAABNkCAQAAAATaAgEAAAAB2wIBAAAAAdwCAQAAAAHdAgEAAAAB3gIBAIsEACHfAgEAAAAB4AIBAAAAAeECAQAAAAEL1wIBAAAAAdgCAQAAAATZAgEAAAAE2gIBAAAAAdsCAQAAAAHcAgEAAAAB3QIBAAAAAd4CAQCMBAAh3wIBAAAAAeACAQAAAAHhAgEAAAABCs8CAACNBAAw0AIAAOkDABDRAgAAjQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIeMCAQCDBAAh5AICAIQEACHlAgIAhAQAIeYCIACOBAAhBQQAAIcEACArAACQBAAgLAAAkAQAINcCIAAAAAHeAiAAjwQAIQUEAACHBAAgKwAAkAQAICwAAJAEACDXAiAAAAAB3gIgAI8EACEC1wIgAAAAAd4CIACQBAAhB88CAACRBAAw0AIAANMDABDRAgAAkQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAhCQYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIQjXAkAAAAAB2AJAAAAABNkCQAAAAATaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAIgEACEUBQAA3gQAIAcAAN8EACAIAADgBAAgCwAA4QQAIAwAAOIEACARAADRBAAgEwAA4wQAIM8CAADdBAAw0AIAAAUAENECAADdBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh6wIBAKEEACHsAgIAowQAIYYDAQChBAAhpgMgAMYEACGnAwEAogQAIasDAAAFACCsAwAABQAgA-gCAAAyACDpAgAAMgAg6gIAADIAIBDPAgAAlgQAMNACAAC7AwAQ0QIAAJYEADDVAkAAhQQAIeICAQCDBAAh5wIBAJcEACHrAgEAlwQAIewCAgCYBAAh7QIBAJcEACHuAgEAgwQAIe8CAQCDBAAh8AIBAJcEACHxAgEAlwQAIfICAACZBAAg8wIAAJkEACD0AgEAlwQAIQ4EAACaBAAgKwAAnwQAICwAAJ8EACDXAgEAAAAB2AIBAAAABdkCAQAAAAXaAgEAAAAB2wIBAAAAAdwCAQAAAAHdAgEAAAAB3gIBAJ4EACHfAgEAAAAB4AIBAAAAAeECAQAAAAENBAAAmgQAICkAAJ0EACAqAACaBAAgKwAAmgQAICwAAJoEACDXAgIAAAAB2AICAAAABdkCAgAAAAXaAgIAAAAB2wICAAAAAdwCAgAAAAHdAgIAAAAB3gICAJwEACEPBAAAmgQAICsAAJsEACAsAACbBAAg1wKAAAAAAdoCgAAAAAHbAoAAAAAB3AKAAAAAAd0CgAAAAAHeAoAAAAAB9QIBAAAAAfYCAQAAAAH3AgEAAAAB-AKAAAAAAfkCgAAAAAH6AoAAAAABCNcCAgAAAAHYAgIAAAAF2QICAAAABdoCAgAAAAHbAgIAAAAB3AICAAAAAd0CAgAAAAHeAgIAmgQAIQzXAoAAAAAB2gKAAAAAAdsCgAAAAAHcAoAAAAAB3QKAAAAAAd4CgAAAAAH1AgEAAAAB9gIBAAAAAfcCAQAAAAH4AoAAAAAB-QKAAAAAAfoCgAAAAAENBAAAmgQAICkAAJ0EACAqAACaBAAgKwAAmgQAICwAAJoEACDXAgIAAAAB2AICAAAABdkCAgAAAAXaAgIAAAAB2wICAAAAAdwCAgAAAAHdAgIAAAAB3gICAJwEACEI1wIIAAAAAdgCCAAAAAXZAggAAAAF2gIIAAAAAdsCCAAAAAHcAggAAAAB3QIIAAAAAd4CCACdBAAhDgQAAJoEACArAACfBAAgLAAAnwQAINcCAQAAAAHYAgEAAAAF2QIBAAAABdoCAQAAAAHbAgEAAAAB3AIBAAAAAd0CAQAAAAHeAgEAngQAId8CAQAAAAHgAgEAAAAB4QIBAAAAAQvXAgEAAAAB2AIBAAAABdkCAQAAAAXaAgEAAAAB2wIBAAAAAdwCAQAAAAHdAgEAAAAB3gIBAJ8EACHfAgEAAAAB4AIBAAAAAeECAQAAAAEQzwIAAKAEADDQAgAAqAMAENECAACgBAAw1QJAAJMEACHiAgEAoQQAIecCAQCiBAAh6wIBAKIEACHsAgIAowQAIe0CAQCiBAAh7gIBAKEEACHvAgEAoQQAIfACAQCiBAAh8QIBAKIEACHyAgAApAQAIPMCAACkBAAg9AIBAKIEACEL1wIBAAAAAdgCAQAAAATZAgEAAAAE2gIBAAAAAdsCAQAAAAHcAgEAAAAB3QIBAAAAAd4CAQCMBAAh3wIBAAAAAeACAQAAAAHhAgEAAAABC9cCAQAAAAHYAgEAAAAF2QIBAAAABdoCAQAAAAHbAgEAAAAB3AIBAAAAAd0CAQAAAAHeAgEAnwQAId8CAQAAAAHgAgEAAAAB4QIBAAAAAQjXAgIAAAAB2AICAAAABdkCAgAAAAXaAgIAAAAB2wICAAAAAdwCAgAAAAHdAgIAAAAB3gICAJoEACEM1wKAAAAAAdoCgAAAAAHbAoAAAAAB3AKAAAAAAd0CgAAAAAHeAoAAAAAB9QIBAAAAAfYCAQAAAAH3AgEAAAAB-AKAAAAAAfkCgAAAAAH6AoAAAAABBc8CAAClBAAw0AIAAKIDABDRAgAApQQAMPsCAgCEBAAh_AIAAKYEACAPBAAAhwQAICsAAKcEACAsAACnBAAg1wKAAAAAAdoCgAAAAAHbAoAAAAAB3AKAAAAAAd0CgAAAAAHeAoAAAAAB9QIBAAAAAfYCAQAAAAH3AgEAAAAB-AKAAAAAAfkCgAAAAAH6AoAAAAABDNcCgAAAAAHaAoAAAAAB2wKAAAAAAdwCgAAAAAHdAoAAAAAB3gKAAAAAAfUCAQAAAAH2AgEAAAAB9wIBAAAAAfgCgAAAAAH5AoAAAAAB-gKAAAAAAQXPAgAAqAQAMNACAACPAwAQ0QIAAKgEADD7AgIAqQQAIfwCAACqBAAgCNcCAgAAAAHYAgIAAAAE2QICAAAABNoCAgAAAAHbAgIAAAAB3AICAAAAAd0CAgAAAAHeAgIAhwQAIQzXAoAAAAAB2gKAAAAAAdsCgAAAAAHcAoAAAAAB3QKAAAAAAd4CgAAAAAH1AgEAAAAB9gIBAAAAAfcCAQAAAAH4AoAAAAAB-QKAAAAAAfoCgAAAAAEJzwIAAKsEADDQAgAAiQMAENECAACrBAAw1QJAAIUEACHiAgEAgwQAIf0CAQCDBAAh_gIBAIMEACH_AkAArAQAIYADQACsBAAhCwQAAJoEACArAACuBAAgLAAArgQAINcCQAAAAAHYAkAAAAAF2QJAAAAABdoCQAAAAAHbAkAAAAAB3AJAAAAAAd0CQAAAAAHeAkAArQQAIQsEAACaBAAgKwAArgQAICwAAK4EACDXAkAAAAAB2AJAAAAABdkCQAAAAAXaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAK0EACEI1wJAAAAAAdgCQAAAAAXZAkAAAAAF2gJAAAAAAdsCQAAAAAHcAkAAAAAB3QJAAAAAAd4CQACuBAAhCc8CAACvBAAw0AIAAPYCABDRAgAArwQAMNUCQACTBAAh4gIBAKEEACH9AgEAoQQAIf4CAQChBAAh_wJAALAEACGAA0AAsAQAIQjXAkAAAAAB2AJAAAAABdkCQAAAAAXaAkAAAAAB2wJAAAAAAdwCQAAAAAHdAkAAAAAB3gJAAK4EACEMzwIAALEEADDQAgAA8AIAENECAACxBAAw1QJAAIUEACHiAgEAgwQAIecCAQCXBAAh6wIBAJcEACGBAwEAgwQAIYIDIACOBAAhgwNAAKwEACGEAwEAlwQAIYUDAQCXBAAhBs8CAACyBAAw0AIAANgCABDRAgAAsgQAMNQCAgCEBAAh4gICAIQEACGGAwEAgwQAIQcNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhA-gCAAAkACDpAgAAJAAg6gIAACQAIAbPAgAAtQQAMNACAAC_AgAQ0QIAALUEADDiAgIAhAQAIYYDAQCDBAAhhwMCAIQEACEHDQAAtAQAIM8CAAC2BAAw0AIAAKwCABDRAgAAtgQAMOICAgCpBAAhhgMBAKEEACGHAwIAqQQAIQfPAgAAtwQAMNACAACmAgAQ0QIAALcEADDTAgEAgwQAIdUCQACFBAAh1gJAAIUEACHnAgEAgwQAIQbPAgAAuAQAMNACAACQAgAQ0QIAALgEADDUAgIAhAQAIeICAgCEBAAhhgMBAIMEACEHDQAAtAQAIM8CAAC5BAAw0AIAAP0BABDRAgAAuQQAMNQCAgCpBAAh4gICAKkEACGGAwEAoQQAIQnPAgAAugQAMNACAAD3AQAQ0QIAALoEADDiAgEAgwQAIfsCAgCEBAAhhgMBAIMEACGIAwEAgwQAIYkDAgCEBAAhigMCAIQEACEJzwIAALsEADDQAgAA4QEAENECAAC7BAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh_wJAAIUEACGHAwEAgwQAIYsDAQCDBAAhCc8CAAC8BAAw0AIAAM4BABDRAgAAvAQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIf8CQACTBAAhhwMBAKEEACGLAwEAoQQAIQXPAgAAvQQAMNACAADIAQAQ0QIAAL0EADDiAgIAhAQAIYYDAQCDBAAhBgMAAL8EACDPAgAAvgQAMNACAAADABDRAgAAvgQAMOICAgCpBAAhhgMBAKEEACED6AIAAAUAIOkCAAAFACDqAgAABQAgEM8CAADABAAw0AIAALABABDRAgAAwAQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAhjAMBAIMEACGNAwEAgwQAIY4DAQCXBAAhjwMBAJcEACGQAwEAlwQAIZEDQACsBAAhkgNAAKwEACGTAwEAlwQAIZQDAQCXBAAhC88CAADBBAAw0AIAAJoBABDRAgAAwQQAMNUCQACFBAAh1gJAAIUEACHiAgEAgwQAIecCAQCDBAAh_wJAAIUEACGVAwEAgwQAIZYDAQCXBAAhlwMBAJcEACEOzwIAAMIEADDQAgAAhAEAENECAADCBAAw4gIBAIMEACGYAwEAgwQAIZkDAQCDBAAhmgMBAIMEACGbAwEAgwQAIZwDAQCDBAAhnQMBAIMEACGeAwEAgwQAIZ8DIACOBAAhoAMgAI4EACGhAyAAjgQAIQrPAgAAwwQAMNACAABuABDRAgAAwwQAMOICAQCDBAAh5wIBAJcEACHrAgEAlwQAIaIDAQCDBAAhowMBAIMEACGkAwEAgwQAIaUDAQCXBAAhC88CAADEBAAw0AIAAFYAENECAADEBAAw1QJAAIUEACHWAkAAhQQAIeICAQCDBAAh6wIBAIMEACHsAgIAmAQAIYYDAQCDBAAhpgMgAI4EACGnAwEAlwQAIQwNAADIBAAgEwAAxwQAIM8CAADFBAAw0AIAADIAENECAADFBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh4wIBAKEEACHkAgIAqQQAIeUCAgCpBAAh5gIgAMYEACEC1wIgAAAAAd4CIACQBAAhCwYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIasDAAA7ACCsAwAAOwAgA-gCAAAuACDpAgAALgAg6gIAAC4AIALSAgEAAAAB0wIBAAAAAQoUAADLBAAgFQAAzAQAIM8CAADKBAAw0AIAAC4AENECAADKBAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIQ4NAADIBAAgEwAAxwQAIM8CAADFBAAw0AIAADIAENECAADFBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh4wIBAKEEACHkAgIAqQQAIeUCAgCpBAAh5gIgAMYEACGrAwAAMgAgrAMAADIAIBAOAADOBAAgDwAAzwQAIBAAANAEACARAADRBAAgFgAAyAQAIM8CAADNBAAw0AIAACQAENECAADNBAAw4gIBAKEEACH7AgIAqQQAIYYDAQChBAAhiAMBAKEEACGJAwIAqQQAIYoDAgCpBAAhqwMAACQAIKwDAAAkACAODgAAzgQAIA8AAM8EACAQAADQBAAgEQAA0QQAIBYAAMgEACDPAgAAzQQAMNACAAAkABDRAgAAzQQAMOICAQChBAAh-wICAKkEACGGAwEAoQQAIYgDAQChBAAhiQMCAKkEACGKAwIAqQQAIQkNAAC0BAAgzwIAALYEADDQAgAArAIAENECAAC2BAAw4gICAKkEACGGAwEAoQQAIYcDAgCpBAAhqwMAAKwCACCsAwAArAIAIAkNAAC0BAAgzwIAALMEADDQAgAAxQIAENECAACzBAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhqwMAAMUCACCsAwAAxQIAIAkNAAC0BAAgzwIAALkEADDQAgAA_QEAENECAAC5BAAw1AICAKkEACHiAgIAqQQAIYYDAQChBAAhqwMAAP0BACCsAwAA_QEAIAPoAgAAIAAg6QIAACAAIOoCAAAgACAC0wIBAAAAAecCAQAAAAEJBgAAlAQAIBUAAMwEACDPAgAA0wQAMNACAAAgABDRAgAA0wQAMNMCAQChBAAh1QJAAJMEACHWAkAAkwQAIecCAQChBAAhAucCAQAAAAGFAwEAAAABDQYAANYEACDPAgAA1QQAMNACAAAbABDRAgAA1QQAMNUCQACTBAAh4gIBAKEEACHnAgEAogQAIesCAQCiBAAhgQMBAKEEACGCAyAAxgQAIYMDQACwBAAhhAMBAKIEACGFAwEAogQAIRQFAADeBAAgBwAA3wQAIAgAAOAEACALAADhBAAgDAAA4gQAIBEAANEEACATAADjBAAgzwIAAN0EADDQAgAABQAQ0QIAAN0EADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHrAgEAoQQAIewCAgCjBAAhhgMBAKEEACGmAyAAxgQAIacDAQCiBAAhqwMAAAUAIKwDAAAFACAPCQAA2AQAIM8CAADXBAAw0AIAABYAENECAADXBAAw4gIBAKEEACGYAwEAoQQAIZkDAQChBAAhmgMBAKEEACGbAwEAoQQAIZwDAQChBAAhnQMBAKEEACGeAwEAoQQAIZ8DIADGBAAhoAMgAMYEACGhAyAAxgQAIQ4GAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAKEEACHnAgEAogQAIesCAQCiBAAhogMBAKEEACGjAwEAoQQAIaQDAQChBAAhpQMBAKIEACGrAwAAEQAgrAMAABEAIAwGAADWBAAgCgAA2gQAIM8CAADZBAAw0AIAABEAENECAADZBAAw4gIBAKEEACHnAgEAogQAIesCAQCiBAAhogMBAKEEACGjAwEAoQQAIaQDAQChBAAhpQMBAKIEACED6AIAABYAIOkCAAAWACDqAgAAFgAgEQYAAJQEACDPAgAA2wQAMNACAAANABDRAgAA2wQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAhjAMBAKEEACGNAwEAoQQAIY4DAQCiBAAhjwMBAKIEACGQAwEAogQAIZEDQACwBAAhkgNAALAEACGTAwEAogQAIZQDAQCiBAAhDAYAAJQEACDPAgAA3AQAMNACAAAJABDRAgAA3AQAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAh_wJAAJMEACGVAwEAoQQAIZYDAQCiBAAhlwMBAKIEACESBQAA3gQAIAcAAN8EACAIAADgBAAgCwAA4QQAIAwAAOIEACARAADRBAAgEwAA4wQAIM8CAADdBAAw0AIAAAUAENECAADdBAAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh6wIBAKEEACHsAgIAowQAIYYDAQChBAAhpgMgAMYEACGnAwEAogQAIQgDAAC_BAAgzwIAAL4EADDQAgAAAwAQ0QIAAL4EADDiAgIAqQQAIYYDAQChBAAhqwMAAAMAIKwDAAADACAD6AIAAAkAIOkCAAAJACDqAgAACQAgA-gCAAANACDpAgAADQAg6gIAAA0AIAPoAgAAEQAg6QIAABEAIOoCAAARACAD6AIAABsAIOkCAAAbACDqAgAAGwAgCwYAAJQEACASAACVBAAgzwIAAJIEADDQAgAAOwAQ0QIAAJIEADDVAkAAkwQAIdYCQACTBAAh4gIBAKEEACHnAgEAoQQAIasDAAA7ACCsAwAAOwAgAAAAAAAFsAMCAAAAAbYDAgAAAAG3AwIAAAABuAMCAAAAAbkDAgAAAAEBsANAAAAAAQGwAwEAAAABBSMAAPoHACAkAACACAAgrQMAAPsHACCuAwAA_wcAILMDAAA0ACAFIwAA-AcAICQAAP0HACCtAwAA-QcAIK4DAAD8BwAgswMAACYAIAMjAAD6BwAgrQMAAPsHACCzAwAANAAgAyMAAPgHACCtAwAA-QcAILMDAAAmACAAAAAAAAGwAyAAAAABBSMAAPIHACAkAAD2BwAgrQMAAPMHACCuAwAA9QcAILMDAAC-AwAgCyMAAPgEADAkAAD9BAAwrQMAAPkEADCuAwAA-gQAMK8DAAD7BAAgsAMAAPwEADCxAwAA_AQAMLIDAAD8BAAwswMAAPwEADC0AwAA_gQAMLUDAAD_BAAwBRUAAO8EACDTAgEAAAAB1AICAAAAAdUCQAAAAAHWAkAAAAABAgAAADAAICMAAIMFACADAAAAMAAgIwAAgwUAICQAAIIFACABHAAA9AcAMAsUAADLBAAgFQAAzAQAIM8CAADKBAAw0AIAAC4AENECAADKBAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIagDAADJBAAgAgAAADAAIBwAAIIFACACAAAAgAUAIBwAAIEFACAIzwIAAP8EADDQAgAAgAUAENECAAD_BAAw0gIBAKEEACHTAgEAoQQAIdQCAgCpBAAh1QJAAJMEACHWAkAAkwQAIQjPAgAA_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-BQAw0AIAAL8FABDRAgAAvgUAMOICAQChBAAh-wICAKkEACGGAwEAoQQAIYgDAQChBAAhiQMCAKkEACGKAwIAqQQAIQXiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYoDAgDpBAAhCQ4AAMIFACAQAADDBQAgEQAAxAUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYoDAgDpBAAhBSMAANcHACAkAADkBwAgrQMAANgHACCuAwAA4wcAILMDAACpAgAgBSMAANUHACAkAADhBwAgrQMAANYHACCuAwAA4AcAILMDAAD6AQAgCyMAAM8FADAkAADUBQAwrQMAANAFADCuAwAA0QUAMK8DAADSBQAgsAMAANMFADCxAwAA0wUAMLIDAADTBQAwswMAANMFADC0AwAA1QUAMLUDAADWBQAwCyMAAMYFADAkAADKBQAwrQMAAMcFADCuAwAAyAUAMK8DAADJBQAgsAMAAPwEADCxAwAA_AQAMLIDAAD8BAAwswMAAPwEADC0AwAAywUAMLUDAAD_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_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_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABAgAAAAsAICMAAPUGACADAAAACwAgIwAA9QYAICQAAPQGACABHAAAwgcAMAwGAACUBAAgzwIAANwEADDQAgAACQAQ0QIAANwEADDVAkAAkwQAIdYCQACTBAAh4gIBAAAAAecCAQChBAAh_wJAAJMEACGVAwEAAAABlgMBAKIEACGXAwEAogQAIQIAAAALACAcAAD0BgAgAgAAAPIGACAcAADzBgAgC88CAADxBgAw0AIAAPIGABDRAgAA8QYAMNUCQACTBAAh1gJAAJMEACHiAgEAoQQAIecCAQChBAAh_wJAAJMEACGVAwEAoQQAIZYDAQCiBAAhlwMBAKIEACELzwIAAPEGADDQAgAA8gYAENECAADxBgAw1QJAAJMEACHWAkAAkwQAIeICAQChBAAh5wIBAKEEACH_AkAAkwQAIZUDAQChBAAhlgMBAKIEACGXAwEAogQAIQfVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQfVAkAA6gQAIdYCQADqBAAh4gIBAOsEACH_AkAA6gQAIZUDAQDrBAAhlgMBAKEFACGXAwEAoQUAIQfVAkAAAAAB1gJAAAAAAeICAQAAAAH_AkAAAAABlQMBAAAAAZYDAQAAAAGXAwEAAAABDQcAAPcGACAIAAD4BgAgCwAA-QYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAGGAwEAAAABpgMgAAAAAacDAQAAAAEEIwAA6gYAMK0DAADrBgAwrwMAAO0GACCzAwAA7gYAMAQjAADeBgAwrQMAAN8GADCvAwAA4QYAILMDAADiBgAwBCMAAMQGADCtAwAAxQYAMK8DAADHBgAgswMAAMgGADAEIwAAuAYAMK0DAAC5BgAwrwMAALsGACCzAwAAvAYAMAQjAACvBgAwrQMAALAGADCvAwAAsgYAILMDAADTBQAwAyMAAKoGACCtAwAAqwYAILMDAAC-AwAgBCMAAJkGADCtAwAAmgYAMK8DAACcBgAgswMAAJ0GADAAAAAABSMAAL0HACAkAADABwAgrQMAAL4HACCuAwAAvwcAILMDAAABACADIwAAvQcAIK0DAAC-BwAgswMAAAEAIAAAAAUjAAC4BwAgJAAAuwcAIK0DAAC5BwAgrgMAALoHACCzAwAAAQAgAyMAALgHACCtAwAAuQcAILMDAAABACAAAAAFIwAAswcAICQAALYHACCtAwAAtAcAIK4DAAC1BwAgswMAABMAIAMjAACzBwAgrQMAALQHACCzAwAAEwAgAAAAByMAAK4HACAkAACxBwAgrQMAAK8HACCuAwAAsAcAILEDAAAFACCyAwAABQAgswMAAAEAIAMjAACuBwAgrQMAAK8HACCzAwAAAQAgAAAAAAAHIwAAqQcAICQAAKwHACCtAwAAqgcAIK4DAACrBwAgsQMAAAMAILIDAAADACCzAwAAswEAIAMjAACpBwAgrQMAAKoHACCzAwAAswEAIAIGAACZBQAgEgAAmgUAIAACDQAAmwcAIBMAAJoHACAFDgAAngcAIA8AAJ8HACAQAACgBwAgEQAAoQcAIBYAAJsHACABDQAA4wUAIAENAADjBQAgAQ0AAOMFACAABQYAAJkFACAKAACjBwAg5wIAAJsFACDrAgAAmwUAIKUDAACbBQAgAAEDAAD-BgAgAAAAAALiAgIAAAABhgMBAAAAAQIAAACzAQAgIwAAqQcAIAMAAAADACAjAACpBwAgJAAArQcAIAQAAAADACAcAACtBwAg4gICAOkEACGGAwEA6wQAIQLiAgIA6QQAIYYDAQDrBAAhDgUAAJkHACAHAAD3BgAgCAAA-AYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhgMBAAAAAaYDIAAAAAGnAwEAAAABAgAAAAEAICMAAK4HACADAAAABQAgIwAArgcAICQAALIHACAQAAAABQAgBQAAmAcAIAcAAKQGACAIAAClBgAgDAAApwYAIBEAAKgGACATAACpBgAgHAAAsgcAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEOBQAAmAcAIAcAAKQGACAIAAClBgAgDAAApwYAIBEAAKgGACATAACpBgAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQgGAACSBwAg4gIBAAAAAecCAQAAAAHrAgEAAAABogMBAAAAAaMDAQAAAAGkAwEAAAABpQMBAAAAAQIAAAATACAjAACzBwAgAwAAABEAICMAALMHACAkAAC3BwAgCgAAABEAIAYAAJEHACAcAAC3BwAg4gIBAOsEACHnAgEAoQUAIesCAQChBQAhogMBAOsEACGjAwEA6wQAIaQDAQDrBAAhpQMBAKEFACEIBgAAkQcAIOICAQDrBAAh5wIBAKEFACHrAgEAoQUAIaIDAQDrBAAhowMBAOsEACGkAwEA6wQAIaUDAQChBQAhDgUAAJkHACAIAAD4BgAgCwAA-QYAIAwAAPoGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhgMBAAAAAaYDIAAAAAGnAwEAAAABAgAAAAEAICMAALgHACADAAAABQAgIwAAuAcAICQAALwHACAQAAAABQAgBQAAmAcAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACATAACpBgAgHAAAvAcAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEOBQAAmAcAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACATAACpBgAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQ4FAACZBwAgBwAA9wYAIAsAAPkGACAMAAD6BgAgEQAA-wYAIBMAAPwGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAAB7AICAAAAAYYDAQAAAAGmAyAAAAABpwMBAAAAAQIAAAABACAjAAC9BwAgAwAAAAUAICMAAL0HACAkAADBBwAgEAAAAAUAIAUAAJgHACAHAACkBgAgCwAApgYAIAwAAKcGACARAACoBgAgEwAAqQYAIBwAAMEHACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHrAgEA6wQAIewCAgCiBQAhhgMBAOsEACGmAyAA9QQAIacDAQChBQAhDgUAAJgHACAHAACkBgAgCwAApgYAIAwAAKcGACARAACoBgAgEwAAqQYAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEH1QJAAAAAAdYCQAAAAAHiAgEAAAAB_wJAAAAAAZUDAQAAAAGWAwEAAAABlwMBAAAAAQzVAkAAAAAB1gJAAAAAAeICAQAAAAGMAwEAAAABjQMBAAAAAY4DAQAAAAGPAwEAAAABkAMBAAAAAZEDQAAAAAGSA0AAAAABkwMBAAAAAZQDAQAAAAEK4gIBAAAAAZkDAQAAAAGaAwEAAAABmwMBAAAAAZwDAQAAAAGdAwEAAAABngMBAAAAAZ8DIAAAAAGgAyAAAAABoQMgAAAAAQbiAgEAAAAB6wIBAAAAAaIDAQAAAAGjAwEAAAABpAMBAAAAAaUDAQAAAAEI1QJAAAAAAeICAQAAAAHrAgEAAAABgQMBAAAAAYIDIAAAAAGDA0AAAAABhAMBAAAAAYUDAQAAAAED0wIBAAAAAdUCQAAAAAHWAkAAAAABB9UCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAGGAwEAAAABpgMgAAAAAacDAQAAAAEF4gIBAAAAAfsCAgAAAAGGAwEAAAABiAMBAAAAAYkDAgAAAAEKDgAA3gUAIA8AAPQFACAQAADfBQAgFgAA4QUAIOICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQIAAAAmACAjAADKBwAgAwAAACQAICMAAMoHACAkAADOBwAgDAAAACQAIA4AAMIFACAPAADyBQAgEAAAwwUAIBYAAMUFACAcAADOBwAg4gIBAOsEACH7AgIA6QQAIYYDAQDrBAAhiAMBAOsEACGJAwIA6QQAIYoDAgDpBAAhCg4AAMIFACAPAADyBQAgEAAAwwUAIBYAAMUFACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACED1AICAAAAAeICAgAAAAGGAwEAAAABAgAAAMICACAjAADPBwAgAwAAAMUCACAjAADPBwAgJAAA0wcAIAUAAADFAgAgHAAA0wcAINQCAgDpBAAh4gICAOkEACGGAwEA6wQAIQPUAgIA6QQAIeICAgDpBAAhhgMBAOsEACEF4gIBAAAAAYYDAQAAAAGIAwEAAAABiQMCAAAAAYoDAgAAAAED1AICAAAAAeICAgAAAAGGAwEAAAABAgAAAPoBACAjAADVBwAgA-ICAgAAAAGGAwEAAAABhwMCAAAAAQIAAACpAgAgIwAA1wcAIA4FAACZBwAgBwAA9wYAIAgAAPgGACALAAD5BgAgDAAA-gYAIBMAAPwGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAAB7AICAAAAAYYDAQAAAAGmAyAAAAABpwMBAAAAAQIAAAABACAjAADZBwAgAwAAAAUAICMAANkHACAkAADdBwAgEAAAAAUAIAUAAJgHACAHAACkBgAgCAAApQYAIAsAAKYGACAMAACnBgAgEwAAqQYAIBwAAN0HACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHrAgEA6wQAIewCAgCiBQAhhgMBAOsEACGmAyAA9QQAIacDAQChBQAhDgUAAJgHACAHAACkBgAgCAAApQYAIAsAAKYGACAMAACnBgAgEwAAqQYAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACED1QJAAAAAAdYCQAAAAAHnAgEAAAABBNICAQAAAAHUAgIAAAAB1QJAAAAAAdYCQAAAAAEDAAAA_QEAICMAANUHACAkAADiBwAgBQAAAP0BACAcAADiBwAg1AICAOkEACHiAgIA6QQAIYYDAQDrBAAhA9QCAgDpBAAh4gICAOkEACGGAwEA6wQAIQMAAACsAgAgIwAA1wcAICQAAOUHACAFAAAArAIAIBwAAOUHACDiAgIA6QQAIYYDAQDrBAAhhwMCAOkEACED4gICAOkEACGGAwEA6wQAIYcDAgDpBAAhBeICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGKAwIAAAABDgUAAJkHACAHAAD3BgAgCAAA-AYAIAsAAPkGACARAAD7BgAgEwAA_AYAINUCQAAAAAHWAkAAAAAB4gIBAAAAAesCAQAAAAHsAgIAAAABhgMBAAAAAaYDIAAAAAGnAwEAAAABAgAAAAEAICMAAOcHACADAAAABQAgIwAA5wcAICQAAOsHACAQAAAABQAgBQAAmAcAIAcAAKQGACAIAAClBgAgCwAApgYAIBEAAKgGACATAACpBgAgHAAA6wcAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIesCAQDrBAAh7AICAKIFACGGAwEA6wQAIaYDIAD1BAAhpwMBAKEFACEOBQAAmAcAIAcAAKQGACAIAAClBgAgCwAApgYAIBEAAKgGACATAACpBgAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQ4FAACZBwAgBwAA9wYAIAgAAPgGACALAAD5BgAgDAAA-gYAIBEAAPsGACDVAkAAAAAB1gJAAAAAAeICAQAAAAHrAgEAAAAB7AICAAAAAYYDAQAAAAGmAyAAAAABpwMBAAAAAQIAAAABACAjAADsBwAgBtUCQAAAAAHWAkAAAAAB4gIBAAAAAeQCAgAAAAHlAgIAAAAB5gIgAAAAAQMAAAAFACAjAADsBwAgJAAA8QcAIBAAAAAFACAFAACYBwAgBwAApAYAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACAcAADxBwAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh6wIBAOsEACHsAgIAogUAIYYDAQDrBAAhpgMgAPUEACGnAwEAoQUAIQ4FAACYBwAgBwAApAYAIAgAAKUGACALAACmBgAgDAAApwYAIBEAAKgGACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHrAgEA6wQAIewCAgCiBQAhhgMBAOsEACGmAyAA9QQAIacDAQChBQAhBQYAAJcFACDVAkAAAAAB1gJAAAAAAeICAQAAAAHnAgEAAAABAgAAAL4DACAjAADyBwAgBNMCAQAAAAHUAgIAAAAB1QJAAAAAAdYCQAAAAAEDAAAAOwAgIwAA8gcAICQAAPcHACAHAAAAOwAgBgAAiQUAIBwAAPcHACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHnAgEA6wQAIQUGAACJBQAg1QJAAOoEACHWAkAA6gQAIeICAQDrBAAh5wIBAOsEACEKDgAA3gUAIA8AAPQFACAQAADfBQAgEQAA4AUAIOICAQAAAAH7AgIAAAABhgMBAAAAAYgDAQAAAAGJAwIAAAABigMCAAAAAQIAAAAmACAjAAD4BwAgCBMAAIQFACDVAkAAAAAB1gJAAAAAAeICAQAAAAHjAgEAAAAB5AICAAAAAeUCAgAAAAHmAiAAAAABAgAAADQAICMAAPoHACADAAAAJAAgIwAA-AcAICQAAP4HACAMAAAAJAAgDgAAwgUAIA8AAPIFACAQAADDBQAgEQAAxAUAIBwAAP4HACDiAgEA6wQAIfsCAgDpBAAhhgMBAOsEACGIAwEA6wQAIYkDAgDpBAAhigMCAOkEACEKDgAAwgUAIA8AAPIFACAQAADDBQAgEQAAxAUAIOICAQDrBAAh-wICAOkEACGGAwEA6wQAIYgDAQDrBAAhiQMCAOkEACGKAwIA6QQAIQMAAAAyACAjAAD6BwAgJAAAgQgAIAoAAAAyACATAAD2BAAgHAAAgQgAINUCQADqBAAh1gJAAOoEACHiAgEA6wQAIeMCAQDrBAAh5AICAOkEACHlAgIA6QQAIeYCIAD1BAAhCBMAAPYEACDVAkAA6gQAIdYCQADqBAAh4gIBAOsEACHjAgEA6wQAIeQCAgDpBAAh5QICAOkEACHmAiAA9QQAIQgEABgFBAIHDAQIEAULFAYMHgkRIwoTPBQCAwcBBAADAQMIAAEGAAEBBgABAwQACAYVAQoZBwEJAAYBChoAAQYfAQIGAAEVAAsGBAAXDgAMDwAOEAAQES0KFjESAgQADQ0nCwENKAACBAAPDSkLAQ0qAAIEABENKwsBDSwAAhQAExUACwMEABYNNxITABQDBAAVBgABEjUTARI2AAENOAACETkAFjoABQc9AAg-AAs_AAxAABFBAAABBUsCAQVRAgUEAB0pAB4qAB8rACAsACEAAAAAAAUEAB0pAB4qAB8rACAsACEBBmMBAQZpAQMEACYrACcsACgAAAADBAAmKwAnLAAoAQkABgEJAAYDBAAtKwAuLAAvAAAAAwQALSsALiwALwEGAAEBBgABAwQANCsANSwANgAAAAMEADQrADUsADYBBgABAQYAAQMEADsrADwsAD0AAAADBAA7KwA8LAA9AAAFBABCKQBDKgBEKwBFLABGAAAAAAAFBABCKQBDKgBEKwBFLABGAAAAAwQATCsATSwATgAAAAMEAEwrAE0sAE4DDgAMDwAOEAAQAw4ADA8ADhAAEAUEAFMpAFQqAFUrAFYsAFcAAAAAAAUEAFMpAFQqAFUrAFYsAFcAAAUEAFwpAF0qAF4rAF8sAGAAAAAAAAUEAFwpAF0qAF4rAF8sAGACBgABFQALAgYAARUACwMEAGUrAGYsAGcAAAADBABlKwBmLABnAAAFBABsKQBtKgBuKwBvLABwAAAAAAAFBABsKQBtKgBuKwBvLABwAAAFBAB1KQB2KgB3KwB4LAB5AAAAAAAFBAB1KQB2KgB3KwB4LAB5AQblAgEBBusCAQMEAH4rAH8sAIABAAAAAwQAfisAfywAgAEAAAADBACGASsAhwEsAIgBAAAAAwQAhgErAIcBLACIAQAAAAUEAI4BKQCPASoAkAErAJEBLACSAQAAAAAABQQAjgEpAI8BKgCQASsAkQEsAJIBAAAABQQAmAEpAJkBKgCaASsAmwEsAJwBAAAAAAAFBACYASkAmQEqAJoBKwCbASwAnAEBBgABAQYAAQMEAKEBKwCiASwAowEAAAADBAChASsAogEsAKMBARMAFAETABQFBACoASkAqQEqAKoBKwCrASwArAEAAAAAAAUEAKgBKQCpASoAqgErAKsBLACsAQIUABMVAAsCFAATFQALBQQAsQEpALIBKgCzASsAtAEsALUBAAAAAAAFBACxASkAsgEqALMBKwC0ASwAtQEXAgEYQgEZQwEaRAEbRQEdRwEeSRkfShogTQEhTxkiUBslUgEmUwEnVBktVxwuWCIvWQYwWgYxWwYyXAYzXQY0XwY1YRk2YiM3ZQY4Zxk5aCQ6agY7awY8bBk9byU-cCk_cQdAcgdBcwdCdAdDdQdEdwdFeRlGeipHfAdIfhlJfytKgAEHS4EBB0yCARlNhQEsToYBME-HAQRQiAEEUYkBBFKKAQRTiwEEVI0BBFWPARlWkAExV5IBBFiUARlZlQEyWpYBBFuXAQRcmAEZXZsBM16cATdfnQEFYJ4BBWGfAQVioAEFY6EBBWSjAQVlpQEZZqYBOGeoAQVoqgEZaasBOWqsAQVrrQEFbK4BGW2xATpusgE-b7QBAnC1AQJxtwECcrgBAnO5AQJ0uwECdb0BGXa-AT93wAECeMIBGXnDAUB6xAECe8UBAnzGARl9yQFBfsoBR3_MAUiAAc0BSIEB0AFIggHRAUiDAdIBSIQB1AFIhQHWARmGAdcBSYcB2QFIiAHbARmJAdwBSooB3QFIiwHeAUiMAd8BGY0B4gFLjgHjAU-PAeQBC5AB5QELkQHmAQuSAecBC5MB6AELlAHqAQuVAewBGZYB7QFQlwHvAQuYAfEBGZkB8gFRmgHzAQubAfQBC5wB9QEZnQH4AVKeAfkBWJ8B-wEQoAH8ARChAf8BEKIBgAIQowGBAhCkAYMCEKUBhQIZpgGGAlmnAYgCEKgBigIZqQGLAlqqAYwCEKsBjQIQrAGOAhmtAZECW64BkgJhrwGTAgqwAZQCCrEBlQIKsgGWAgqzAZcCCrQBmQIKtQGbAhm2AZwCYrcBngIKuAGgAhm5AaECY7oBogIKuwGjAgq8AaQCGb0BpwJkvgGoAmi_AaoCDMABqwIMwQGuAgzCAa8CDMMBsAIMxAGyAgzFAbQCGcYBtQJpxwG3AgzIAbkCGckBugJqygG7AgzLAbwCDMwBvQIZzQHAAmvOAcECcc8BwwIO0AHEAg7RAccCDtIByAIO0wHJAg7UAcsCDtUBzQIZ1gHOAnLXAdACDtgB0gIZ2QHTAnPaAdQCDtsB1QIO3AHWAhndAdkCdN4B2gJ63wHbAgngAdwCCeEB3QIJ4gHeAgnjAd8CCeQB4QIJ5QHjAhnmAeQCe-cB5wIJ6AHpAhnpAeoCfOoB7AIJ6wHtAgnsAe4CGe0B8QJ97gHyAoEB7wH0AoIB8AH1AoIB8QH4AoIB8gH5AoIB8wH6AoIB9AH8AoIB9QH-Ahn2Af8CgwH3AYEDggH4AYMDGfkBhAOEAfoBhQOCAfsBhgOCAfwBhwMZ_QGKA4UB_gGLA4kB_wGNA4oBgAKOA4oBgQKRA4oBggKSA4oBgwKTA4oBhAKVA4oBhQKXAxmGApgDiwGHApoDigGIApwDGYkCnQOMAYoCngOKAYsCnwOKAYwCoAMZjQKjA40BjgKkA5MBjwKmA5QBkAKnA5QBkQKqA5QBkgKrA5QBkwKsA5QBlAKuA5QBlQKwAxmWArEDlQGXArMDlAGYArUDGZkCtgOWAZoCtwOUAZsCuAOUAZwCuQMZnQK8A5cBngK9A50BnwK_AxSgAsADFKECwgMUogLDAxSjAsQDFKQCxgMUpQLIAxmmAskDngGnAssDFKgCzQMZqQLOA58BqgLPAxSrAtADFKwC0QMZrQLUA6ABrgLVA6QBrwLWAxOwAtcDE7EC2AMTsgLZAxOzAtoDE7QC3AMTtQLeAxm2At8DpQG3AuEDE7gC4wMZuQLkA6YBugLlAxO7AuYDE7wC5wMZvQLqA6cBvgLrA60BvwLsAxLAAu0DEsEC7gMSwgLvAxLDAvADEsQC8gMSxQL0AxnGAvUDrgHHAvcDEsgC-QMZyQL6A68BygL7AxLLAvwDEswC_QMZzQKABLABzgKBBLYB" + strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"users\",\"_count\",\"role\",\"user\",\"sessions\",\"accounts\",\"profile\",\"addresses\",\"profiles\",\"licenseKey\",\"schedule\",\"block\",\"proclamations\",\"solfeggioFrequency\",\"category\",\"group\",\"proclamation\",\"userProclamations\",\"blockProclamations\",\"blocks\",\"licenseKeys\",\"schedules\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Profile.findUnique\",\"Profile.findUniqueOrThrow\",\"Profile.findFirst\",\"Profile.findFirstOrThrow\",\"Profile.findMany\",\"Profile.createOne\",\"Profile.createMany\",\"Profile.createManyAndReturn\",\"Profile.updateOne\",\"Profile.updateMany\",\"Profile.updateManyAndReturn\",\"Profile.upsertOne\",\"Profile.deleteOne\",\"Profile.deleteMany\",\"Profile.groupBy\",\"Profile.aggregate\",\"Address.findUnique\",\"Address.findUniqueOrThrow\",\"Address.findFirst\",\"Address.findFirstOrThrow\",\"Address.findMany\",\"Address.createOne\",\"Address.createMany\",\"Address.createManyAndReturn\",\"Address.updateOne\",\"Address.updateMany\",\"Address.updateManyAndReturn\",\"Address.upsertOne\",\"Address.deleteOne\",\"Address.deleteMany\",\"Address.groupBy\",\"Address.aggregate\",\"Session.findUnique\",\"Session.findUniqueOrThrow\",\"Session.findFirst\",\"Session.findFirstOrThrow\",\"Session.findMany\",\"Session.createOne\",\"Session.createMany\",\"Session.createManyAndReturn\",\"Session.updateOne\",\"Session.updateMany\",\"Session.updateManyAndReturn\",\"Session.upsertOne\",\"Session.deleteOne\",\"Session.deleteMany\",\"Session.groupBy\",\"Session.aggregate\",\"Account.findUnique\",\"Account.findUniqueOrThrow\",\"Account.findFirst\",\"Account.findFirstOrThrow\",\"Account.findMany\",\"Account.createOne\",\"Account.createMany\",\"Account.createManyAndReturn\",\"Account.updateOne\",\"Account.updateMany\",\"Account.updateManyAndReturn\",\"Account.upsertOne\",\"Account.deleteOne\",\"Account.deleteMany\",\"Account.groupBy\",\"Account.aggregate\",\"UserRole.findUnique\",\"UserRole.findUniqueOrThrow\",\"UserRole.findFirst\",\"UserRole.findFirstOrThrow\",\"UserRole.findMany\",\"UserRole.createOne\",\"UserRole.createMany\",\"UserRole.createManyAndReturn\",\"UserRole.updateOne\",\"UserRole.updateMany\",\"UserRole.updateManyAndReturn\",\"UserRole.upsertOne\",\"UserRole.deleteOne\",\"UserRole.deleteMany\",\"UserRole.groupBy\",\"UserRole.aggregate\",\"Verification.findUnique\",\"Verification.findUniqueOrThrow\",\"Verification.findFirst\",\"Verification.findFirstOrThrow\",\"Verification.findMany\",\"Verification.createOne\",\"Verification.createMany\",\"Verification.createManyAndReturn\",\"Verification.updateOne\",\"Verification.updateMany\",\"Verification.updateManyAndReturn\",\"Verification.upsertOne\",\"Verification.deleteOne\",\"Verification.deleteMany\",\"Verification.groupBy\",\"Verification.aggregate\",\"Proclamation.findUnique\",\"Proclamation.findUniqueOrThrow\",\"Proclamation.findFirst\",\"Proclamation.findFirstOrThrow\",\"Proclamation.findMany\",\"Proclamation.createOne\",\"Proclamation.createMany\",\"Proclamation.createManyAndReturn\",\"Proclamation.updateOne\",\"Proclamation.updateMany\",\"Proclamation.updateManyAndReturn\",\"Proclamation.upsertOne\",\"Proclamation.deleteOne\",\"Proclamation.deleteMany\",\"Proclamation.groupBy\",\"Proclamation.aggregate\",\"ProclamationGroup.findUnique\",\"ProclamationGroup.findUniqueOrThrow\",\"ProclamationGroup.findFirst\",\"ProclamationGroup.findFirstOrThrow\",\"ProclamationGroup.findMany\",\"ProclamationGroup.createOne\",\"ProclamationGroup.createMany\",\"ProclamationGroup.createManyAndReturn\",\"ProclamationGroup.updateOne\",\"ProclamationGroup.updateMany\",\"ProclamationGroup.updateManyAndReturn\",\"ProclamationGroup.upsertOne\",\"ProclamationGroup.deleteOne\",\"ProclamationGroup.deleteMany\",\"ProclamationGroup.groupBy\",\"ProclamationGroup.aggregate\",\"UserProclamations.findUnique\",\"UserProclamations.findUniqueOrThrow\",\"UserProclamations.findFirst\",\"UserProclamations.findFirstOrThrow\",\"UserProclamations.findMany\",\"UserProclamations.createOne\",\"UserProclamations.createMany\",\"UserProclamations.createManyAndReturn\",\"UserProclamations.updateOne\",\"UserProclamations.updateMany\",\"UserProclamations.updateManyAndReturn\",\"UserProclamations.upsertOne\",\"UserProclamations.deleteOne\",\"UserProclamations.deleteMany\",\"UserProclamations.groupBy\",\"UserProclamations.aggregate\",\"SolfeggioFrequency.findUnique\",\"SolfeggioFrequency.findUniqueOrThrow\",\"SolfeggioFrequency.findFirst\",\"SolfeggioFrequency.findFirstOrThrow\",\"SolfeggioFrequency.findMany\",\"SolfeggioFrequency.createOne\",\"SolfeggioFrequency.createMany\",\"SolfeggioFrequency.createManyAndReturn\",\"SolfeggioFrequency.updateOne\",\"SolfeggioFrequency.updateMany\",\"SolfeggioFrequency.updateManyAndReturn\",\"SolfeggioFrequency.upsertOne\",\"SolfeggioFrequency.deleteOne\",\"SolfeggioFrequency.deleteMany\",\"SolfeggioFrequency.groupBy\",\"SolfeggioFrequency.aggregate\",\"ProclamationCategory.findUnique\",\"ProclamationCategory.findUniqueOrThrow\",\"ProclamationCategory.findFirst\",\"ProclamationCategory.findFirstOrThrow\",\"ProclamationCategory.findMany\",\"ProclamationCategory.createOne\",\"ProclamationCategory.createMany\",\"ProclamationCategory.createManyAndReturn\",\"ProclamationCategory.updateOne\",\"ProclamationCategory.updateMany\",\"ProclamationCategory.updateManyAndReturn\",\"ProclamationCategory.upsertOne\",\"ProclamationCategory.deleteOne\",\"ProclamationCategory.deleteMany\",\"ProclamationCategory.groupBy\",\"ProclamationCategory.aggregate\",\"LicenseKey.findUnique\",\"LicenseKey.findUniqueOrThrow\",\"LicenseKey.findFirst\",\"LicenseKey.findFirstOrThrow\",\"LicenseKey.findMany\",\"LicenseKey.createOne\",\"LicenseKey.createMany\",\"LicenseKey.createManyAndReturn\",\"LicenseKey.updateOne\",\"LicenseKey.updateMany\",\"LicenseKey.updateManyAndReturn\",\"LicenseKey.upsertOne\",\"LicenseKey.deleteOne\",\"LicenseKey.deleteMany\",\"LicenseKey.groupBy\",\"LicenseKey.aggregate\",\"ApiKey.findUnique\",\"ApiKey.findUniqueOrThrow\",\"ApiKey.findFirst\",\"ApiKey.findFirstOrThrow\",\"ApiKey.findMany\",\"ApiKey.createOne\",\"ApiKey.createMany\",\"ApiKey.createManyAndReturn\",\"ApiKey.updateOne\",\"ApiKey.updateMany\",\"ApiKey.updateManyAndReturn\",\"ApiKey.upsertOne\",\"ApiKey.deleteOne\",\"ApiKey.deleteMany\",\"ApiKey.groupBy\",\"ApiKey.aggregate\",\"ColorPalette.findUnique\",\"ColorPalette.findUniqueOrThrow\",\"ColorPalette.findFirst\",\"ColorPalette.findFirstOrThrow\",\"ColorPalette.findMany\",\"ColorPalette.createOne\",\"ColorPalette.createMany\",\"ColorPalette.createManyAndReturn\",\"ColorPalette.updateOne\",\"ColorPalette.updateMany\",\"ColorPalette.updateManyAndReturn\",\"ColorPalette.upsertOne\",\"ColorPalette.deleteOne\",\"ColorPalette.deleteMany\",\"ColorPalette.groupBy\",\"ColorPalette.aggregate\",\"AuditLog.findUnique\",\"AuditLog.findUniqueOrThrow\",\"AuditLog.findFirst\",\"AuditLog.findFirstOrThrow\",\"AuditLog.findMany\",\"AuditLog.createOne\",\"AuditLog.createMany\",\"AuditLog.createManyAndReturn\",\"AuditLog.updateOne\",\"AuditLog.updateMany\",\"AuditLog.updateManyAndReturn\",\"AuditLog.upsertOne\",\"AuditLog.deleteOne\",\"AuditLog.deleteMany\",\"AuditLog.groupBy\",\"AuditLog.aggregate\",\"Schedule.findUnique\",\"Schedule.findUniqueOrThrow\",\"Schedule.findFirst\",\"Schedule.findFirstOrThrow\",\"Schedule.findMany\",\"Schedule.createOne\",\"Schedule.createMany\",\"Schedule.createManyAndReturn\",\"Schedule.updateOne\",\"Schedule.updateMany\",\"Schedule.updateManyAndReturn\",\"Schedule.upsertOne\",\"Schedule.deleteOne\",\"Schedule.deleteMany\",\"Schedule.groupBy\",\"Schedule.aggregate\",\"Block.findUnique\",\"Block.findUniqueOrThrow\",\"Block.findFirst\",\"Block.findFirstOrThrow\",\"Block.findMany\",\"Block.createOne\",\"Block.createMany\",\"Block.createManyAndReturn\",\"Block.updateOne\",\"Block.updateMany\",\"Block.updateManyAndReturn\",\"Block.upsertOne\",\"Block.deleteOne\",\"Block.deleteMany\",\"Block.groupBy\",\"Block.aggregate\",\"BlockProclamations.findUnique\",\"BlockProclamations.findUniqueOrThrow\",\"BlockProclamations.findFirst\",\"BlockProclamations.findFirstOrThrow\",\"BlockProclamations.findMany\",\"BlockProclamations.createOne\",\"BlockProclamations.createMany\",\"BlockProclamations.createManyAndReturn\",\"BlockProclamations.updateOne\",\"BlockProclamations.updateMany\",\"BlockProclamations.updateManyAndReturn\",\"BlockProclamations.upsertOne\",\"BlockProclamations.deleteOne\",\"BlockProclamations.deleteMany\",\"BlockProclamations.groupBy\",\"BlockProclamations.aggregate\",\"AND\",\"OR\",\"NOT\",\"blockId\",\"proclamationId\",\"sortOrder\",\"createdAt\",\"updatedAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"contains\",\"startsWith\",\"endsWith\",\"id\",\"scheduleId\",\"day\",\"startMinutes\",\"endMinutes\",\"dynamicAudio\",\"licenseKeyId\",\"userId\",\"type\",\"email\",\"roleId\",\"roleName\",\"page\",\"action\",\"entity\",\"entityId\",\"before\",\"after\",\"ip\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"solfeggioFrequencyId\",\"colors\",\"keyPrefix\",\"keyHash\",\"expiresAt\",\"revokedAt\",\"key\",\"active\",\"lastValidatedAt\",\"systemName\",\"systemNameLower\",\"name\",\"every\",\"some\",\"none\",\"value\",\"content\",\"categoryId\",\"groupId\",\"identifier\",\"accountId\",\"providerId\",\"accessToken\",\"refreshToken\",\"idToken\",\"accessTokenExpiresAt\",\"refreshTokenExpiresAt\",\"scope\",\"password\",\"token\",\"ipAddress\",\"userAgent\",\"profileId\",\"street\",\"city\",\"state\",\"postalCode\",\"country\",\"isDefaultMailing\",\"isDefaultBilling\",\"isDefaultShipping\",\"firstName\",\"lastName\",\"phoneNumber\",\"ghlContactId\",\"emailVerified\",\"image\",\"userId_proclamationId\",\"blockId_proclamationId\",\"userId_systemNameLower\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "owi2AbACEgUAAOgEACAHAADpBAAgCAAA6gQAIAsAAOsEACAUAADWBAAgFwAA7AQAIBgAAO0EACDRAgAA5wQAMNICAAAFABDTAgAA5wQAMNcCQACmBAAh2AJAAKYEACHkAgEAAAAB7QIBAAAAAe4CAgCoBAAhiAMBAKUEACGqAyAA3AQAIasDAQCnBAAhAQAAAAEAIAYDAADEBAAg0QIAAMMEADDSAgAAAwAQ0wIAAMMEADDkAgIArgQAIYgDAQClBAAhAQAAAAMAIBIFAADoBAAgBwAA6QQAIAgAAOoEACALAADrBAAgFAAA1gQAIBcAAOwEACAYAADtBAAg0QIAAOcEADDSAgAABQAQ0wIAAOcEADDXAkAApgQAIdgCQACmBAAh5AIBAKUEACHtAgEApQQAIe4CAgCoBAAhiAMBAKUEACGqAyAA3AQAIasDAQCnBAAhCQUAAL8HACAHAADABwAgCAAAwQcAIAsAAMIHACAUAAC5BwAgFwAAwwcAIBgAAMQHACDuAgAA-gQAIKsDAAD6BAAgAwAAAAUAIAEAAAYAMAIAAAEAIAEAAAAFACAMBgAA0AQAINECAADmBAAw0gIAAAkAENMCAADmBAAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh6wIBAKUEACGBA0AApgQAIZoDAQClBAAhmwMBAKcEACGcAwEApwQAIQMGAACzBwAgmwMAAPoEACCcAwAA-gQAIAwGAADQBAAg0QIAAOYEADDSAgAACQAQ0wIAAOYEADDXAkAApgQAIdgCQACmBAAh5AIBAAAAAesCAQClBAAhgQNAAKYEACGaAwEAAAABmwMBAKcEACGcAwEApwQAIQMAAAAJACABAAAKADACAAALACARBgAA0AQAINECAADlBAAw0gIAAA0AENMCAADlBAAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh6wIBAKUEACGRAwEApQQAIZIDAQClBAAhkwMBAKcEACGUAwEApwQAIZUDAQCnBAAhlgNAALUEACGXA0AAtQQAIZgDAQCnBAAhmQMBAKcEACEIBgAAswcAIJMDAAD6BAAglAMAAPoEACCVAwAA-gQAIJYDAAD6BAAglwMAAPoEACCYAwAA-gQAIJkDAAD6BAAgEQYAANAEACDRAgAA5QQAMNICAAANABDTAgAA5QQAMNcCQACmBAAh2AJAAKYEACHkAgEAAAAB6wIBAKUEACGRAwEApQQAIZIDAQClBAAhkwMBAKcEACGUAwEApwQAIZUDAQCnBAAhlgNAALUEACGXA0AAtQQAIZgDAQCnBAAhmQMBAKcEACEDAAAADQAgAQAADgAwAgAADwAgDAYAAMwEACAKAADkBAAg0QIAAOMEADDSAgAAEQAQ0wIAAOMEADDkAgEApQQAIesCAQCnBAAh7QIBAKcEACGmAwEApQQAIacDAQClBAAhqAMBAKUEACGpAwEApwQAIQUGAACzBwAgCgAAvgcAIOsCAAD6BAAg7QIAAPoEACCpAwAA-gQAIAwGAADMBAAgCgAA5AQAINECAADjBAAw0gIAABEAENMCAADjBAAw5AIBAAAAAesCAQCnBAAh7QIBAKcEACGmAwEApQQAIacDAQClBAAhqAMBAKUEACGpAwEApwQAIQMAAAARACABAAASADACAAATACABAAAABQAgDwkAAOIEACDRAgAA4QQAMNICAAAWABDTAgAA4QQAMOQCAQClBAAh7AIBAKUEACGdAwEApQQAIZ4DAQClBAAhnwMBAKUEACGgAwEApQQAIaEDAQClBAAhogMBAKUEACGjAyAA3AQAIaQDIADcBAAhpQMgANwEACEBCQAAvQcAIA8JAADiBAAg0QIAAOEEADDSAgAAFgAQ0wIAAOEEADDkAgEAAAAB7AIBAKUEACGdAwEApQQAIZ4DAQClBAAhnwMBAKUEACGgAwEApQQAIaEDAQClBAAhogMBAKUEACGjAyAA3AQAIaQDIADcBAAhpQMgANwEACEDAAAAFgAgAQAAFwAwAgAAGAAgAQAAABYAIA4GAADMBAAgDQAA4AQAINECAADfBAAw0gIAABsAENMCAADfBAAw1wJAAKYEACHkAgEApQQAIesCAQCnBAAh7QIBAKcEACGDAwEApQQAIYQDIADcBAAhhQNAALUEACGGAwEApwQAIYcDAQCnBAAhBwYAALMHACANAAC8BwAg6wIAAPoEACDtAgAA-gQAIIUDAAD6BAAghgMAAPoEACCHAwAA-gQAIA8GAADMBAAgDQAA4AQAINECAADfBAAw0gIAABsAENMCAADfBAAw1wJAAKYEACHkAgEAAAAB6wIBAKcEACHtAgEApwQAIYMDAQAAAAGEAyAA3AQAIYUDQAC1BAAhhgMBAKcEACGHAwEApwQAIa4DAADeBAAgAwAAABsAIAEAABwAMAIAAB0AIAEAAAAFACAMBgAAzAQAIAwAAMsEACAWAADNBAAg0QIAAMoEADDSAgAAIAAQ0wIAAMoEADDXAkAApgQAIdgCQACmBAAh5AIBAKUEACHqAgEApwQAIesCAQCnBAAh7AIBAKcEACEBAAAAIAAgAQAAABsAIAEAAAAFACANDQAA3QQAIA8AANcEACDRAgAA2wQAMNICAAAkABDTAgAA2wQAMNcCQACmBAAh2AJAAKYEACHkAgEApQQAIeUCAQClBAAh5gIBAKcEACHnAgIArgQAIegCAgCuBAAh6QIgANwEACEDDQAAvAcAIA8AALoHACDmAgAA-gQAIA0NAADdBAAgDwAA1wQAINECAADbBAAw0gIAACQAENMCAADbBAAw1wJAAKYEACHYAkAApgQAIeQCAQAAAAHlAgEApQQAIeYCAQCnBAAh5wICAK4EACHoAgIArgQAIekCIADcBAAhAwAAACQAIAEAACUAMAIAACYAIAoOAADaBAAgEwAA0QQAINECAADZBAAw0gIAACgAENMCAADZBAAw1AIBAKUEACHVAgEApQQAIdYCAgCuBAAh1wJAAKYEACHYAkAApgQAIQIOAAC7BwAgEwAAtQcAIAsOAADaBAAgEwAA0QQAINECAADZBAAw0gIAACgAENMCAADZBAAw1AIBAKUEACHVAgEApQQAIdYCAgCuBAAh1wJAAKYEACHYAkAApgQAIa0DAADYBAAgAwAAACgAIAEAACkAMAIAACoAIA4QAADTBAAgEQAA1AQAIBIAANUEACAUAADWBAAgFQAA1wQAINECAADSBAAw0gIAACwAENMCAADSBAAw5AIBAKUEACH9AgIArgQAIYgDAQClBAAhjQMBAKUEACGOAwIArgQAIY8DAgCuBAAhBRAAALYHACARAAC3BwAgEgAAuAcAIBQAALkHACAVAAC6BwAgDhAAANMEACARAADUBAAgEgAA1QQAIBQAANYEACAVAADXBAAg0QIAANIEADDSAgAALAAQ0wIAANIEADDkAgEAAAAB_QICAK4EACGIAwEAAAABjQMBAKUEACGOAwIArgQAIY8DAgCuBAAhAwAAACwAIAEAAC0AMAIAAC4AIAEAAAAsACADAAAALAAgAQAALQAwAgAALgAgAQAAACwAIAMAAAAsACABAAAtADACAAAuACABAAAALAAgCQYAANAEACATAADRBAAg0QIAAM8EADDSAgAANQAQ0wIAAM8EADDVAgEApQQAIdcCQACmBAAh2AJAAKYEACHrAgEApQQAIQIGAACzBwAgEwAAtQcAIAoGAADQBAAgEwAA0QQAINECAADPBAAw0gIAADUAENMCAADPBAAw1QIBAKUEACHXAkAApgQAIdgCQACmBAAh6wIBAKUEACGsAwAAzgQAIAMAAAA1ACABAAA2ADACAAA3ACADAAAAKAAgAQAAKQAwAgAAKgAgAQAAADUAIAEAAAAoACABAAAAKAAgAQAAACQAIAMAAAA1ACABAAA2ADACAAA3ACAGBgAAswcAIAwAALIHACAWAAC0BwAg6gIAAPoEACDrAgAA-gQAIOwCAAD6BAAgDAYAAMwEACAMAADLBAAgFgAAzQQAINECAADKBAAw0gIAACAAENMCAADKBAAw1wJAAKYEACHYAkAApgQAIeQCAQAAAAHqAgEAAAAB6wIBAKcEACHsAgEApwQAIQMAAAAgACABAAA_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-gQAIJQDAAD6BAAglQMAAPoEACCWAwAA-gQAIJcDAAD6BAAgmAMAAPoEACCZAwAA-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_QEAENMCAAC_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-gQAIO0CAAD6BAAghQMAAPoEACCGAwAA-gQAIIcDAAD6BAAgDNECAAC2BAAw0gIAAPYCABDTAgAAtgQAMNcCQACNBAAh5AIBAIsEACHrAgEAlgQAIe0CAQCWBAAhgwMBAIsEACGEAyAAlwQAIYUDQACxBAAhhgMBAJYEACGHAwEAlgQAIQMAAAAbACABAAD1AgAwKgAA9gIAIAMAAAAbACABAAAcADACAAAdACAJ0QIAALQEADDSAgAA_AIAENMCAAC0BAAw1wJAAKYEACHkAgEAAAAB_wIBAKUEACGAAwEAAAABgQNAALUEACGCA0AAtQQAIQEAAAD5AgAgAQAAAPkCACAJ0QIAALQEADDSAgAA_AIAENMCAAC0BAAw1wJAAKYEACHkAgEApQQAIf8CAQClBAAhgAMBAKUEACGBA0AAtQQAIYIDQAC1BAAhAoEDAAD6BAAgggMAAPoEACADAAAA_AIAIAEAAP0CADACAAD5AgAgAwAAAPwCACABAAD9AgAwAgAA-QIAIAMAAAD8AgAgAQAA_QIAMAIAAPkCACAG1wJAAAAAAeQCAQAAAAH_AgEAAAABgAMBAAAAAYEDQAAAAAGCA0AAAAABAR4AAIEDACAG1wJAAAAAAeQCAQAAAAH_AgEAAAABgAMBAAAAAYEDQAAAAAGCA0AAAAABAR4AAIMDADABHgAAgwMAMAbXAkAA9AQAIeQCAQD1BAAh_wIBAPUEACGAAwEA9QQAIYEDQAC1BQAhggNAALUFACECAAAA-QIAIB4AAIYDACAG1wJAAPQEACHkAgEA9QQAIf8CAQD1BAAhgAMBAPUEACGBA0AAtQUAIYIDQAC1BQAhAgAAAPwCACAeAACIAwAgAgAAAPwCACAeAACIAwAgAwAAAPkCACAlAACBAwAgJgAAhgMAIAEAAAD5AgAgAQAAAPwCACAFBAAAsgUAIC0AALQFACAuAACzBQAggQMAAPoEACCCAwAA-gQAIAnRAgAAsAQAMNICAACPAwAQ0wIAALAEADDXAkAAjQQAIeQCAQCLBAAh_wIBAIsEACGAAwEAiwQAIYEDQACxBAAhggNAALEEACEDAAAA_AIAIAEAAI4DADAqAACPAwAgAwAAAPwCACABAAD9AgAwAgAA-QIAIAXRAgAArQQAMNICAACVAwAQ0wIAAK0EADD9AgIAAAAB_gIAAK8EACABAAAAkgMAIAEAAACSAwAgBdECAACtBAAw0gIAAJUDABDTAgAArQQAMP0CAgCuBAAh_gIAAK8EACAAAwAAAJUDACABAACWAwAwAgAAkgMAIAMAAACVAwAgAQAAlgMAMAIAAJIDACADAAAAlQMAIAEAAJYDADACAACSAwAgAv0CAgAAAAH-AoAAAAABAR4AAJoDACAC_QICAAAAAf4CgAAAAAEBHgAAnAMAMAEeAACcAwAwAv0CAgDzBAAh_gKAAAAAAQIAAACSAwAgHgAAnwMAIAL9AgIA8wQAIf4CgAAAAAECAAAAlQMAIB4AAKEDACACAAAAlQMAIB4AAKEDACADAAAAkgMAICUAAJoDACAmAACfAwAgAQAAAJIDACABAAAAlQMAIAUEAACtBQAgKwAArgUAICwAALEFACAtAACwBQAgLgAArwUAIAXRAgAAqgQAMNICAACoAwAQ0wIAAKoEADD9AgIAjAQAIf4CAACrBAAgAwAAAJUDACABAACnAwAwKgAAqAMAIAMAAACVAwAgAQAAlgMAMAIAAJIDACAQ0QIAAKQEADDSAgAArgMAENMCAACkBAAw1wJAAKYEACHkAgEAAAAB6wIBAKcEACHtAgEApwQAIe4CAgCoBAAh7wIBAKcEACHwAgEApQQAIfECAQClBAAh8gIBAKcEACHzAgEApwQAIfQCAACpBAAg9QIAAKkEACD2AgEApwQAIQEAAACrAwAgAQAAAKsDACAQ0QIAAKQEADDSAgAArgMAENMCAACkBAAw1wJAAKYEACHkAgEApQQAIesCAQCnBAAh7QIBAKcEACHuAgIAqAQAIe8CAQCnBAAh8AIBAKUEACHxAgEApQQAIfICAQCnBAAh8wIBAKcEACH0AgAAqQQAIPUCAACpBAAg9gIBAKcEACEJ6wIAAPoEACDtAgAA-gQAIO4CAAD6BAAg7wIAAPoEACDyAgAA-gQAIPMCAAD6BAAg9AIAAPoEACD1AgAA-gQAIPYCAAD6BAAgAwAAAK4DACABAACvAwAwAgAAqwMAIAMAAACuAwAgAQAArwMAMAIAAKsDACADAAAArgMAIAEAAK8DADACAACrAwAgDdcCQAAAAAHkAgEAAAAB6wIBAAAAAe0CAQAAAAHuAgIAAAAB7wIBAAAAAfACAQAAAAHxAgEAAAAB8gIBAAAAAfMCAQAAAAH0AoAAAAAB9QKAAAAAAfYCAQAAAAEBHgAAswMAIA3XAkAAAAAB5AIBAAAAAesCAQAAAAHtAgEAAAAB7gICAAAAAe8CAQAAAAHwAgEAAAAB8QIBAAAAAfICAQAAAAHzAgEAAAAB9AKAAAAAAfUCgAAAAAH2AgEAAAABAR4AALUDADABHgAAtQMAMA3XAkAA9AQAIeQCAQD1BAAh6wIBAIAFACHtAgEAgAUAIe4CAgCsBQAh7wIBAIAFACHwAgEA9QQAIfECAQD1BAAh8gIBAIAFACHzAgEAgAUAIfQCgAAAAAH1AoAAAAAB9gIBAIAFACECAAAAqwMAIB4AALgDACAN1wJAAPQEACHkAgEA9QQAIesCAQCABQAh7QIBAIAFACHuAgIArAUAIe8CAQCABQAh8AIBAPUEACHxAgEA9QQAIfICAQCABQAh8wIBAIAFACH0AoAAAAAB9QKAAAAAAfYCAQCABQAhAgAAAK4DACAeAAC6AwAgAgAAAK4DACAeAAC6AwAgAwAAAKsDACAlAACzAwAgJgAAuAMAIAEAAACrAwAgAQAAAK4DACAOBAAApwUAICsAAKgFACAsAACrBQAgLQAAqgUAIC4AAKkFACDrAgAA-gQAIO0CAAD6BAAg7gIAAPoEACDvAgAA-gQAIPICAAD6BAAg8wIAAPoEACD0AgAA-gQAIPUCAAD6BAAg9gIAAPoEACAQ0QIAAJ4EADDSAgAAwQMAENMCAACeBAAw1wJAAI0EACHkAgEAiwQAIesCAQCWBAAh7QIBAJYEACHuAgIAnwQAIe8CAQCWBAAh8AIBAIsEACHxAgEAiwQAIfICAQCWBAAh8wIBAJYEACH0AgAAoAQAIPUCAACgBAAg9gIBAJYEACEDAAAArgMAIAEAAMADADAqAADBAwAgAwAAAK4DACABAACvAwAwAgAAqwMAIAEAAABAACABAAAAQAAgAwAAACAAIAEAAD8AMAIAAEAAIAMAAAAgACABAAA_ADACAABAACADAAAAIAAgAQAAPwAwAgAAQAAgCQYAAKUFACAMAACkBQAgFgAApgUAINcCQAAAAAHYAkAAAAAB5AIBAAAAAeoCAQAAAAHrAgEAAAAB7AIBAAAAAQEeAADJAwAgBtcCQAAAAAHYAkAAAAAB5AIBAAAAAeoCAQAAAAHrAgEAAAAB7AIBAAAAAQEeAADLAwAwAR4AAMsDADABAAAAGwAgAQAAAAUAIAkGAACWBQAgDAAAlQUAIBYAAJcFACDXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHqAgEAgAUAIesCAQCABQAh7AIBAIAFACECAAAAQAAgHgAA0AMAIAbXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHqAgEAgAUAIesCAQCABQAh7AIBAIAFACECAAAAIAAgHgAA0gMAIAIAAAAgACAeAADSAwAgAQAAABsAIAEAAAAFACADAAAAQAAgJQAAyQMAICYAANADACABAAAAQAAgAQAAACAAIAYEAACSBQAgLQAAlAUAIC4AAJMFACDqAgAA-gQAIOsCAAD6BAAg7AIAAPoEACAJ0QIAAJ0EADDSAgAA2wMAENMCAACdBAAw1wJAAI0EACHYAkAAjQQAIeQCAQCLBAAh6gIBAJYEACHrAgEAlgQAIewCAQCWBAAhAwAAACAAIAEAANoDADAqAADbAwAgAwAAACAAIAEAAD8AMAIAAEAAIAEAAAAmACABAAAAJgAgAwAAACQAIAEAACUAMAIAACYAIAMAAAAkACABAAAlADACAAAmACADAAAAJAAgAQAAJQAwAgAAJgAgCg0AAJAFACAPAACRBQAg1wJAAAAAAdgCQAAAAAHkAgEAAAAB5QIBAAAAAeYCAQAAAAHnAgIAAAAB6AICAAAAAekCIAAAAAEBHgAA4wMAIAjXAkAAAAAB2AJAAAAAAeQCAQAAAAHlAgEAAAAB5gIBAAAAAecCAgAAAAHoAgIAAAAB6QIgAAAAAQEeAADlAwAwAR4AAOUDADAKDQAAggUAIA8AAIMFACDXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHlAgEA9QQAIeYCAQCABQAh5wICAPMEACHoAgIA8wQAIekCIACBBQAhAgAAACYAIB4AAOgDACAI1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAh5QIBAPUEACHmAgEAgAUAIecCAgDzBAAh6AICAPMEACHpAiAAgQUAIQIAAAAkACAeAADqAwAgAgAAACQAIB4AAOoDACADAAAAJgAgJQAA4wMAICYAAOgDACABAAAAJgAgAQAAACQAIAYEAAD7BAAgKwAA_AQAICwAAP8EACAtAAD-BAAgLgAA_QQAIOYCAAD6BAAgC9ECAACVBAAw0gIAAPEDABDTAgAAlQQAMNcCQACNBAAh2AJAAI0EACHkAgEAiwQAIeUCAQCLBAAh5gIBAJYEACHnAgIAjAQAIegCAgCMBAAh6QIgAJcEACEDAAAAJAAgAQAA8AMAMCoAAPEDACADAAAAJAAgAQAAJQAwAgAAJgAgAQAAACoAIAEAAAAqACADAAAAKAAgAQAAKQAwAgAAKgAgAwAAACgAIAEAACkAMAIAACoAIAMAAAAoACABAAApADACAAAqACAHDgAA-AQAIBMAAPkEACDUAgEAAAAB1QIBAAAAAdYCAgAAAAHXAkAAAAAB2AJAAAAAAQEeAAD5AwAgBdQCAQAAAAHVAgEAAAAB1gICAAAAAdcCQAAAAAHYAkAAAAABAR4AAPsDADABHgAA-wMAMAcOAAD2BAAgEwAA9wQAINQCAQD1BAAh1QIBAPUEACHWAgIA8wQAIdcCQAD0BAAh2AJAAPQEACECAAAAKgAgHgAA_gMAIAXUAgEA9QQAIdUCAQD1BAAh1gICAPMEACHXAkAA9AQAIdgCQAD0BAAhAgAAACgAIB4AAIAEACACAAAAKAAgHgAAgAQAIAMAAAAqACAlAAD5AwAgJgAA_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-QIBAAAAAfoCgAAAAAH7AoAAAAAB_AKAAAAAAQ0EAACbBAAgKwAAowQAICwAAJsEACAtAACbBAAgLgAAmwQAINkCAgAAAAHaAgIAAAAF2wICAAAABdwCAgAAAAHdAgIAAAAB3gICAAAAAd8CAgAAAAHgAgIAogQAIQjZAggAAAAB2gIIAAAABdsCCAAAAAXcAggAAAAB3QIIAAAAAd4CCAAAAAHfAggAAAAB4AIIAKMEACEQ0QIAAKQEADDSAgAArgMAENMCAACkBAAw1wJAAKYEACHkAgEApQQAIesCAQCnBAAh7QIBAKcEACHuAgIAqAQAIe8CAQCnBAAh8AIBAKUEACHxAgEApQQAIfICAQCnBAAh8wIBAKcEACH0AgAAqQQAIPUCAACpBAAg9gIBAKcEACEL2QIBAAAAAdoCAQAAAATbAgEAAAAE3AIBAAAAAd0CAQAAAAHeAgEAAAAB3wIBAAAAAeACAQCUBAAh4QIBAAAAAeICAQAAAAHjAgEAAAABCNkCQAAAAAHaAkAAAAAE2wJAAAAABNwCQAAAAAHdAkAAAAAB3gJAAAAAAd8CQAAAAAHgAkAAkAQAIQvZAgEAAAAB2gIBAAAABdsCAQAAAAXcAgEAAAAB3QIBAAAAAd4CAQAAAAHfAgEAAAAB4AIBAJwEACHhAgEAAAAB4gIBAAAAAeMCAQAAAAEI2QICAAAAAdoCAgAAAAXbAgIAAAAF3AICAAAAAd0CAgAAAAHeAgIAAAAB3wICAAAAAeACAgCbBAAhDNkCgAAAAAHcAoAAAAAB3QKAAAAAAd4CgAAAAAHfAoAAAAAB4AKAAAAAAfcCAQAAAAH4AgEAAAAB-QIBAAAAAfoCgAAAAAH7AoAAAAAB_AKAAAAAAQXRAgAAqgQAMNICAACoAwAQ0wIAAKoEADD9AgIAjAQAIf4CAACrBAAgDwQAAI8EACAtAACsBAAgLgAArAQAINkCgAAAAAHcAoAAAAAB3QKAAAAAAd4CgAAAAAHfAoAAAAAB4AKAAAAAAfcCAQAAAAH4AgEAAAAB-QIBAAAAAfoCgAAAAAH7AoAAAAAB_AKAAAAAAQzZAoAAAAAB3AKAAAAAAd0CgAAAAAHeAoAAAAAB3wKAAAAAAeACgAAAAAH3AgEAAAAB-AIBAAAAAfkCAQAAAAH6AoAAAAAB-wKAAAAAAfwCgAAAAAEF0QIAAK0EADDSAgAAlQMAENMCAACtBAAw_QICAK4EACH-AgAArwQAIAjZAgIAAAAB2gICAAAABNsCAgAAAATcAgIAAAAB3QICAAAAAd4CAgAAAAHfAgIAAAAB4AICAI8EACEM2QKAAAAAAdwCgAAAAAHdAoAAAAAB3gKAAAAAAd8CgAAAAAHgAoAAAAAB9wIBAAAAAfgCAQAAAAH5AgEAAAAB-gKAAAAAAfsCgAAAAAH8AoAAAAABCdECAACwBAAw0gIAAI8DABDTAgAAsAQAMNcCQACNBAAh5AIBAIsEACH_AgEAiwQAIYADAQCLBAAhgQNAALEEACGCA0AAsQQAIQsEAACbBAAgLQAAswQAIC4AALMEACDZAkAAAAAB2gJAAAAABdsCQAAAAAXcAkAAAAAB3QJAAAAAAd4CQAAAAAHfAkAAAAAB4AJAALIEACELBAAAmwQAIC0AALMEACAuAACzBAAg2QJAAAAAAdoCQAAAAAXbAkAAAAAF3AJAAAAAAd0CQAAAAAHeAkAAAAAB3wJAAAAAAeACQACyBAAhCNkCQAAAAAHaAkAAAAAF2wJAAAAABdwCQAAAAAHdAkAAAAAB3gJAAAAAAd8CQAAAAAHgAkAAswQAIQnRAgAAtAQAMNICAAD8AgAQ0wIAALQEADDXAkAApgQAIeQCAQClBAAh_wIBAKUEACGAAwEApQQAIYEDQAC1BAAhggNAALUEACEI2QJAAAAAAdoCQAAAAAXbAkAAAAAF3AJAAAAAAd0CQAAAAAHeAkAAAAAB3wJAAAAAAeACQACzBAAhDNECAAC2BAAw0gIAAPYCABDTAgAAtgQAMNcCQACNBAAh5AIBAIsEACHrAgEAlgQAIe0CAQCWBAAhgwMBAIsEACGEAyAAlwQAIYUDQACxBAAhhgMBAJYEACGHAwEAlgQAIQbRAgAAtwQAMNICAADeAgAQ0wIAALcEADDWAgIAjAQAIeQCAgCMBAAhiAMBAIsEACEHDwAAuQQAINECAAC4BAAw0gIAAMsCABDTAgAAuAQAMNYCAgCuBAAh5AICAK4EACGIAwEApQQAIQOJAwAALAAgigMAACwAIIsDAAAsACAG0QIAALoEADDSAgAAxQIAENMCAAC6BAAw5AICAIwEACGIAwEAiwQAIYwDAgCMBAAhBw8AALkEACDRAgAAuwQAMNICAACyAgAQ0wIAALsEADDkAgIArgQAIYgDAQClBAAhjAMCAK4EACEH0QIAALwEADDSAgAArAIAENMCAAC8BAAw1QIBAIsEACHXAkAAjQQAIdgCQACNBAAh6wIBAIsEACEG0QIAAL0EADDSAgAAlgIAENMCAAC9BAAw1gICAIwEACHkAgIAjAQAIYgDAQCLBAAhBw8AALkEACDRAgAAvgQAMNICAACDAgAQ0wIAAL4EADDWAgIArgQAIeQCAgCuBAAhiAMBAKUEACEJ0QIAAL8EADDSAgAA_QEAENMCAAC_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_QICAK4EACGIAwEApQQAIY0DAQClBAAhjgMCAK4EACGPAwIArgQAIQkPAAC5BAAg0QIAALsEADDSAgAAsgIAENMCAAC7BAAw5AICAK4EACGIAwEApQQAIYwDAgCuBAAhrwMAALICACCwAwAAsgIAIAkPAAC5BAAg0QIAALgEADDSAgAAywIAENMCAAC4BAAw1gICAK4EACHkAgIArgQAIYgDAQClBAAhrwMAAMsCACCwAwAAywIAIAkPAAC5BAAg0QIAAL4EADDSAgAAgwIAENMCAAC-BAAw1gICAK4EACHkAgIArgQAIYgDAQClBAAhrwMAAIMCACCwAwAAgwIAIAOJAwAANQAgigMAADUAIIsDAAA1ACADiQMAACgAIIoDAAAoACCLAwAAKAAgAtQCAQAAAAHVAgEAAAABCg4AANoEACATAADRBAAg0QIAANkEADDSAgAAKAAQ0wIAANkEADDUAgEApQQAIdUCAQClBAAh1gICAK4EACHXAkAApgQAIdgCQACmBAAhDw0AAN0EACAPAADXBAAg0QIAANsEADDSAgAAJAAQ0wIAANsEADDXAkAApgQAIdgCQACmBAAh5AIBAKUEACHlAgEApQQAIeYCAQCnBAAh5wICAK4EACHoAgIArgQAIekCIADcBAAhrwMAACQAILADAAAkACANDQAA3QQAIA8AANcEACDRAgAA2wQAMNICAAAkABDTAgAA2wQAMNcCQACmBAAh2AJAAKYEACHkAgEApQQAIeUCAQClBAAh5gIBAKcEACHnAgIArgQAIegCAgCuBAAh6QIgANwEACEC2QIgAAAAAeACIACZBAAhDgYAAMwEACAMAADLBAAgFgAAzQQAINECAADKBAAw0gIAACAAENMCAADKBAAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh6gIBAKcEACHrAgEApwQAIewCAQCnBAAhrwMAACAAILADAAAgACAC6wIBAAAAAYcDAQAAAAEOBgAAzAQAIA0AAOAEACDRAgAA3wQAMNICAAAbABDTAgAA3wQAMNcCQACmBAAh5AIBAKUEACHrAgEApwQAIe0CAQCnBAAhgwMBAKUEACGEAyAA3AQAIYUDQAC1BAAhhgMBAKcEACGHAwEApwQAIQ4GAADMBAAgDAAAywQAIBYAAM0EACDRAgAAygQAMNICAAAgABDTAgAAygQAMNcCQACmBAAh2AJAAKYEACHkAgEApQQAIeoCAQCnBAAh6wIBAKcEACHsAgEApwQAIa8DAAAgACCwAwAAIAAgDwkAAOIEACDRAgAA4QQAMNICAAAWABDTAgAA4QQAMOQCAQClBAAh7AIBAKUEACGdAwEApQQAIZ4DAQClBAAhnwMBAKUEACGgAwEApQQAIaEDAQClBAAhogMBAKUEACGjAyAA3AQAIaQDIADcBAAhpQMgANwEACEOBgAAzAQAIAoAAOQEACDRAgAA4wQAMNICAAAR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_BQAgCQAAACAAIAYAAJYFACAWAACXBQAgHgAAvwUAINcCQAD0BAAh2AJAAPQEACHkAgEA9QQAIesCAQCABQAh7AIBAIAFACEHBgAAlgUAIBYAAJcFACDXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHrAgEAgAUAIewCAQCABQAhAyUAAIQIACCxAwAAhQgAILcDAAABACADJQAAuwUAILEDAAC8BQAgtwMAAEAAIAAAAAAACyUAAMgFADAmAADNBQAwsQMAAMkFADCyAwAAygUAMLMDAADLBQAgtAMAAMwFADC1AwAAzAUAMLYDAADMBQAwtwMAAMwFADC4AwAAzgUAMLkDAADPBQAwCRAAAO8FACASAADwBQAgFAAA8QUAIBUAAPIFACDkAgEAAAAB_QICAAAAAYgDAQAAAAGNAwEAAAABjwMCAAAAAQIAAAAuACAlAADuBQAgAwAAAC4AICUAAO4FACAmAADSBQAgAR4AAIMIADAOEAAA0wQAIBEAANQEACASAADVBAAgFAAA1gQAIBUAANcEACDRAgAA0gQAMNICAAAsABDTAgAA0gQAMOQCAQAAAAH9AgIArgQAIYgDAQAAAAGNAwEApQQAIY4DAgCuBAAhjwMCAK4EACECAAAALgAgHgAA0gUAIAIAAADQBQAgHgAA0QUAIAnRAgAAzwUAMNICAADQBQAQ0wIAAM8FADDkAgEApQQAIf0CAgCuBAAhiAMBAKUEACGNAwEApQQAIY4DAgCuBAAhjwMCAK4EACEJ0QIAAM8FADDSAgAA0AUAENMCAADPBQAw5AIBAKUEACH9AgIArgQAIYgDAQClBAAhjQMBAKUEACGOAwIArgQAIY8DAgCuBAAhBeQCAQD1BAAh_QICAPMEACGIAwEA9QQAIY0DAQD1BAAhjwMCAPMEACEJEAAA0wUAIBIAANQFACAUAADVBQAgFQAA1gUAIOQCAQD1BAAh_QICAPMEACGIAwEA9QQAIY0DAQD1BAAhjwMCAPMEACEFJQAA9AcAICYAAIEIACCxAwAA9QcAILIDAACACAAgtwMAAK8CACAFJQAA8gcAICYAAP4HACCxAwAA8wcAILIDAAD9BwAgtwMAAIACACALJQAA4AUAMCYAAOUFADCxAwAA4QUAMLIDAADiBQAwswMAAOMFACC0AwAA5AUAMLUDAADkBQAwtgMAAOQFADC3AwAA5AUAMLgDAADmBQAwuQMAAOcFADALJQAA1wUAMCYAANsFADCxAwAA2AUAMLIDAADZBQAwswMAANoFACC0AwAAiAUAMLUDAACIBQAwtgMAAIgFADC3AwAAiAUAMLgDAADcBQAwuQMAAIsFADAFDgAA-AQAINQCAQAAAAHWAgIAAAAB1wJAAAAAAdgCQAAAAAECAAAAKgAgJQAA3wUAIAMAAAAqACAlAADfBQAgJgAA3gUAIAEeAAD8BwAwAgAAACoAIB4AAN4FACACAAAAjAUAIB4AAN0FACAE1AIBAPUEACHWAgIA8wQAIdcCQAD0BAAh2AJAAPQEACEFDgAA9gQAINQCAQD1BAAh1gICAPMEACHXAkAA9AQAIdgCQAD0BAAhBQ4AAPgEACDUAgEAAAAB1gICAAAAAdcCQAAAAAHYAkAAAAABBAYAAO0FACDXAkAAAAAB2AJAAAAAAesCAQAAAAECAAAANwAgJQAA7AUAIAMAAAA3ACAlAADsBQAgJgAA6gUAIAEeAAD7BwAwCgYAANAEACATAADRBAAg0QIAAM8EADDSAgAANQAQ0wIAAM8EADDVAgEApQQAIdcCQACmBAAh2AJAAKYEACHrAgEApQQAIawDAADOBAAgAgAAADcAIB4AAOoFACACAAAA6AUAIB4AAOkFACAH0QIAAOcFADDSAgAA6AUAENMCAADnBQAw1QIBAKUEACHXAkAApgQAIdgCQACmBAAh6wIBAKUEACEH0QIAAOcFADDSAgAA6AUAENMCAADnBQAw1QIBAKUEACHXAkAApgQAIdgCQACmBAAh6wIBAKUEACED1wJAAPQEACHYAkAA9AQAIesCAQD1BAAhBAYAAOsFACDXAkAA9AQAIdgCQAD0BAAh6wIBAPUEACEFJQAA9gcAICYAAPkHACCxAwAA9wcAILIDAAD4BwAgtwMAAAEAIAQGAADtBQAg1wJAAAAAAdgCQAAAAAHrAgEAAAABAyUAAPYHACCxAwAA9wcAILcDAAABACAJEAAA7wUAIBIAAPAFACAUAADxBQAgFQAA8gUAIOQCAQAAAAH9AgIAAAABiAMBAAAAAY0DAQAAAAGPAwIAAAABAyUAAPQHACCxAwAA9QcAILcDAACvAgAgAyUAAPIHACCxAwAA8wcAILcDAACAAgAgBCUAAOAFADCxAwAA4QUAMLMDAADjBQAgtwMAAOQFADAEJQAA1wUAMLEDAADYBQAwswMAANoFACC3AwAAiAUAMAQlAADIBQAwsQMAAMkFADCzAwAAywUAILcDAADMBQAwAAAAAAAACyUAAPsFADAmAAD_BQAwsQMAAPwFADCyAwAA_QUAMLMDAAD-BQAgtAMAAMwFADC1AwAAzAUAMLYDAADMBQAwtwMAAMwFADC4AwAAgAYAMLkDAADPBQAwCREAAIUGACASAADwBQAgFAAA8QUAIBUAAPIFACDkAgEAAAABiAMBAAAAAY0DAQAAAAGOAwIAAAABjwMCAAAAAQIAAAAuACAlAACEBgAgAwAAAC4AICUAAIQGACAmAACCBgAgAR4AAPEHADACAAAALgAgHgAAggYAIAIAAADQBQAgHgAAgQYAIAXkAgEA9QQAIYgDAQD1BAAhjQMBAPUEACGOAwIA8wQAIY8DAgDzBAAhCREAAIMGACASAADUBQAgFAAA1QUAIBUAANYFACDkAgEA9QQAIYgDAQD1BAAhjQMBAPUEACGOAwIA8wQAIY8DAgDzBAAhBSUAAOwHACAmAADvBwAgsQMAAO0HACCyAwAA7gcAILcDAADIAgAgCREAAIUGACASAADwBQAgFAAA8QUAIBUAAPIFACDkAgEAAAABiAMBAAAAAY0DAQAAAAGOAwIAAAABjwMCAAAAAQMlAADsBwAgsQMAAO0HACC3AwAAyAIAIAQlAAD7BQAwsQMAAPwFADCzAwAA_gUAILcDAADMBQAwAAAABSUAAOcHACAmAADqBwAgsQMAAOgHACCyAwAA6QcAILcDAAAuACADJQAA5wcAILEDAADoBwAgtwMAAC4AIAAAAAAACyUAAJIGADAmAACWBgAwsQMAAJMGADCyAwAAlAYAMLMDAACVBgAgtAMAAMwFADC1AwAAzAUAMLYDAADMBQAwtwMAAMwFADC4AwAAlwYAMLkDAADPBQAwCRAAAO8FACARAACFBgAgFAAA8QUAIBUAAPIFACDkAgEAAAAB_QICAAAAAYgDAQAAAAGNAwEAAAABjgMCAAAAAQIAAAAuACAlAACaBgAgAwAAAC4AICUAAJoGACAmAACZBgAgAR4AAOYHADACAAAALgAgHgAAmQYAIAIAAADQBQAgHgAAmAYAIAXkAgEA9QQAIf0CAgDzBAAhiAMBAPUEACGNAwEA9QQAIY4DAgDzBAAhCRAAANMFACARAACDBgAgFAAA1QUAIBUAANYFACDkAgEA9QQAIf0CAgDzBAAhiAMBAPUEACGNAwEA9QQAIY4DAgDzBAAhCRAAAO8FACARAACFBgAgFAAA8QUAIBUAAPIFACDkAgEAAAAB_QICAAAAAYgDAQAAAAGNAwEAAAABjgMCAAAAAQQlAACSBgAwsQMAAJMGADCzAwAAlQYAILcDAADMBQAwAAAAAAAAAAAAAAAAAAslAACqBgAwJgAArwYAMLEDAACrBgAwsgMAAKwGADCzAwAArQYAILQDAACuBgAwtQMAAK4GADC2AwAArgYAMLcDAACuBgAwuAMAALAGADC5AwAAsQYAMA0HAACPBwAgCAAAkAcAIAsAAJEHACAUAACTBwAgFwAAkgcAIBgAAJQHACDXAkAAAAAB2AJAAAAAAeQCAQAAAAHtAgEAAAABiAMBAAAAAaoDIAAAAAGrAwEAAAABAgAAAAEAICUAAI4HACADAAAAAQAgJQAAjgcAICYAALQGACABHgAA5QcAMBIFAADoBAAgBwAA6QQAIAgAAOoEACALAADrBAAgFAAA1gQAIBcAAOwEACAYAADtBAAg0QIAAOcEADDSAgAABQAQ0wIAAOcEADDXAkAApgQAIdgCQACmBAAh5AIBAAAAAe0CAQAAAAHuAgIAqAQAIYgDAQClBAAhqgMgANwEACGrAwEApwQAIQIAAAABACAeAAC0BgAgAgAAALIGACAeAACzBgAgC9ECAACxBgAw0gIAALIGABDTAgAAsQYAMNcCQACmBAAh2AJAAKYEACHkAgEApQQAIe0CAQClBAAh7gICAKgEACGIAwEApQQAIaoDIADcBAAhqwMBAKcEACEL0QIAALEGADDSAgAAsgYAENMCAACxBgAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh7QIBAKUEACHuAgIAqAQAIYgDAQClBAAhqgMgANwEACGrAwEApwQAIQfXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHtAgEA9QQAIYgDAQD1BAAhqgMgAIEFACGrAwEAgAUAIQ0HAAC1BgAgCAAAtgYAIAsAALcGACAUAAC5BgAgFwAAuAYAIBgAALoGACDXAkAA9AQAIdgCQAD0BAAh5AIBAPUEACHtAgEA9QQAIYgDAQD1BAAhqgMgAIEFACGrAwEAgAUAIQslAACCBwAwJgAAhwcAMLEDAACDBwAwsgMAAIQHADCzAwAAhQcAILQDAACGBwAwtQMAAIYHADC2AwAAhgcAMLcDAACGBwAwuAMAAIgHADC5AwAAiQcAMAslAAD2BgAwJgAA-wYAMLEDAAD3BgAwsgMAAPgGADCzAwAA-QYAILQDAAD6BgAwtQMAAPoGADC2AwAA-gYAMLcDAAD6BgAwuAMAAPwGADC5AwAA_QYAMAslAADcBgAwJgAA4QYAMLEDAADdBgAwsgMAAN4GADCzAwAA3wYAILQDAADgBgAwtQMAAOAGADC2AwAA4AYAMLcDAADgBgAwuAMAAOIGADC5AwAA4wYAMAslAADQBgAwJgAA1QYAMLEDAADRBgAwsgMAANIGADCzAwAA0wYAILQDAADUBgAwtQMAANQGADC2AwAA1AYAMLcDAADUBgAwuAMAANYGADC5AwAA1wYAMAslAADHBgAwJgAAywYAMLEDAADIBgAwsgMAAMkGADCzAwAAygYAILQDAADkBQAwtQMAAOQFADC2AwAA5AUAMLcDAADkBQAwuAMAAMwGADC5AwAA5wUAMAslAAC7BgAwJgAAwAYAMLEDAAC8BgAwsgMAAL0GADCzAwAAvgYAILQDAAC_BgAwtQMAAL8GADC2AwAAvwYAMLcDAAC_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_gYAIB4AAP8GACAQ0QIAAP0GADDSAgAA_gYAENMCAAD9BgAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh6wIBAKUEACGRAwEApQQAIZIDAQClBAAhkwMBAKcEACGUAwEApwQAIZUDAQCnBAAhlgNAALUEACGXA0AAtQQAIZgDAQCnBAAhmQMBAKcEACEQ0QIAAP0GADDSAgAA_gYAENMCAAD9BgAw1wJAAKYEACHYAkAApgQAIeQCAQClBAAh6wIBAKUEACGRAwEApQQAIZIDAQClBAAhkwMBAKcEACGUAwEApwQAIZUDAQCnBAAhlgNAALUEACGXA0AAtQQAIZgDAQCnBAAhmQMBAKcEACEM1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAhkQMBAPUEACGSAwEA9QQAIZMDAQCABQAhlAMBAIAFACGVAwEAgAUAIZYDQAC1BQAhlwNAALUFACGYAwEAgAUAIZkDAQCABQAhDNcCQAD0BAAh2AJAAPQEACHkAgEA9QQAIZEDAQD1BAAhkgMBAPUEACGTAwEAgAUAIZQDAQCABQAhlQMBAIAFACGWA0AAtQUAIZcDQAC1BQAhmAMBAIAFACGZAwEAgAUAIQzXAkAAAAAB2AJAAAAAAeQCAQAAAAGRAwEAAAABkgMBAAAAAZMDAQAAAAGUAwEAAAABlQMBAAAAAZYDQAAAAAGXA0AAAAABmAMBAAAAAZkDAQAAAAEH1wJAAAAAAdgCQAAAAAHkAgEAAAABgQNAAAAAAZoDAQAAAAGbAwEAAAABnAMBAAAAAQIAAAALACAlAACNBwAgAwAAAAsAICUAAI0HACAmAACMBwAgAR4AAN4HADAMBgAA0AQAINECAADmBAAw0gIAAAkAENMCAADmBAAw1wJAAKYEACHYAkAApgQAIeQCAQAAAAHrAgEApQQAIYEDQACmBAAhmgMBAAAAAZsDAQCnBAAhnAMBAKcEACECAAAACwAgHgAAjAcAIAIAAACKBwAgHgAAiwcAIAvRAgAAiQcAMNICAACKBwAQ0wIAAIkHADDXAkAApgQAIdgCQACmBAAh5AIBAKUEACHrAgEApQQAIYEDQACmBAAhmgMBAKUEACGbAwEApwQAIZwDAQCnBAAhC9ECAACJBwAw0gIAAIoHABDTAgAAiQcAMNcCQACmBAAh2AJAAKYEACHkAgEApQQAIesCAQClBAAhgQNAAKYEACGaAwEApQQAIZsDAQCnBAAhnAMBAKcEACEH1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAhgQNAAPQEACGaAwEA9QQAIZsDAQCABQAhnAMBAIAFACEH1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAhgQNAAPQEACGaAwEA9QQAIZsDAQCABQAhnAMBAIAFACEH1wJAAAAAAdgCQAAAAAHkAgEAAAABgQNAAAAAAZoDAQAAAAGbAwEAAAABnAMBAAAAAQ0HAACPBwAgCAAAkAcAIAsAAJEHACAUAACTBwAgFwAAkgcAIBgAAJQHACDXAkAAAAAB2AJAAAAAAeQCAQAAAAHtAgEAAAABiAMBAAAAAaoDIAAAAAGrAwEAAAABBCUAAIIHADCxAwAAgwcAMLMDAACFBwAgtwMAAIYHADAEJQAA9gYAMLEDAAD3BgAwswMAAPkGACC3AwAA-gYAMAQlAADcBgAwsQMAAN0GADCzAwAA3wYAILcDAADgBgAwBCUAANAGADCxAwAA0QYAMLMDAADTBgAgtwMAANQGADAEJQAAxwYAMLEDAADIBgAwswMAAMoGACC3AwAA5AUAMAQlAAC7BgAwsQMAALwGADCzAwAAvgYAILcDAAC_BgAwBCUAAKoGADCxAwAAqwYAMLMDAACtBgAgtwMAAK4GADAAAAAABSUAANkHACAmAADcBwAgsQMAANoHACCyAwAA2wcAILcDAAABACADJQAA2QcAILEDAADaBwAgtwMAAAEAIAAAAAUlAADUBwAgJgAA1wcAILEDAADVBwAgsgMAANYHACC3AwAAAQAgAyUAANQHACCxAwAA1QcAILcDAAABACAAAAAFJQAAzwcAICYAANIHACCxAwAA0AcAILIDAADRBwAgtwMAABMAIAMlAADPBwAgsQMAANAHACC3AwAAEwAgAAAAByUAAMoHACAmAADNBwAgsQMAAMsHACCyAwAAzAcAILUDAAAFACC2AwAABQAgtwMAAAEAIAMlAADKBwAgsQMAAMsHACC3AwAAAQAgAAAAAAAHJQAAxQcAICYAAMgHACCxAwAAxgcAILIDAADHBwAgtQMAAAMAILYDAAADACC3AwAAuQEAIAMlAADFBwAgsQMAAMYHACC3AwAAuQEAIAcGAACzBwAgDQAAvAcAIOsCAAD6BAAg7QIAAPoEACCFAwAA-gQAIIYDAAD6BAAghwMAAPoEACAJBQAAvwcAIAcAAMAHACAIAADBBwAgCwAAwgcAIBQAALkHACAXAADDBwAgGAAAxAcAIO4CAAD6BAAgqwMAAPoEACAABRAAALYHACARAAC3BwAgEgAAuAcAIBQAALkHACAVAAC6BwAgAQ8AAPQFACABDwAA9AUAIAEPAAD0BQAgAAADDQAAvAcAIA8AALoHACDmAgAA-gQAIAYGAACzBwAgDAAAsgcAIBYAALQHACDqAgAA-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_BwAgBQAAAIMCACAeAAD_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_QICAAAAAYgDAQAAAAGNAwEAAAABjgMCAAAAAY8DAgAAAAECAAAALgAgJQAAmggAIAkNAACQBQAg1wJAAAAAAdgCQAAAAAHkAgEAAAAB5QIBAAAAAeYCAQAAAAHnAgIAAAAB6AICAAAAAekCIAAAAAECAAAAJgAgJQAAnAgAIAMAAAAsACAlAACaCAAgJgAAoAgAIAwAAAAsACAQAADTBQAgEQAAgwYAIBIAANQFACAUAADVBQAgHgAAoAgAIOQCAQD1BAAh_QICAPMEACGIAwEA9QQAIY0DAQD1BAAhjgMCAPMEACGPAwIA8wQAIQoQAADTBQAgEQAAgwYAIBIAANQFACAUAADVBQAg5AIBAPUEACH9AgIA8wQAIYgDAQD1BAAhjQMBAPUEACGOAwIA8wQAIY8DAgDzBAAhAwAAACQAICUAAJwIACAmAACjCAAgCwAAACQAIA0AAIIFACAeAACjCAAg1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAh5QIBAPUEACHmAgEAgAUAIecCAgDzBAAh6AICAPMEACHpAiAAgQUAIQkNAACCBQAg1wJAAPQEACHYAkAA9AQAIeQCAQD1BAAh5QIBAPUEACHmAgEAgAUAIecCAgDzBAAh6AICAPMEACHpAiAAgQUAIQgEABgFBAIHDAQIEAULFAYUPhQXHgkYQQoCAwcBBAADAQMIAAEGAAEBBgABAwQACAYVAQoZBwEJAAYBChoAAgYfAQ0hCgQEABcGIwEMIgkWJwsDBAAWDQAKDysMAg4ACxMADQYEABUQAA4RABASABIUOBQVOQwCBAAPDy8NAQ8wAAIEABEPMQ0BDzIAAgQAEw8zDQEPNAACBgABEwANAhQ6ABU7AAEPPAABFj0ABgdCAAhDAAtEABRGABdFABhHAAABBVECAQVXAgUEAB0rAB4sAB8tACAuACEAAAAAAAUEAB0rAB4sAB8tACAuACEBBmkBAQZvAQMEACYtACcuACgAAAADBAAmLQAnLgAoAQkABgEJAAYDBAAtLQAuLgAvAAAAAwQALS0ALi4ALwEGAAEBBgABAwQANC0ANS4ANgAAAAMEADQtADUuADYBBgABAQYAAQMEADstADwuAD0AAAADBAA7LQA8LgA9AAAFBABCKwBDLABELQBFLgBGAAAAAAAFBABCKwBDLABELQBFLgBGAAAAAwQATC0ATS4ATgAAAAMEAEwtAE0uAE4DEAAOEQAQEgASAxAADhEAEBIAEgUEAFMrAFQsAFUtAFYuAFcAAAAAAAUEAFMrAFQsAFUtAFYuAFcAAAUEAFwrAF0sAF4tAF8uAGAAAAAAAAUEAFwrAF0sAF4tAF8uAGACBgABEwANAgYAARMADQMEAGUtAGYuAGcAAAADBABlLQBmLgBnAAAFBABsKwBtLABuLQBvLgBwAAAAAAAFBABsKwBtLABuLQBvLgBwAAAFBAB1KwB2LAB3LQB4LgB5AAAAAAAFBAB1KwB2LAB3LQB4LgB5AQbrAgEBBvECAQMEAH4tAH8uAIABAAAAAwQAfi0Afy4AgAEAAAADBACGAS0AhwEuAIgBAAAAAwQAhgEtAIcBLgCIAQAAAAUEAI4BKwCPASwAkAEtAJEBLgCSAQAAAAAABQQAjgErAI8BLACQAS0AkQEuAJIBAAAABQQAmAErAJkBLACaAS0AmwEuAJwBAAAAAAAFBACYASsAmQEsAJoBLQCbAS4AnAECBs8DAQzOAwkCBtYDAQzVAwkDBAChAS0AogEuAKMBAAAAAwQAoQEtAKIBLgCjAQENAAoBDQAKBQQAqAErAKkBLACqAS0AqwEuAKwBAAAAAAAFBACoASsAqQEsAKoBLQCrAS4ArAECDgALEwANAg4ACxMADQUEALEBKwCyASwAswEtALQBLgC1AQAAAAAABQQAsQErALIBLACzAS0AtAEuALUBGQIBGkgBG0kBHEoBHUsBH00BIE8ZIVAaIlMBI1UZJFYbJ1gBKFkBKVoZL10cMF4iMV8GMmAGM2EGNGIGNWMGNmUGN2cZOGgjOWsGOm0ZO24kPHAGPXEGPnIZP3UlQHYpQXcHQngHQ3kHRHoHRXsHRn0HR38ZSIABKkmCAQdKhAEZS4UBK0yGAQdNhwEHTogBGU-LASxQjAEwUY0BBFKOAQRTjwEEVJABBFWRAQRWkwEEV5UBGViWATFZmAEEWpoBGVubATJcnAEEXZ0BBF6eARlfoQEzYKIBN2GjAQVipAEFY6UBBWSmAQVlpwEFZqkBBWerARlorAE4aa4BBWqwARlrsQE5bLIBBW2zAQVutAEZb7cBOnC4AT5xugECcrsBAnO9AQJ0vgECdb8BAnbBAQJ3wwEZeMQBP3nGAQJ6yAEZe8kBQHzKAQJ9ywECfswBGX_PAUGAAdABR4EB0gFIggHTAUiDAdYBSIQB1wFIhQHYAUiGAdoBSIcB3AEZiAHdAUmJAd8BSIoB4QEZiwHiAUqMAeMBSI0B5AFIjgHlARmPAegBS5AB6QFPkQHqAQ2SAesBDZMB7AENlAHtAQ2VAe4BDZYB8AENlwHyARmYAfMBUJkB9QENmgH3ARmbAfgBUZwB-QENnQH6AQ2eAfsBGZ8B_gFSoAH_AVihAYECEqIBggISowGFAhKkAYYCEqUBhwISpgGJAhKnAYsCGagBjAJZqQGOAhKqAZACGasBkQJarAGSAhKtAZMCEq4BlAIZrwGXAluwAZgCYbEBmQIUsgGaAhSzAZsCFLQBnAIUtQGdAhS2AZ8CFLcBoQIZuAGiAmK5AaQCFLoBpgIZuwGnAmO8AagCFL0BqQIUvgGqAhm_Aa0CZMABrgJowQGwAg7CAbECDsMBtAIOxAG1Ag7FAbYCDsYBuAIOxwG6AhnIAbsCackBvQIOygG_AhnLAcACaswBwQIOzQHCAg7OAcMCGc8BxgJr0AHHAnHRAckCENIBygIQ0wHNAhDUAc4CENUBzwIQ1gHRAhDXAdMCGdgB1AJy2QHWAhDaAdgCGdsB2QJz3AHaAhDdAdsCEN4B3AIZ3wHfAnTgAeACeuEB4QIJ4gHiAgnjAeMCCeQB5AIJ5QHlAgnmAecCCecB6QIZ6AHqAnvpAe0CCeoB7wIZ6wHwAnzsAfICCe0B8wIJ7gH0AhnvAfcCffAB-AKBAfEB-gKCAfIB-wKCAfMB_gKCAfQB_wKCAfUBgAOCAfYBggOCAfcBhAMZ-AGFA4MB-QGHA4IB-gGJAxn7AYoDhAH8AYsDggH9AYwDggH-AY0DGf8BkAOFAYACkQOJAYECkwOKAYIClAOKAYMClwOKAYQCmAOKAYUCmQOKAYYCmwOKAYcCnQMZiAKeA4sBiQKgA4oBigKiAxmLAqMDjAGMAqQDigGNAqUDigGOAqYDGY8CqQONAZACqgOTAZECrAOUAZICrQOUAZMCsAOUAZQCsQOUAZUCsgOUAZYCtAOUAZcCtgMZmAK3A5UBmQK5A5QBmgK7AxmbArwDlgGcAr0DlAGdAr4DlAGeAr8DGZ8CwgOXAaACwwOdAaECxAMKogLFAwqjAsYDCqQCxwMKpQLIAwqmAsoDCqcCzAMZqALNA54BqQLRAwqqAtMDGasC1AOfAawC1wMKrQLYAwquAtkDGa8C3AOgAbAC3QOkAbEC3gMLsgLfAwuzAuADC7QC4QMLtQLiAwu2AuQDC7cC5gMZuALnA6UBuQLpAwu6AusDGbsC7AOmAbwC7QMLvQLuAwu-Au8DGb8C8gOnAcAC8wOtAcEC9AMMwgL1AwzDAvYDDMQC9wMMxQL4AwzGAvoDDMcC_AMZyAL9A64ByQL_AwzKAoEEGcsCggSvAcwCgwQMzQKEBAzOAoUEGc8CiASwAdACiQS2AQ" } async function decodeBase64AsWasm(wasmBase64: string): Promise { diff --git a/src/lib/generated/prisma/internal/prismaNamespace.ts b/src/lib/generated/prisma/internal/prismaNamespace.ts index 9d918be..3d7c867 100644 --- a/src/lib/generated/prisma/internal/prismaNamespace.ts +++ b/src/lib/generated/prisma/internal/prismaNamespace.ts @@ -2069,7 +2069,9 @@ export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typ export const ScheduleScalarFieldEnum = { id: 'id', + licenseKeyId: 'licenseKeyId', userId: 'userId', + type: 'type', createdAt: 'createdAt', updatedAt: 'updatedAt' } as const @@ -2080,6 +2082,7 @@ export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typ export const BlockScalarFieldEnum = { id: 'id', scheduleId: 'scheduleId', + day: 'day', startMinutes: 'startMinutes', endMinutes: 'endMinutes', dynamicAudio: 'dynamicAudio', diff --git a/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts index fe7def9..ada708e 100644 --- a/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/src/lib/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -290,7 +290,9 @@ export type AuditLogScalarFieldEnum = (typeof AuditLogScalarFieldEnum)[keyof typ export const ScheduleScalarFieldEnum = { id: 'id', + licenseKeyId: 'licenseKeyId', userId: 'userId', + type: 'type', createdAt: 'createdAt', updatedAt: 'updatedAt' } as const @@ -301,6 +303,7 @@ export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typ export const BlockScalarFieldEnum = { id: 'id', scheduleId: 'scheduleId', + day: 'day', startMinutes: 'startMinutes', endMinutes: 'endMinutes', dynamicAudio: 'dynamicAudio', diff --git a/src/lib/generated/prisma/models/Block.ts b/src/lib/generated/prisma/models/Block.ts index 28969e0..72b06e8 100644 --- a/src/lib/generated/prisma/models/Block.ts +++ b/src/lib/generated/prisma/models/Block.ts @@ -39,6 +39,7 @@ export type BlockSumAggregateOutputType = { export type BlockMinAggregateOutputType = { id: string | null scheduleId: string | null + day: string | null startMinutes: number | null endMinutes: number | null dynamicAudio: boolean | null @@ -49,6 +50,7 @@ export type BlockMinAggregateOutputType = { export type BlockMaxAggregateOutputType = { id: string | null scheduleId: string | null + day: string | null startMinutes: number | null endMinutes: number | null dynamicAudio: boolean | null @@ -59,6 +61,7 @@ export type BlockMaxAggregateOutputType = { export type BlockCountAggregateOutputType = { id: number scheduleId: number + day: number startMinutes: number endMinutes: number dynamicAudio: number @@ -81,6 +84,7 @@ export type BlockSumAggregateInputType = { export type BlockMinAggregateInputType = { id?: true scheduleId?: true + day?: true startMinutes?: true endMinutes?: true dynamicAudio?: true @@ -91,6 +95,7 @@ export type BlockMinAggregateInputType = { export type BlockMaxAggregateInputType = { id?: true scheduleId?: true + day?: true startMinutes?: true endMinutes?: true dynamicAudio?: true @@ -101,6 +106,7 @@ export type BlockMaxAggregateInputType = { export type BlockCountAggregateInputType = { id?: true scheduleId?: true + day?: true startMinutes?: true endMinutes?: true dynamicAudio?: true @@ -198,6 +204,7 @@ export type BlockGroupByArgs | string scheduleId?: Prisma.StringFilter<"Block"> | string + day?: Prisma.StringNullableFilter<"Block"> | string | null startMinutes?: Prisma.IntFilter<"Block"> | number endMinutes?: Prisma.IntFilter<"Block"> | number dynamicAudio?: Prisma.BoolFilter<"Block"> | boolean @@ -243,6 +251,7 @@ export type BlockWhereInput = { export type BlockOrderByWithRelationInput = { id?: Prisma.SortOrder scheduleId?: Prisma.SortOrder + day?: Prisma.SortOrderInput | Prisma.SortOrder startMinutes?: Prisma.SortOrder endMinutes?: Prisma.SortOrder dynamicAudio?: Prisma.SortOrder @@ -258,6 +267,7 @@ export type BlockWhereUniqueInput = Prisma.AtLeast<{ OR?: Prisma.BlockWhereInput[] NOT?: Prisma.BlockWhereInput | Prisma.BlockWhereInput[] scheduleId?: Prisma.StringFilter<"Block"> | string + day?: Prisma.StringNullableFilter<"Block"> | string | null startMinutes?: Prisma.IntFilter<"Block"> | number endMinutes?: Prisma.IntFilter<"Block"> | number dynamicAudio?: Prisma.BoolFilter<"Block"> | boolean @@ -270,6 +280,7 @@ export type BlockWhereUniqueInput = Prisma.AtLeast<{ export type BlockOrderByWithAggregationInput = { id?: Prisma.SortOrder scheduleId?: Prisma.SortOrder + day?: Prisma.SortOrderInput | Prisma.SortOrder startMinutes?: Prisma.SortOrder endMinutes?: Prisma.SortOrder dynamicAudio?: Prisma.SortOrder @@ -288,6 +299,7 @@ export type BlockScalarWhereWithAggregatesInput = { NOT?: Prisma.BlockScalarWhereWithAggregatesInput | Prisma.BlockScalarWhereWithAggregatesInput[] id?: Prisma.StringWithAggregatesFilter<"Block"> | string scheduleId?: Prisma.StringWithAggregatesFilter<"Block"> | string + day?: Prisma.StringNullableWithAggregatesFilter<"Block"> | string | null startMinutes?: Prisma.IntWithAggregatesFilter<"Block"> | number endMinutes?: Prisma.IntWithAggregatesFilter<"Block"> | number dynamicAudio?: Prisma.BoolWithAggregatesFilter<"Block"> | boolean @@ -297,6 +309,7 @@ export type BlockScalarWhereWithAggregatesInput = { export type BlockCreateInput = { id?: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -309,6 +322,7 @@ export type BlockCreateInput = { export type BlockUncheckedCreateInput = { id?: string scheduleId: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -319,6 +333,7 @@ export type BlockUncheckedCreateInput = { export type BlockUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -331,6 +346,7 @@ export type BlockUpdateInput = { export type BlockUncheckedUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string scheduleId?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -342,6 +358,7 @@ export type BlockUncheckedUpdateInput = { export type BlockCreateManyInput = { id?: string scheduleId: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -351,6 +368,7 @@ export type BlockCreateManyInput = { export type BlockUpdateManyMutationInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -361,6 +379,7 @@ export type BlockUpdateManyMutationInput = { export type BlockUncheckedUpdateManyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string scheduleId?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -381,6 +400,7 @@ export type BlockOrderByRelationAggregateInput = { export type BlockCountOrderByAggregateInput = { id?: Prisma.SortOrder scheduleId?: Prisma.SortOrder + day?: Prisma.SortOrder startMinutes?: Prisma.SortOrder endMinutes?: Prisma.SortOrder dynamicAudio?: Prisma.SortOrder @@ -396,6 +416,7 @@ export type BlockAvgOrderByAggregateInput = { export type BlockMaxOrderByAggregateInput = { id?: Prisma.SortOrder scheduleId?: Prisma.SortOrder + day?: Prisma.SortOrder startMinutes?: Prisma.SortOrder endMinutes?: Prisma.SortOrder dynamicAudio?: Prisma.SortOrder @@ -406,6 +427,7 @@ export type BlockMaxOrderByAggregateInput = { export type BlockMinOrderByAggregateInput = { id?: Prisma.SortOrder scheduleId?: Prisma.SortOrder + day?: Prisma.SortOrder startMinutes?: Prisma.SortOrder endMinutes?: Prisma.SortOrder dynamicAudio?: Prisma.SortOrder @@ -481,6 +503,7 @@ export type BlockUpdateOneRequiredWithoutProclamationsNestedInput = { export type BlockCreateWithoutScheduleInput = { id?: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -491,6 +514,7 @@ export type BlockCreateWithoutScheduleInput = { export type BlockUncheckedCreateWithoutScheduleInput = { id?: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -531,6 +555,7 @@ export type BlockScalarWhereInput = { NOT?: Prisma.BlockScalarWhereInput | Prisma.BlockScalarWhereInput[] id?: Prisma.StringFilter<"Block"> | string scheduleId?: Prisma.StringFilter<"Block"> | string + day?: Prisma.StringNullableFilter<"Block"> | string | null startMinutes?: Prisma.IntFilter<"Block"> | number endMinutes?: Prisma.IntFilter<"Block"> | number dynamicAudio?: Prisma.BoolFilter<"Block"> | boolean @@ -540,6 +565,7 @@ export type BlockScalarWhereInput = { export type BlockCreateWithoutProclamationsInput = { id?: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -551,6 +577,7 @@ export type BlockCreateWithoutProclamationsInput = { export type BlockUncheckedCreateWithoutProclamationsInput = { id?: string scheduleId: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -576,6 +603,7 @@ export type BlockUpdateToOneWithWhereWithoutProclamationsInput = { export type BlockUpdateWithoutProclamationsInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -587,6 +615,7 @@ export type BlockUpdateWithoutProclamationsInput = { export type BlockUncheckedUpdateWithoutProclamationsInput = { id?: Prisma.StringFieldUpdateOperationsInput | string scheduleId?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -596,6 +625,7 @@ export type BlockUncheckedUpdateWithoutProclamationsInput = { export type BlockCreateManyScheduleInput = { id?: string + day?: string | null startMinutes: number endMinutes: number dynamicAudio?: boolean @@ -605,6 +635,7 @@ export type BlockCreateManyScheduleInput = { export type BlockUpdateWithoutScheduleInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -615,6 +646,7 @@ export type BlockUpdateWithoutScheduleInput = { export type BlockUncheckedUpdateWithoutScheduleInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -625,6 +657,7 @@ export type BlockUncheckedUpdateWithoutScheduleInput = { export type BlockUncheckedUpdateManyWithoutScheduleInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + day?: Prisma.NullableStringFieldUpdateOperationsInput | string | null startMinutes?: Prisma.IntFieldUpdateOperationsInput | number endMinutes?: Prisma.IntFieldUpdateOperationsInput | number dynamicAudio?: Prisma.BoolFieldUpdateOperationsInput | boolean @@ -666,6 +699,7 @@ export type BlockCountOutputTypeCountProclamationsArgs = runtime.Types.Extensions.GetSelect<{ id?: boolean scheduleId?: boolean + day?: boolean startMinutes?: boolean endMinutes?: boolean dynamicAudio?: boolean @@ -679,6 +713,7 @@ export type BlockSelect = runtime.Types.Extensions.GetSelect<{ id?: boolean scheduleId?: boolean + day?: boolean startMinutes?: boolean endMinutes?: boolean dynamicAudio?: boolean @@ -690,6 +725,7 @@ export type BlockSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ id?: boolean scheduleId?: boolean + day?: boolean startMinutes?: boolean endMinutes?: boolean dynamicAudio?: boolean @@ -701,6 +737,7 @@ export type BlockSelectUpdateManyAndReturn = runtime.Types.Extensions.GetOmit<"id" | "scheduleId" | "startMinutes" | "endMinutes" | "dynamicAudio" | "createdAt" | "updatedAt", ExtArgs["result"]["block"]> +export type BlockOmit = runtime.Types.Extensions.GetOmit<"id" | "scheduleId" | "day" | "startMinutes" | "endMinutes" | "dynamicAudio" | "createdAt" | "updatedAt", ExtArgs["result"]["block"]> export type BlockInclude = { schedule?: boolean | Prisma.ScheduleDefaultArgs proclamations?: boolean | Prisma.Block$proclamationsArgs @@ -730,6 +767,7 @@ export type $BlockPayload readonly scheduleId: Prisma.FieldRef<"Block", 'String'> + readonly day: Prisma.FieldRef<"Block", 'String'> readonly startMinutes: Prisma.FieldRef<"Block", 'Int'> readonly endMinutes: Prisma.FieldRef<"Block", 'Int'> readonly dynamicAudio: Prisma.FieldRef<"Block", 'Boolean'> diff --git a/src/lib/generated/prisma/models/LicenseKey.ts b/src/lib/generated/prisma/models/LicenseKey.ts index 4a423ca..d8912ab 100644 --- a/src/lib/generated/prisma/models/LicenseKey.ts +++ b/src/lib/generated/prisma/models/LicenseKey.ts @@ -215,6 +215,7 @@ export type LicenseKeyWhereInput = { systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null user?: Prisma.XOR | null + schedule?: Prisma.XOR | null } export type LicenseKeyOrderByWithRelationInput = { @@ -228,6 +229,7 @@ export type LicenseKeyOrderByWithRelationInput = { systemName?: Prisma.SortOrderInput | Prisma.SortOrder systemNameLower?: Prisma.SortOrderInput | Prisma.SortOrder user?: Prisma.UserOrderByWithRelationInput + schedule?: Prisma.ScheduleOrderByWithRelationInput } export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{ @@ -245,6 +247,7 @@ export type LicenseKeyWhereUniqueInput = Prisma.AtLeast<{ systemName?: Prisma.StringNullableFilter<"LicenseKey"> | string | null systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null user?: Prisma.XOR | null + schedule?: Prisma.XOR | null }, "id" | "key" | "userId_systemNameLower"> export type LicenseKeyOrderByWithAggregationInput = { @@ -287,6 +290,7 @@ export type LicenseKeyCreateInput = { systemName?: string | null systemNameLower?: string | null user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput + schedule?: Prisma.ScheduleCreateNestedOneWithoutLicenseKeyInput } export type LicenseKeyUncheckedCreateInput = { @@ -299,6 +303,7 @@ export type LicenseKeyUncheckedCreateInput = { email?: string | null systemName?: string | null systemNameLower?: string | null + schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutLicenseKeyInput } export type LicenseKeyUpdateInput = { @@ -311,6 +316,7 @@ export type LicenseKeyUpdateInput = { systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput + schedule?: Prisma.ScheduleUpdateOneWithoutLicenseKeyNestedInput } export type LicenseKeyUncheckedUpdateInput = { @@ -323,6 +329,7 @@ export type LicenseKeyUncheckedUpdateInput = { email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutLicenseKeyNestedInput } export type LicenseKeyCreateManyInput = { @@ -411,6 +418,11 @@ export type LicenseKeyMinOrderByAggregateInput = { systemNameLower?: Prisma.SortOrder } +export type LicenseKeyNullableScalarRelationFilter = { + is?: Prisma.LicenseKeyWhereInput | null + isNot?: Prisma.LicenseKeyWhereInput | null +} + export type LicenseKeyCreateNestedManyWithoutUserInput = { create?: Prisma.XOR | Prisma.LicenseKeyCreateWithoutUserInput[] | Prisma.LicenseKeyUncheckedCreateWithoutUserInput[] connectOrCreate?: Prisma.LicenseKeyCreateOrConnectWithoutUserInput | Prisma.LicenseKeyCreateOrConnectWithoutUserInput[] @@ -453,6 +465,22 @@ export type LicenseKeyUncheckedUpdateManyWithoutUserNestedInput = { deleteMany?: Prisma.LicenseKeyScalarWhereInput | Prisma.LicenseKeyScalarWhereInput[] } +export type LicenseKeyCreateNestedOneWithoutScheduleInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.LicenseKeyCreateOrConnectWithoutScheduleInput + connect?: Prisma.LicenseKeyWhereUniqueInput +} + +export type LicenseKeyUpdateOneWithoutScheduleNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.LicenseKeyCreateOrConnectWithoutScheduleInput + upsert?: Prisma.LicenseKeyUpsertWithoutScheduleInput + disconnect?: Prisma.LicenseKeyWhereInput | boolean + delete?: Prisma.LicenseKeyWhereInput | boolean + connect?: Prisma.LicenseKeyWhereUniqueInput + update?: Prisma.XOR, Prisma.LicenseKeyUncheckedUpdateWithoutScheduleInput> +} + export type LicenseKeyCreateWithoutUserInput = { id?: string key: string @@ -462,6 +490,7 @@ export type LicenseKeyCreateWithoutUserInput = { email?: string | null systemName?: string | null systemNameLower?: string | null + schedule?: Prisma.ScheduleCreateNestedOneWithoutLicenseKeyInput } export type LicenseKeyUncheckedCreateWithoutUserInput = { @@ -473,6 +502,7 @@ export type LicenseKeyUncheckedCreateWithoutUserInput = { email?: string | null systemName?: string | null systemNameLower?: string | null + schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutLicenseKeyInput } export type LicenseKeyCreateOrConnectWithoutUserInput = { @@ -516,6 +546,70 @@ export type LicenseKeyScalarWhereInput = { systemNameLower?: Prisma.StringNullableFilter<"LicenseKey"> | string | null } +export type LicenseKeyCreateWithoutScheduleInput = { + id?: string + key: string + active: boolean + createdAt?: Date | string + lastValidatedAt?: Date | string | null + email?: string | null + systemName?: string | null + systemNameLower?: string | null + user?: Prisma.UserCreateNestedOneWithoutLicenseKeysInput +} + +export type LicenseKeyUncheckedCreateWithoutScheduleInput = { + id?: string + key: string + active: boolean + createdAt?: Date | string + lastValidatedAt?: Date | string | null + userId?: string | null + email?: string | null + systemName?: string | null + systemNameLower?: string | null +} + +export type LicenseKeyCreateOrConnectWithoutScheduleInput = { + where: Prisma.LicenseKeyWhereUniqueInput + create: Prisma.XOR +} + +export type LicenseKeyUpsertWithoutScheduleInput = { + update: Prisma.XOR + create: Prisma.XOR + where?: Prisma.LicenseKeyWhereInput +} + +export type LicenseKeyUpdateToOneWithWhereWithoutScheduleInput = { + where?: Prisma.LicenseKeyWhereInput + data: Prisma.XOR +} + +export type LicenseKeyUpdateWithoutScheduleInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + key?: Prisma.StringFieldUpdateOperationsInput | string + active?: Prisma.BoolFieldUpdateOperationsInput | boolean + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + user?: Prisma.UserUpdateOneWithoutLicenseKeysNestedInput +} + +export type LicenseKeyUncheckedUpdateWithoutScheduleInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + key?: Prisma.StringFieldUpdateOperationsInput | string + active?: Prisma.BoolFieldUpdateOperationsInput | boolean + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + lastValidatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + export type LicenseKeyCreateManyUserInput = { id?: string key: string @@ -536,6 +630,7 @@ export type LicenseKeyUpdateWithoutUserInput = { email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + schedule?: Prisma.ScheduleUpdateOneWithoutLicenseKeyNestedInput } export type LicenseKeyUncheckedUpdateWithoutUserInput = { @@ -547,6 +642,7 @@ export type LicenseKeyUncheckedUpdateWithoutUserInput = { email?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null systemNameLower?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutLicenseKeyNestedInput } export type LicenseKeyUncheckedUpdateManyWithoutUserInput = { @@ -573,6 +669,7 @@ export type LicenseKeySelect + schedule?: boolean | Prisma.LicenseKey$scheduleArgs }, ExtArgs["result"]["licenseKey"]> export type LicenseKeySelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ @@ -616,6 +713,7 @@ export type LicenseKeySelectScalar = { export type LicenseKeyOmit = runtime.Types.Extensions.GetOmit<"id" | "key" | "active" | "createdAt" | "lastValidatedAt" | "userId" | "email" | "systemName" | "systemNameLower", ExtArgs["result"]["licenseKey"]> export type LicenseKeyInclude = { user?: boolean | Prisma.LicenseKey$userArgs + schedule?: boolean | Prisma.LicenseKey$scheduleArgs } export type LicenseKeyIncludeCreateManyAndReturn = { user?: boolean | Prisma.LicenseKey$userArgs @@ -628,6 +726,7 @@ export type $LicenseKeyPayload | null + schedule: Prisma.$SchedulePayload | null } scalars: runtime.Types.Extensions.GetPayloadResult<{ id: string @@ -1034,6 +1133,7 @@ readonly fields: LicenseKeyFieldRefs; export interface Prisma__LicenseKeyClient extends Prisma.PrismaPromise { readonly [Symbol.toStringTag]: "PrismaPromise" user = {}>(args?: Prisma.Subset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + schedule = {}>(args?: Prisma.Subset>): Prisma.Prisma__ScheduleClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. @@ -1491,6 +1591,25 @@ export type LicenseKey$userArgs = { + /** + * Select specific fields to fetch from the Schedule + */ + select?: Prisma.ScheduleSelect | null + /** + * Omit specific fields from the Schedule + */ + omit?: Prisma.ScheduleOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ScheduleInclude | null + where?: Prisma.ScheduleWhereInput +} + /** * LicenseKey without action */ diff --git a/src/lib/generated/prisma/models/Schedule.ts b/src/lib/generated/prisma/models/Schedule.ts index 88ca289..972dd15 100644 --- a/src/lib/generated/prisma/models/Schedule.ts +++ b/src/lib/generated/prisma/models/Schedule.ts @@ -26,21 +26,27 @@ export type AggregateSchedule = { export type ScheduleMinAggregateOutputType = { id: string | null + licenseKeyId: string | null userId: string | null + type: string | null createdAt: Date | null updatedAt: Date | null } export type ScheduleMaxAggregateOutputType = { id: string | null + licenseKeyId: string | null userId: string | null + type: string | null createdAt: Date | null updatedAt: Date | null } export type ScheduleCountAggregateOutputType = { id: number + licenseKeyId: number userId: number + type: number createdAt: number updatedAt: number _all: number @@ -49,21 +55,27 @@ export type ScheduleCountAggregateOutputType = { export type ScheduleMinAggregateInputType = { id?: true + licenseKeyId?: true userId?: true + type?: true createdAt?: true updatedAt?: true } export type ScheduleMaxAggregateInputType = { id?: true + licenseKeyId?: true userId?: true + type?: true createdAt?: true updatedAt?: true } export type ScheduleCountAggregateInputType = { id?: true + licenseKeyId?: true userId?: true + type?: true createdAt?: true updatedAt?: true _all?: true @@ -143,7 +155,9 @@ export type ScheduleGroupByArgs | string - userId?: Prisma.StringFilter<"Schedule"> | string + licenseKeyId?: Prisma.StringNullableFilter<"Schedule"> | string | null + userId?: Prisma.StringNullableFilter<"Schedule"> | string | null + type?: Prisma.StringNullableFilter<"Schedule"> | string | null createdAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string - user?: Prisma.XOR + licenseKey?: Prisma.XOR | null + user?: Prisma.XOR | null blocks?: Prisma.BlockListRelationFilter } export type ScheduleOrderByWithRelationInput = { id?: Prisma.SortOrder - userId?: Prisma.SortOrder + licenseKeyId?: Prisma.SortOrderInput | Prisma.SortOrder + userId?: Prisma.SortOrderInput | Prisma.SortOrder + type?: Prisma.SortOrderInput | Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder + licenseKey?: Prisma.LicenseKeyOrderByWithRelationInput user?: Prisma.UserOrderByWithRelationInput blocks?: Prisma.BlockOrderByRelationAggregateInput } export type ScheduleWhereUniqueInput = Prisma.AtLeast<{ id?: string - userId?: string + licenseKeyId?: string AND?: Prisma.ScheduleWhereInput | Prisma.ScheduleWhereInput[] OR?: Prisma.ScheduleWhereInput[] NOT?: Prisma.ScheduleWhereInput | Prisma.ScheduleWhereInput[] + userId?: Prisma.StringNullableFilter<"Schedule"> | string | null + type?: Prisma.StringNullableFilter<"Schedule"> | string | null createdAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string - user?: Prisma.XOR + licenseKey?: Prisma.XOR | null + user?: Prisma.XOR | null blocks?: Prisma.BlockListRelationFilter -}, "id" | "userId"> +}, "id" | "licenseKeyId"> export type ScheduleOrderByWithAggregationInput = { id?: Prisma.SortOrder - userId?: Prisma.SortOrder + licenseKeyId?: Prisma.SortOrderInput | Prisma.SortOrder + userId?: Prisma.SortOrderInput | Prisma.SortOrder + type?: Prisma.SortOrderInput | Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder _count?: Prisma.ScheduleCountOrderByAggregateInput @@ -214,22 +239,28 @@ export type ScheduleScalarWhereWithAggregatesInput = { OR?: Prisma.ScheduleScalarWhereWithAggregatesInput[] NOT?: Prisma.ScheduleScalarWhereWithAggregatesInput | Prisma.ScheduleScalarWhereWithAggregatesInput[] id?: Prisma.StringWithAggregatesFilter<"Schedule"> | string - userId?: Prisma.StringWithAggregatesFilter<"Schedule"> | string + licenseKeyId?: Prisma.StringNullableWithAggregatesFilter<"Schedule"> | string | null + userId?: Prisma.StringNullableWithAggregatesFilter<"Schedule"> | string | null + type?: Prisma.StringNullableWithAggregatesFilter<"Schedule"> | string | null createdAt?: Prisma.DateTimeWithAggregatesFilter<"Schedule"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Schedule"> | Date | string } export type ScheduleCreateInput = { id?: string + type?: string | null createdAt?: Date | string updatedAt?: Date | string - user: Prisma.UserCreateNestedOneWithoutScheduleInput + licenseKey?: Prisma.LicenseKeyCreateNestedOneWithoutScheduleInput + user?: Prisma.UserCreateNestedOneWithoutSchedulesInput blocks?: Prisma.BlockCreateNestedManyWithoutScheduleInput } export type ScheduleUncheckedCreateInput = { id?: string - userId: string + licenseKeyId?: string | null + userId?: string | null + type?: string | null createdAt?: Date | string updatedAt?: Date | string blocks?: Prisma.BlockUncheckedCreateNestedManyWithoutScheduleInput @@ -237,15 +268,19 @@ export type ScheduleUncheckedCreateInput = { export type ScheduleUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string - user?: Prisma.UserUpdateOneRequiredWithoutScheduleNestedInput + licenseKey?: Prisma.LicenseKeyUpdateOneWithoutScheduleNestedInput + user?: Prisma.UserUpdateOneWithoutSchedulesNestedInput blocks?: Prisma.BlockUpdateManyWithoutScheduleNestedInput } export type ScheduleUncheckedUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - userId?: Prisma.StringFieldUpdateOperationsInput | string + licenseKeyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string blocks?: Prisma.BlockUncheckedUpdateManyWithoutScheduleNestedInput @@ -253,24 +288,39 @@ export type ScheduleUncheckedUpdateInput = { export type ScheduleCreateManyInput = { id?: string - userId: string + licenseKeyId?: string | null + userId?: string | null + type?: string | null createdAt?: Date | string updatedAt?: Date | string } export type ScheduleUpdateManyMutationInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } export type ScheduleUncheckedUpdateManyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - userId?: Prisma.StringFieldUpdateOperationsInput | string + licenseKeyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } +export type ScheduleListRelationFilter = { + every?: Prisma.ScheduleWhereInput + some?: Prisma.ScheduleWhereInput + none?: Prisma.ScheduleWhereInput +} + +export type ScheduleOrderByRelationAggregateInput = { + _count?: Prisma.SortOrder +} + export type ScheduleNullableScalarRelationFilter = { is?: Prisma.ScheduleWhereInput | null isNot?: Prisma.ScheduleWhereInput | null @@ -278,21 +328,27 @@ export type ScheduleNullableScalarRelationFilter = { export type ScheduleCountOrderByAggregateInput = { id?: Prisma.SortOrder + licenseKeyId?: Prisma.SortOrder userId?: Prisma.SortOrder + type?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } export type ScheduleMaxOrderByAggregateInput = { id?: Prisma.SortOrder + licenseKeyId?: Prisma.SortOrder userId?: Prisma.SortOrder + type?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } export type ScheduleMinOrderByAggregateInput = { id?: Prisma.SortOrder + licenseKeyId?: Prisma.SortOrder userId?: Prisma.SortOrder + type?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } @@ -302,36 +358,78 @@ export type ScheduleScalarRelationFilter = { isNot?: Prisma.ScheduleWhereInput } -export type ScheduleCreateNestedOneWithoutUserInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput +export type ScheduleCreateNestedManyWithoutUserInput = { + create?: Prisma.XOR | Prisma.ScheduleCreateWithoutUserInput[] | Prisma.ScheduleUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput | Prisma.ScheduleCreateOrConnectWithoutUserInput[] + createMany?: Prisma.ScheduleCreateManyUserInputEnvelope + connect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] +} + +export type ScheduleUncheckedCreateNestedManyWithoutUserInput = { + create?: Prisma.XOR | Prisma.ScheduleCreateWithoutUserInput[] | Prisma.ScheduleUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput | Prisma.ScheduleCreateOrConnectWithoutUserInput[] + createMany?: Prisma.ScheduleCreateManyUserInputEnvelope + connect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] +} + +export type ScheduleUpdateManyWithoutUserNestedInput = { + create?: Prisma.XOR | Prisma.ScheduleCreateWithoutUserInput[] | Prisma.ScheduleUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput | Prisma.ScheduleCreateOrConnectWithoutUserInput[] + upsert?: Prisma.ScheduleUpsertWithWhereUniqueWithoutUserInput | Prisma.ScheduleUpsertWithWhereUniqueWithoutUserInput[] + createMany?: Prisma.ScheduleCreateManyUserInputEnvelope + set?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + disconnect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + delete?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + connect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + update?: Prisma.ScheduleUpdateWithWhereUniqueWithoutUserInput | Prisma.ScheduleUpdateWithWhereUniqueWithoutUserInput[] + updateMany?: Prisma.ScheduleUpdateManyWithWhereWithoutUserInput | Prisma.ScheduleUpdateManyWithWhereWithoutUserInput[] + deleteMany?: Prisma.ScheduleScalarWhereInput | Prisma.ScheduleScalarWhereInput[] +} + +export type ScheduleUncheckedUpdateManyWithoutUserNestedInput = { + create?: Prisma.XOR | Prisma.ScheduleCreateWithoutUserInput[] | Prisma.ScheduleUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput | Prisma.ScheduleCreateOrConnectWithoutUserInput[] + upsert?: Prisma.ScheduleUpsertWithWhereUniqueWithoutUserInput | Prisma.ScheduleUpsertWithWhereUniqueWithoutUserInput[] + createMany?: Prisma.ScheduleCreateManyUserInputEnvelope + set?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + disconnect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + delete?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + connect?: Prisma.ScheduleWhereUniqueInput | Prisma.ScheduleWhereUniqueInput[] + update?: Prisma.ScheduleUpdateWithWhereUniqueWithoutUserInput | Prisma.ScheduleUpdateWithWhereUniqueWithoutUserInput[] + updateMany?: Prisma.ScheduleUpdateManyWithWhereWithoutUserInput | Prisma.ScheduleUpdateManyWithWhereWithoutUserInput[] + deleteMany?: Prisma.ScheduleScalarWhereInput | Prisma.ScheduleScalarWhereInput[] +} + +export type ScheduleCreateNestedOneWithoutLicenseKeyInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutLicenseKeyInput connect?: Prisma.ScheduleWhereUniqueInput } -export type ScheduleUncheckedCreateNestedOneWithoutUserInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput +export type ScheduleUncheckedCreateNestedOneWithoutLicenseKeyInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutLicenseKeyInput connect?: Prisma.ScheduleWhereUniqueInput } -export type ScheduleUpdateOneWithoutUserNestedInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput - upsert?: Prisma.ScheduleUpsertWithoutUserInput +export type ScheduleUpdateOneWithoutLicenseKeyNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutLicenseKeyInput + upsert?: Prisma.ScheduleUpsertWithoutLicenseKeyInput disconnect?: Prisma.ScheduleWhereInput | boolean delete?: Prisma.ScheduleWhereInput | boolean connect?: Prisma.ScheduleWhereUniqueInput - update?: Prisma.XOR, Prisma.ScheduleUncheckedUpdateWithoutUserInput> + update?: Prisma.XOR, Prisma.ScheduleUncheckedUpdateWithoutLicenseKeyInput> } -export type ScheduleUncheckedUpdateOneWithoutUserNestedInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutUserInput - upsert?: Prisma.ScheduleUpsertWithoutUserInput +export type ScheduleUncheckedUpdateOneWithoutLicenseKeyNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.ScheduleCreateOrConnectWithoutLicenseKeyInput + upsert?: Prisma.ScheduleUpsertWithoutLicenseKeyInput disconnect?: Prisma.ScheduleWhereInput | boolean delete?: Prisma.ScheduleWhereInput | boolean connect?: Prisma.ScheduleWhereUniqueInput - update?: Prisma.XOR, Prisma.ScheduleUncheckedUpdateWithoutUserInput> + update?: Prisma.XOR, Prisma.ScheduleUncheckedUpdateWithoutLicenseKeyInput> } export type ScheduleCreateNestedOneWithoutBlocksInput = { @@ -350,13 +448,17 @@ export type ScheduleUpdateOneRequiredWithoutBlocksNestedInput = { export type ScheduleCreateWithoutUserInput = { id?: string + type?: string | null createdAt?: Date | string updatedAt?: Date | string + licenseKey?: Prisma.LicenseKeyCreateNestedOneWithoutScheduleInput blocks?: Prisma.BlockCreateNestedManyWithoutScheduleInput } export type ScheduleUncheckedCreateWithoutUserInput = { id?: string + licenseKeyId?: string | null + type?: string | null createdAt?: Date | string updatedAt?: Date | string blocks?: Prisma.BlockUncheckedCreateNestedManyWithoutScheduleInput @@ -367,26 +469,86 @@ export type ScheduleCreateOrConnectWithoutUserInput = { create: Prisma.XOR } -export type ScheduleUpsertWithoutUserInput = { - update: Prisma.XOR - create: Prisma.XOR - where?: Prisma.ScheduleWhereInput +export type ScheduleCreateManyUserInputEnvelope = { + data: Prisma.ScheduleCreateManyUserInput | Prisma.ScheduleCreateManyUserInput[] + skipDuplicates?: boolean } -export type ScheduleUpdateToOneWithWhereWithoutUserInput = { - where?: Prisma.ScheduleWhereInput +export type ScheduleUpsertWithWhereUniqueWithoutUserInput = { + where: Prisma.ScheduleWhereUniqueInput + update: Prisma.XOR + create: Prisma.XOR +} + +export type ScheduleUpdateWithWhereUniqueWithoutUserInput = { + where: Prisma.ScheduleWhereUniqueInput data: Prisma.XOR } -export type ScheduleUpdateWithoutUserInput = { +export type ScheduleUpdateManyWithWhereWithoutUserInput = { + where: Prisma.ScheduleScalarWhereInput + data: Prisma.XOR +} + +export type ScheduleScalarWhereInput = { + AND?: Prisma.ScheduleScalarWhereInput | Prisma.ScheduleScalarWhereInput[] + OR?: Prisma.ScheduleScalarWhereInput[] + NOT?: Prisma.ScheduleScalarWhereInput | Prisma.ScheduleScalarWhereInput[] + id?: Prisma.StringFilter<"Schedule"> | string + licenseKeyId?: Prisma.StringNullableFilter<"Schedule"> | string | null + userId?: Prisma.StringNullableFilter<"Schedule"> | string | null + type?: Prisma.StringNullableFilter<"Schedule"> | string | null + createdAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Schedule"> | Date | string +} + +export type ScheduleCreateWithoutLicenseKeyInput = { + id?: string + type?: string | null + createdAt?: Date | string + updatedAt?: Date | string + user?: Prisma.UserCreateNestedOneWithoutSchedulesInput + blocks?: Prisma.BlockCreateNestedManyWithoutScheduleInput +} + +export type ScheduleUncheckedCreateWithoutLicenseKeyInput = { + id?: string + userId?: string | null + type?: string | null + createdAt?: Date | string + updatedAt?: Date | string + blocks?: Prisma.BlockUncheckedCreateNestedManyWithoutScheduleInput +} + +export type ScheduleCreateOrConnectWithoutLicenseKeyInput = { + where: Prisma.ScheduleWhereUniqueInput + create: Prisma.XOR +} + +export type ScheduleUpsertWithoutLicenseKeyInput = { + update: Prisma.XOR + create: Prisma.XOR + where?: Prisma.ScheduleWhereInput +} + +export type ScheduleUpdateToOneWithWhereWithoutLicenseKeyInput = { + where?: Prisma.ScheduleWhereInput + data: Prisma.XOR +} + +export type ScheduleUpdateWithoutLicenseKeyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + user?: Prisma.UserUpdateOneWithoutSchedulesNestedInput blocks?: Prisma.BlockUpdateManyWithoutScheduleNestedInput } -export type ScheduleUncheckedUpdateWithoutUserInput = { +export type ScheduleUncheckedUpdateWithoutLicenseKeyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string blocks?: Prisma.BlockUncheckedUpdateManyWithoutScheduleNestedInput @@ -394,14 +556,18 @@ export type ScheduleUncheckedUpdateWithoutUserInput = { export type ScheduleCreateWithoutBlocksInput = { id?: string + type?: string | null createdAt?: Date | string updatedAt?: Date | string - user: Prisma.UserCreateNestedOneWithoutScheduleInput + licenseKey?: Prisma.LicenseKeyCreateNestedOneWithoutScheduleInput + user?: Prisma.UserCreateNestedOneWithoutSchedulesInput } export type ScheduleUncheckedCreateWithoutBlocksInput = { id?: string - userId: string + licenseKeyId?: string | null + userId?: string | null + type?: string | null createdAt?: Date | string updatedAt?: Date | string } @@ -424,14 +590,52 @@ export type ScheduleUpdateToOneWithWhereWithoutBlocksInput = { export type ScheduleUpdateWithoutBlocksInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string - user?: Prisma.UserUpdateOneRequiredWithoutScheduleNestedInput + licenseKey?: Prisma.LicenseKeyUpdateOneWithoutScheduleNestedInput + user?: Prisma.UserUpdateOneWithoutSchedulesNestedInput } export type ScheduleUncheckedUpdateWithoutBlocksInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - userId?: Prisma.StringFieldUpdateOperationsInput | string + licenseKeyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ScheduleCreateManyUserInput = { + id?: string + licenseKeyId?: string | null + type?: string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ScheduleUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + licenseKey?: Prisma.LicenseKeyUpdateOneWithoutScheduleNestedInput + blocks?: Prisma.BlockUpdateManyWithoutScheduleNestedInput +} + +export type ScheduleUncheckedUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + licenseKeyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + blocks?: Prisma.BlockUncheckedUpdateManyWithoutScheduleNestedInput +} + +export type ScheduleUncheckedUpdateManyWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + licenseKeyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + type?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -469,59 +673,76 @@ export type ScheduleCountOutputTypeCountBlocksArgs = runtime.Types.Extensions.GetSelect<{ id?: boolean + licenseKeyId?: boolean userId?: boolean + type?: boolean createdAt?: boolean updatedAt?: boolean - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs blocks?: boolean | Prisma.Schedule$blocksArgs _count?: boolean | Prisma.ScheduleCountOutputTypeDefaultArgs }, ExtArgs["result"]["schedule"]> export type ScheduleSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ id?: boolean + licenseKeyId?: boolean userId?: boolean + type?: boolean createdAt?: boolean updatedAt?: boolean - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs }, ExtArgs["result"]["schedule"]> export type ScheduleSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ id?: boolean + licenseKeyId?: boolean userId?: boolean + type?: boolean createdAt?: boolean updatedAt?: boolean - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs }, ExtArgs["result"]["schedule"]> export type ScheduleSelectScalar = { id?: boolean + licenseKeyId?: boolean userId?: boolean + type?: boolean createdAt?: boolean updatedAt?: boolean } -export type ScheduleOmit = runtime.Types.Extensions.GetOmit<"id" | "userId" | "createdAt" | "updatedAt", ExtArgs["result"]["schedule"]> +export type ScheduleOmit = runtime.Types.Extensions.GetOmit<"id" | "licenseKeyId" | "userId" | "type" | "createdAt" | "updatedAt", ExtArgs["result"]["schedule"]> export type ScheduleInclude = { - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs blocks?: boolean | Prisma.Schedule$blocksArgs _count?: boolean | Prisma.ScheduleCountOutputTypeDefaultArgs } export type ScheduleIncludeCreateManyAndReturn = { - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs } export type ScheduleIncludeUpdateManyAndReturn = { - user?: boolean | Prisma.UserDefaultArgs + licenseKey?: boolean | Prisma.Schedule$licenseKeyArgs + user?: boolean | Prisma.Schedule$userArgs } export type $SchedulePayload = { name: "Schedule" objects: { - user: Prisma.$UserPayload + licenseKey: Prisma.$LicenseKeyPayload | null + user: Prisma.$UserPayload | null blocks: Prisma.$BlockPayload[] } scalars: runtime.Types.Extensions.GetPayloadResult<{ id: string - userId: string + licenseKeyId: string | null + userId: string | null + type: string | null createdAt: Date updatedAt: Date }, ExtArgs["result"]["schedule"]> @@ -918,7 +1139,8 @@ readonly fields: ScheduleFieldRefs; */ export interface Prisma__ScheduleClient extends Prisma.PrismaPromise { readonly [Symbol.toStringTag]: "PrismaPromise" - user = {}>(args?: Prisma.Subset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions> + licenseKey = {}>(args?: Prisma.Subset>): Prisma.Prisma__LicenseKeyClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + user = {}>(args?: Prisma.Subset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> blocks = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> /** * Attaches callbacks for the resolution and/or rejection of the Promise. @@ -950,7 +1172,9 @@ export interface Prisma__ScheduleClient + readonly licenseKeyId: Prisma.FieldRef<"Schedule", 'String'> readonly userId: Prisma.FieldRef<"Schedule", 'String'> + readonly type: Prisma.FieldRef<"Schedule", 'String'> readonly createdAt: Prisma.FieldRef<"Schedule", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"Schedule", 'DateTime'> } @@ -1353,6 +1577,44 @@ export type ScheduleDeleteManyArgs = { + /** + * Select specific fields to fetch from the LicenseKey + */ + select?: Prisma.LicenseKeySelect | null + /** + * Omit specific fields from the LicenseKey + */ + omit?: Prisma.LicenseKeyOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.LicenseKeyInclude | null + where?: Prisma.LicenseKeyWhereInput +} + +/** + * Schedule.user + */ +export type Schedule$userArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + where?: Prisma.UserWhereInput +} + /** * Schedule.blocks */ diff --git a/src/lib/generated/prisma/models/User.ts b/src/lib/generated/prisma/models/User.ts index 7ee86a5..5254841 100644 --- a/src/lib/generated/prisma/models/User.ts +++ b/src/lib/generated/prisma/models/User.ts @@ -246,7 +246,7 @@ export type UserWhereInput = { profiles?: Prisma.ProfileListRelationFilter licenseKeys?: Prisma.LicenseKeyListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter - schedule?: Prisma.XOR | null + schedules?: Prisma.ScheduleListRelationFilter } export type UserOrderByWithRelationInput = { @@ -264,7 +264,7 @@ export type UserOrderByWithRelationInput = { profiles?: Prisma.ProfileOrderByRelationAggregateInput licenseKeys?: Prisma.LicenseKeyOrderByRelationAggregateInput userProclamations?: Prisma.UserProclamationsOrderByRelationAggregateInput - schedule?: Prisma.ScheduleOrderByWithRelationInput + schedules?: Prisma.ScheduleOrderByRelationAggregateInput } export type UserWhereUniqueInput = Prisma.AtLeast<{ @@ -285,7 +285,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{ profiles?: Prisma.ProfileListRelationFilter licenseKeys?: Prisma.LicenseKeyListRelationFilter userProclamations?: Prisma.UserProclamationsListRelationFilter - schedule?: Prisma.XOR | null + schedules?: Prisma.ScheduleListRelationFilter }, "id" | "email"> export type UserOrderByWithAggregationInput = { @@ -332,7 +332,7 @@ export type UserCreateInput = { profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateInput = { @@ -349,7 +349,7 @@ export type UserUncheckedCreateInput = { profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserUpdateInput = { @@ -366,7 +366,7 @@ export type UserUpdateInput = { profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateInput = { @@ -383,7 +383,7 @@ export type UserUncheckedUpdateInput = { profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserCreateManyInput = { @@ -619,18 +619,20 @@ export type UserUpdateOneWithoutLicenseKeysNestedInput = { update?: Prisma.XOR, Prisma.UserUncheckedUpdateWithoutLicenseKeysInput> } -export type UserCreateNestedOneWithoutScheduleInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.UserCreateOrConnectWithoutScheduleInput +export type UserCreateNestedOneWithoutSchedulesInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.UserCreateOrConnectWithoutSchedulesInput connect?: Prisma.UserWhereUniqueInput } -export type UserUpdateOneRequiredWithoutScheduleNestedInput = { - create?: Prisma.XOR - connectOrCreate?: Prisma.UserCreateOrConnectWithoutScheduleInput - upsert?: Prisma.UserUpsertWithoutScheduleInput +export type UserUpdateOneWithoutSchedulesNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.UserCreateOrConnectWithoutSchedulesInput + upsert?: Prisma.UserUpsertWithoutSchedulesInput + disconnect?: Prisma.UserWhereInput | boolean + delete?: Prisma.UserWhereInput | boolean connect?: Prisma.UserWhereUniqueInput - update?: Prisma.XOR, Prisma.UserUncheckedUpdateWithoutScheduleInput> + update?: Prisma.XOR, Prisma.UserUncheckedUpdateWithoutSchedulesInput> } export type UserCreateWithoutProfilesInput = { @@ -646,7 +648,7 @@ export type UserCreateWithoutProfilesInput = { accounts?: Prisma.AccountCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutProfilesInput = { @@ -662,7 +664,7 @@ export type UserUncheckedCreateWithoutProfilesInput = { accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutProfilesInput = { @@ -694,7 +696,7 @@ export type UserUpdateWithoutProfilesInput = { accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutProfilesInput = { @@ -710,7 +712,7 @@ export type UserUncheckedUpdateWithoutProfilesInput = { accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserCreateWithoutSessionsInput = { @@ -726,7 +728,7 @@ export type UserCreateWithoutSessionsInput = { profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutSessionsInput = { @@ -742,7 +744,7 @@ export type UserUncheckedCreateWithoutSessionsInput = { profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutSessionsInput = { @@ -774,7 +776,7 @@ export type UserUpdateWithoutSessionsInput = { profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutSessionsInput = { @@ -790,7 +792,7 @@ export type UserUncheckedUpdateWithoutSessionsInput = { profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserCreateWithoutAccountsInput = { @@ -806,7 +808,7 @@ export type UserCreateWithoutAccountsInput = { profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutAccountsInput = { @@ -822,7 +824,7 @@ export type UserUncheckedCreateWithoutAccountsInput = { profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutAccountsInput = { @@ -854,7 +856,7 @@ export type UserUpdateWithoutAccountsInput = { profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutAccountsInput = { @@ -870,7 +872,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = { profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserCreateWithoutRoleInput = { @@ -886,7 +888,7 @@ export type UserCreateWithoutRoleInput = { profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutRoleInput = { @@ -902,7 +904,7 @@ export type UserUncheckedCreateWithoutRoleInput = { profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutRoleInput = { @@ -958,7 +960,7 @@ export type UserCreateWithoutUserProclamationsInput = { accounts?: Prisma.AccountCreateNestedManyWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutUserProclamationsInput = { @@ -974,7 +976,7 @@ export type UserUncheckedCreateWithoutUserProclamationsInput = { accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput licenseKeys?: Prisma.LicenseKeyUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutUserProclamationsInput = { @@ -1006,7 +1008,7 @@ export type UserUpdateWithoutUserProclamationsInput = { accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutUserProclamationsInput = { @@ -1022,7 +1024,7 @@ export type UserUncheckedUpdateWithoutUserProclamationsInput = { accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserCreateWithoutLicenseKeysInput = { @@ -1038,7 +1040,7 @@ export type UserCreateWithoutLicenseKeysInput = { accounts?: Prisma.AccountCreateNestedManyWithoutUserInput profiles?: Prisma.ProfileCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleCreateNestedManyWithoutUserInput } export type UserUncheckedCreateWithoutLicenseKeysInput = { @@ -1054,7 +1056,7 @@ export type UserUncheckedCreateWithoutLicenseKeysInput = { accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput profiles?: Prisma.ProfileUncheckedCreateNestedManyWithoutUserInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput - schedule?: Prisma.ScheduleUncheckedCreateNestedOneWithoutUserInput + schedules?: Prisma.ScheduleUncheckedCreateNestedManyWithoutUserInput } export type UserCreateOrConnectWithoutLicenseKeysInput = { @@ -1086,7 +1088,7 @@ export type UserUpdateWithoutLicenseKeysInput = { accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutLicenseKeysInput = { @@ -1102,10 +1104,10 @@ export type UserUncheckedUpdateWithoutLicenseKeysInput = { accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } -export type UserCreateWithoutScheduleInput = { +export type UserCreateWithoutSchedulesInput = { id: string name: string email: string @@ -1121,7 +1123,7 @@ export type UserCreateWithoutScheduleInput = { userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutUserInput } -export type UserUncheckedCreateWithoutScheduleInput = { +export type UserUncheckedCreateWithoutSchedulesInput = { id: string name: string email: string @@ -1137,23 +1139,23 @@ export type UserUncheckedCreateWithoutScheduleInput = { userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutUserInput } -export type UserCreateOrConnectWithoutScheduleInput = { +export type UserCreateOrConnectWithoutSchedulesInput = { where: Prisma.UserWhereUniqueInput - create: Prisma.XOR + create: Prisma.XOR } -export type UserUpsertWithoutScheduleInput = { - update: Prisma.XOR - create: Prisma.XOR +export type UserUpsertWithoutSchedulesInput = { + update: Prisma.XOR + create: Prisma.XOR where?: Prisma.UserWhereInput } -export type UserUpdateToOneWithWhereWithoutScheduleInput = { +export type UserUpdateToOneWithWhereWithoutSchedulesInput = { where?: Prisma.UserWhereInput - data: Prisma.XOR + data: Prisma.XOR } -export type UserUpdateWithoutScheduleInput = { +export type UserUpdateWithoutSchedulesInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string @@ -1169,7 +1171,7 @@ export type UserUpdateWithoutScheduleInput = { userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput } -export type UserUncheckedUpdateWithoutScheduleInput = { +export type UserUncheckedUpdateWithoutSchedulesInput = { id?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string email?: Prisma.StringFieldUpdateOperationsInput | string @@ -1208,7 +1210,7 @@ export type UserUpdateWithoutRoleInput = { profiles?: Prisma.ProfileUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateWithoutRoleInput = { @@ -1224,7 +1226,7 @@ export type UserUncheckedUpdateWithoutRoleInput = { profiles?: Prisma.ProfileUncheckedUpdateManyWithoutUserNestedInput licenseKeys?: Prisma.LicenseKeyUncheckedUpdateManyWithoutUserNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutUserNestedInput - schedule?: Prisma.ScheduleUncheckedUpdateOneWithoutUserNestedInput + schedules?: Prisma.ScheduleUncheckedUpdateManyWithoutUserNestedInput } export type UserUncheckedUpdateManyWithoutRoleInput = { @@ -1248,6 +1250,7 @@ export type UserCountOutputType = { profiles: number licenseKeys: number userProclamations: number + schedules: number } export type UserCountOutputTypeSelect = { @@ -1256,6 +1259,7 @@ export type UserCountOutputTypeSelect = { + where?: Prisma.ScheduleWhereInput +} + export type UserSelect = runtime.Types.Extensions.GetSelect<{ id?: boolean @@ -1319,7 +1330,7 @@ export type UserSelect licenseKeys?: boolean | Prisma.User$licenseKeysArgs userProclamations?: boolean | Prisma.User$userProclamationsArgs - schedule?: boolean | Prisma.User$scheduleArgs + schedules?: boolean | Prisma.User$schedulesArgs _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs }, ExtArgs["result"]["user"]> @@ -1366,7 +1377,7 @@ export type UserInclude licenseKeys?: boolean | Prisma.User$licenseKeysArgs userProclamations?: boolean | Prisma.User$userProclamationsArgs - schedule?: boolean | Prisma.User$scheduleArgs + schedules?: boolean | Prisma.User$schedulesArgs _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs } export type UserIncludeCreateManyAndReturn = { @@ -1385,7 +1396,7 @@ export type $UserPayload[] licenseKeys: Prisma.$LicenseKeyPayload[] userProclamations: Prisma.$UserProclamationsPayload[] - schedule: Prisma.$SchedulePayload | null + schedules: Prisma.$SchedulePayload[] } scalars: runtime.Types.Extensions.GetPayloadResult<{ id: string @@ -1796,7 +1807,7 @@ export interface Prisma__UserClient = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> licenseKeys = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> userProclamations = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> - schedule = {}>(args?: Prisma.Subset>): Prisma.Prisma__ScheduleClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + schedules = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. @@ -2374,9 +2385,9 @@ export type User$userProclamationsArgs = { +export type User$schedulesArgs = { /** * Select specific fields to fetch from the Schedule */ @@ -2390,6 +2401,11 @@ export type User$scheduleArgs | null where?: Prisma.ScheduleWhereInput + orderBy?: Prisma.ScheduleOrderByWithRelationInput | Prisma.ScheduleOrderByWithRelationInput[] + cursor?: Prisma.ScheduleWhereUniqueInput + take?: number + skip?: number + distinct?: Prisma.ScheduleScalarFieldEnum | Prisma.ScheduleScalarFieldEnum[] } /** From 3bb8e0189bd42f5ed1e9fa1427bf8e096ea62d95 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:22:38 -0500 Subject: [PATCH 09/25] Portal: per-license schedule service, shared validator, and key-only GET Implements the shared validateSchedule validator (daily + weekly, midnight spillover, Sunday->Monday wrap, adjacency, empty-block rejection), a per-License-Key schedule service (get/set type, CRUD blocks, weekly prefill with confirmation), the content-hash version/ETag serializer, the new GET /api/v2/licenses/[licenseKey]/schedule (200/304/404, type null), and additive evolution of the legacy GET /api/v2/schedule. Adds validator, serializer (against docs/fixtures), endpoint ETag/304, and service tests. --- src/lib/schedule-utils.spec.ts | 97 ++++++ src/lib/schedule-utils.ts | 181 ++++++++++ src/lib/server/schedule.spec.ts | 185 +++++----- src/lib/server/schedule.ts | 324 +++++++++++++++--- src/lib/server/v2-schedule.spec.ts | 169 +++++++++ src/lib/server/v2-serializers.ts | 65 ++++ .../(user)/schedule/+page.server.ts | 126 +++++-- .../(user)/schedule/page.server.spec.ts | 109 +++--- .../licenses/[licenseKey]/schedule/+server.ts | 48 +++ .../[licenseKey]/schedule/server.spec.ts | 105 ++++++ src/routes/api/v2/schedule/+server.ts | 26 +- src/routes/api/v2/schedule/schedule.spec.ts | 81 +++-- 12 files changed, 1285 insertions(+), 231 deletions(-) create mode 100644 src/lib/server/v2-schedule.spec.ts create mode 100644 src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts create mode 100644 src/routes/api/v2/licenses/[licenseKey]/schedule/server.spec.ts diff --git a/src/lib/schedule-utils.spec.ts b/src/lib/schedule-utils.spec.ts index 8d27ef5..b0317d0 100644 --- a/src/lib/schedule-utils.spec.ts +++ b/src/lib/schedule-utils.spec.ts @@ -3,6 +3,7 @@ import { blockDuration, blockSegments, findOverlap, + findScheduleOverlap, fiveMinuteOptions, formatTime, hmToMinutes, @@ -11,6 +12,8 @@ import { snapMinutes, toHm, validateBlock, + validateBlockShape, + validateSchedule, type BlockInput, type BlockSibling } from './schedule-utils'; @@ -282,3 +285,97 @@ describe('validateBlock', () => { expect(validateBlock(input({ startMinutes: 300, endMinutes: 360 }), siblings).ok).toBe(true); }); }); + +// ─── Schedule-wide validation (daily + weekly) ───────────────────────── + +const block = ( + startMinutes: number, + endMinutes: number, + day: string | null = null, + proclamationIds: string[] = ['p1'] +) => ({ day, startMinutes, endMinutes, proclamationIds }); + +describe('validateBlockShape', () => { + it('rejects start == end', () => { + expect(validateBlockShape(block(360, 360))).toEqual({ + ok: false, + error: "Start and end time can't be the same." + }); + }); + + it('rejects an empty block', () => { + expect(validateBlockShape(block(360, 420, null, []))).toEqual({ + ok: false, + error: 'Select at least one proclamation.' + }); + }); + + it('accepts a single-minute block', () => { + expect(validateBlockShape(block(360, 361))).toEqual({ ok: true }); + }); +}); + +describe('validateSchedule daily', () => { + it('accepts adjacent blocks (touching boundaries)', () => { + expect(validateSchedule('daily', [block(360, 420), block(420, 510)])).toEqual({ ok: true }); + }); + + it('accepts a single-minute block', () => { + expect(validateSchedule('daily', [block(360, 361)])).toEqual({ ok: true }); + }); + + it('rejects overlapping blocks', () => { + expect(validateSchedule('daily', [block(360, 480), block(420, 540)]).ok).toBe(false); + }); + + it('rejects a midnight-crossing block that spills into an early block', () => { + // 21:00→09:00 covers [0,540) on the next day; 08:00→08:30 overlaps. + expect(validateSchedule('daily', [block(1260, 540, null), block(480, 510, null)]).ok).toBe( + false + ); + }); + + it('accepts a crossing block and its exact complement', () => { + expect(validateSchedule('daily', [block(1260, 540, null), block(540, 1260, null)])).toEqual({ + ok: true + }); + }); +}); + +describe('validateSchedule weekly', () => { + it('applies rules per day', () => { + expect(validateSchedule('weekly', [block(360, 420, 'mon'), block(360, 420, 'tue')])).toEqual({ + ok: true + }); + }); + + it('rejects overlaps within the same day', () => { + expect(validateSchedule('weekly', [block(360, 480, 'wed'), block(420, 540, 'wed')]).ok).toBe( + false + ); + }); + + it('rejects Sunday midnight-crossing spilling into Monday', () => { + expect(validateSchedule('weekly', [block(1380, 60, 'sun'), block(0, 120, 'mon')]).ok).toBe( + false + ); + }); + + it('accepts a Sunday crossing block adjacent to a Monday block', () => { + expect(validateSchedule('weekly', [block(1380, 60, 'sun'), block(60, 120, 'mon')])).toEqual({ + ok: true + }); + }); + + it('rejects a Saturday crossing spilling into Sunday', () => { + expect(validateSchedule('weekly', [block(1380, 60, 'sat'), block(30, 90, 'sun')]).ok).toBe( + false + ); + }); +}); + +describe('findScheduleOverlap', () => { + it('returns null for gaps', () => { + expect(findScheduleOverlap('daily', [block(360, 420), block(600, 660)])).toBeNull(); + }); +}); diff --git a/src/lib/schedule-utils.ts b/src/lib/schedule-utils.ts index 935f470..8738873 100644 --- a/src/lib/schedule-utils.ts +++ b/src/lib/schedule-utils.ts @@ -23,6 +23,187 @@ export type BlockInput = { export type ValidationResult = { ok: true } | { ok: false; error: string }; +// ─── Schedule-wide validation (daily + weekly, midnight spillover) ───── + +export type ScheduleType = 'daily' | 'weekly'; + +export const WEEK_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const; +export type WeekDay = (typeof WEEK_DAYS)[number]; + +/** A Block as seen by the validator. `day` is null for daily schedules. */ +export type ValidationBlock = { + id?: string; + day?: string | null; + startMinutes: number; + endMinutes: number; + proclamationIds?: string[]; +}; + +/** Block length in minutes, counting a wrap past midnight as the next day. */ +function blockLinearLength(block: { startMinutes: number; endMinutes: number }): number { + return block.endMinutes > block.startMinutes + ? block.endMinutes - block.startMinutes + : block.endMinutes - block.startMinutes + MINUTES_PER_DAY; +} + +/** Linear half-open interval `[start, end)` relative to `dayIndex`. */ +function dayInterval( + block: { startMinutes: number; endMinutes: number }, + dayIndex: number +): [number, number] { + const base = dayIndex * MINUTES_PER_DAY; + const start = base + block.startMinutes; + return [start, start + blockLinearLength(block)]; +} + +function isCrossing(block: { startMinutes: number; endMinutes: number }): boolean { + return block.endMinutes < block.startMinutes; +} + +function blocksForDay(blocks: ValidationBlock[], dayIndex: number): ValidationBlock[] { + const day = WEEK_DAYS[dayIndex]; + return blocks.filter((b) => (b.day ?? null) === day); +} + +/** + * First overlapping pair of Blocks in a schedule, or null. Implements the + * contract's overlap rules: touching boundaries are fine; a midnight-crossing + * Block spills into the next day (weekly Sunday→Monday) and must not overlap the + * next day's early Blocks. + */ +export function findScheduleOverlap( + type: ScheduleType, + blocks: ValidationBlock[] +): { a: ValidationBlock; b: ValidationBlock } | null { + if (type === 'daily') { + // Daily is a single day list that wraps onto itself: a crossing Block + // spills into the next day's copy of the same list. First check the + // same-day intervals, then every crossing Block against the next day. + const intervals = blocks.map((b) => ({ block: b, interval: dayInterval(b, 0) })); + const sameDay = firstPairOverlap(intervals); + if (sameDay) return sameDay; + + for (const crossing of blocks.filter(isCrossing)) { + // The crossing Block's post-midnight tail, expressed in this day's + // 0-based frame (it spans [0, endMinutes)). + const spill: IntervalEntry = { + block: crossing, + interval: [0, crossing.endMinutes] + }; + for (const { block, interval } of intervals) { + if (block === crossing) continue; + if (segmentsIntersect(spill.interval, interval)) { + return { a: crossing, b: block }; + } + } + } + return null; + } + + for (let d = 0; d < WEEK_DAYS.length; d++) { + const dayBlocks = blocksForDay(blocks, d); + const intervals = dayBlocks.map((b) => ({ block: b, interval: dayInterval(b, d) })); + + // Same-day overlaps (a crossing Block's late segment vs the same day). + const pair = firstPairOverlap(intervals); + if (pair) return pair; + + // Spillover from the previous day's crossing Blocks into this day's early + // Blocks. All spills start at midnight, so their union is [0, max(end)). + const prevIndex = (d - 1 + WEEK_DAYS.length) % WEEK_DAYS.length; + const prevCrossing = blocksForDay(blocks, prevIndex).filter(isCrossing); + for (const prev of prevCrossing) { + const spill: [number, number] = [d * MINUTES_PER_DAY, d * MINUTES_PER_DAY + prev.endMinutes]; + for (const { block, interval } of intervals) { + if (segmentsIntersect(spill, interval)) { + return { a: prev, b: block }; + } + } + } + } + return null; +} + +type IntervalEntry = { block: ValidationBlock; interval: [number, number] }; + +function firstPairOverlap( + entries: IntervalEntry[] +): { a: ValidationBlock; b: ValidationBlock } | null { + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + if (segmentsIntersect(entries[i].interval, entries[j].interval)) { + return { a: entries[i].block, b: entries[j].block }; + } + } + } + return null; +} + +/** Validate one Block's shape (time range + at least one Proclamation). */ +export function validateBlockShape(block: ValidationBlock): ValidationResult { + const { startMinutes, endMinutes, proclamationIds } = block; + if ( + !Number.isInteger(startMinutes) || + !Number.isInteger(endMinutes) || + startMinutes < 0 || + startMinutes > MINUTES_PER_DAY || + endMinutes < 0 || + endMinutes > MINUTES_PER_DAY + ) { + return { ok: false, error: 'Enter a valid start and end time.' }; + } + if (startMinutes === endMinutes) { + return { ok: false, error: "Start and end time can't be the same." }; + } + if (block.day != null && !WEEK_DAYS.includes(block.day as WeekDay)) { + return { ok: false, error: 'Pick a valid day of the week.' }; + } + if (proclamationIds !== undefined) { + if (!Array.isArray(proclamationIds) || proclamationIds.length === 0) { + return { ok: false, error: 'Select at least one proclamation.' }; + } + } + return { ok: true }; +} + +/** + * Validate a whole schedule. The server is authoritative; the client calls the + * same function for instant feedback. + */ +export function validateSchedule(type: ScheduleType, blocks: ValidationBlock[]): ValidationResult { + for (const block of blocks) { + const shape = validateBlockShape(block); + if (!shape.ok) return shape; + } + const overlap = findScheduleOverlap(type, blocks); + if (overlap) { + return { ok: false, error: overlapError(overlap.a, overlap.b) }; + } + return { ok: true }; +} + +/** + * Validate a candidate Block against the schedule's other Blocks (which must + * exclude the candidate when editing). + */ +export function validateBlockAgainstSchedule( + type: ScheduleType, + candidate: ValidationBlock, + siblings: ValidationBlock[] +): ValidationResult { + const shape = validateBlockShape(candidate); + if (!shape.ok) return shape; + const overlap = findScheduleOverlap(type, [...siblings, candidate]); + if (overlap) { + return { ok: false, error: overlapError(overlap.a, overlap.b) }; + } + return { ok: true }; +} + +function overlapError(a: ValidationBlock, b: ValidationBlock): string { + return `This overlaps with your ${formatTime(a.startMinutes)}–${formatTime(a.endMinutes)} block.`; +} + function normalizeMinutes(minutes: number): number { return (((Math.trunc(minutes) % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY) | 0; } diff --git a/src/lib/server/schedule.spec.ts b/src/lib/server/schedule.spec.ts index 2006c91..f4d79f8 100644 --- a/src/lib/server/schedule.spec.ts +++ b/src/lib/server/schedule.spec.ts @@ -2,27 +2,40 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('$lib/db', () => ({ prisma: { - schedule: { findUnique: vi.fn(), create: vi.fn() }, + schedule: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() }, + licenseKey: { findUnique: vi.fn() }, block: { findMany: vi.fn(), findFirst: vi.fn(), create: vi.fn(), + createMany: vi.fn(), update: vi.fn(), - delete: vi.fn() - } + delete: vi.fn(), + deleteMany: vi.fn() + }, + blockProclamations: { createMany: vi.fn() } } })); import { prisma } from '$lib/db'; -import { createBlock, deleteBlock, getOrCreateSchedule, listBlocks, updateBlock } from './schedule'; +import { + createBlock, + deleteBlock, + getOrCreateScheduleForLicense, + listBlocksForLicense, + setScheduleType, + updateBlock +} from './schedule'; const scheduleFindUnique = prisma.schedule.findUnique as ReturnType; const scheduleCreate = prisma.schedule.create as ReturnType; +const scheduleUpdate = prisma.schedule.update as ReturnType; const blockFindMany = prisma.block.findMany as ReturnType; const blockFindFirst = prisma.block.findFirst as ReturnType; const blockCreate = prisma.block.create as ReturnType; const blockUpdate = prisma.block.update as ReturnType; const blockDelete = prisma.block.delete as ReturnType; +const blockDeleteMany = prisma.block.deleteMany as ReturnType; const validInput = { startMinutes: 6 * 60, @@ -34,52 +47,95 @@ beforeEach(() => { vi.clearAllMocks(); }); -describe('getOrCreateSchedule', () => { +describe('getOrCreateScheduleForLicense', () => { it('returns the existing schedule', async () => { - scheduleFindUnique.mockResolvedValue({ id: 's1', userId: 'u1' }); + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1' }); - const result = await getOrCreateSchedule('u1'); + const result = await getOrCreateScheduleForLicense('lk1'); - expect(result).toEqual({ id: 's1', userId: 'u1' }); + expect(result).toEqual({ id: 's1', licenseKeyId: 'lk1' }); expect(scheduleCreate).not.toHaveBeenCalled(); }); - it('creates the singleton schedule when missing', async () => { + it('creates a schedule keyed by license when missing', async () => { scheduleFindUnique.mockResolvedValue(null); - scheduleCreate.mockResolvedValue({ id: 's1', userId: 'u1' }); + scheduleCreate.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1' }); - await getOrCreateSchedule('u1'); + await getOrCreateScheduleForLicense('lk1'); - expect(scheduleCreate).toHaveBeenCalledWith({ data: { userId: 'u1' } }); + expect(scheduleCreate).toHaveBeenCalledWith({ data: { licenseKeyId: 'lk1' } }); }); }); -describe('listBlocks', () => { - it('scopes to the user and includes proclamations with frequencies', async () => { +describe('listBlocksForLicense', () => { + it('scopes to the license key', async () => { blockFindMany.mockResolvedValue([]); - await listBlocks('u1'); + await listBlocksForLicense('lk1'); expect(blockFindMany).toHaveBeenCalledWith( expect.objectContaining({ - where: { schedule: { userId: 'u1' } }, - orderBy: { startMinutes: 'asc' }, + where: { schedule: { licenseKeyId: 'lk1' } }, include: expect.objectContaining({ proclamations: expect.anything() }) }) ); }); }); +describe('setScheduleType', () => { + beforeEach(() => { + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: null }); + }); + + it('set type when empty', async () => { + blockFindMany.mockResolvedValue([]); + scheduleUpdate.mockResolvedValue({}); + + const result = await setScheduleType('lk1', 'daily'); + + expect(result).toEqual({ ok: true }); + expect(scheduleUpdate).toHaveBeenCalledWith({ + where: { id: 's1' }, + data: { type: 'daily' } + }); + }); + + it('requires confirmation to switch a populated schedule', async () => { + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); + blockFindMany.mockResolvedValue([ + { id: 'b1', startMinutes: 0, endMinutes: 60, dynamicAudio: true, proclamations: [] } + ]); + + const result = await setScheduleType('lk1', 'weekly'); + + expect(result).toEqual({ ok: false, error: 'confirm' }); + expect(scheduleUpdate).not.toHaveBeenCalled(); + }); + + it('clearing to null deletes the blocks', async () => { + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); + blockFindMany.mockResolvedValue([]); + blockDeleteMany.mockResolvedValue({ count: 0 }); + scheduleUpdate.mockResolvedValue({}); + + const result = await setScheduleType('lk1', null, { confirmed: true }); + + expect(result).toEqual({ ok: true }); + expect(blockDeleteMany).toHaveBeenCalledWith({ where: { scheduleId: 's1' } }); + expect(scheduleUpdate).toHaveBeenCalledWith({ where: { id: 's1' }, data: { type: null } }); + }); +}); + describe('createBlock', () => { beforeEach(() => { - scheduleFindUnique.mockResolvedValue({ id: 's1', userId: 'u1' }); + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); }); it('creates a block with deduped proclamations in order', async () => { blockFindMany.mockResolvedValue([]); blockCreate.mockResolvedValue({ id: 'new-block' }); - const result = await createBlock('u1', { + const result = await createBlock('lk1', { ...validInput, proclamationIds: ['p1', 'p1', 'p2'] }); @@ -88,6 +144,7 @@ describe('createBlock', () => { expect(blockCreate).toHaveBeenCalledWith({ data: { scheduleId: 's1', + day: null, startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, dynamicAudio: true, @@ -101,63 +158,21 @@ describe('createBlock', () => { }); }); - it('defaults dynamic audio on when the field is absent', async () => { - blockFindMany.mockResolvedValue([]); - blockCreate.mockResolvedValue({ id: 'new-block' }); + it('rejects when no type is set', async () => { + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: null }); - await createBlock('u1', validInput); + const result = await createBlock('lk1', validInput); - expect(blockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ dynamicAudio: true }) - }) - ); - }); - - it('persists dynamic audio off when the field is explicitly false', async () => { - blockFindMany.mockResolvedValue([]); - blockCreate.mockResolvedValue({ id: 'new-block' }); - - await createBlock('u1', { ...validInput, dynamicAudio: false }); - - expect(blockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ dynamicAudio: false }) - }) - ); - }); - - it('persists the dynamic audio flag', async () => { - blockFindMany.mockResolvedValue([]); - blockCreate.mockResolvedValue({ id: 'new-block' }); - - await createBlock('u1', { ...validInput, dynamicAudio: true }); - - expect(blockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ dynamicAudio: true }) - }) - ); - }); - - it('rejects a fifth block without creating', async () => { - blockFindMany.mockResolvedValue([ - { id: 'a', startMinutes: 0, endMinutes: 60 }, - { id: 'b', startMinutes: 60, endMinutes: 120 }, - { id: 'c', startMinutes: 120, endMinutes: 180 }, - { id: 'd', startMinutes: 180, endMinutes: 240 } - ]); - - const result = await createBlock('u1', validInput); - - expect(result).toEqual({ ok: false, error: 'You can have at most 4 blocks.' }); + expect(result).toEqual({ ok: false, error: 'Set a schedule type first.' }); expect(blockCreate).not.toHaveBeenCalled(); }); it('rejects an overlapping block with a specific message', async () => { - blockFindMany.mockResolvedValue([{ id: 'a', startMinutes: 7 * 60, endMinutes: 9 * 60 }]); + blockFindMany.mockResolvedValue([ + { id: 'a', day: null, startMinutes: 7 * 60, endMinutes: 9 * 60, proclamations: [] } + ]); - const result = await createBlock('u1', validInput); + const result = await createBlock('lk1', validInput); expect(result).toEqual({ ok: false, @@ -165,13 +180,22 @@ describe('createBlock', () => { }); expect(blockCreate).not.toHaveBeenCalled(); }); + + it('rejects an empty block before creating', async () => { + blockFindMany.mockResolvedValue([]); + + const result = await createBlock('lk1', { ...validInput, proclamationIds: [] }); + + expect(result).toEqual({ ok: false, error: 'Select at least one proclamation.' }); + expect(blockCreate).not.toHaveBeenCalled(); + }); }); describe('updateBlock', () => { - it('returns not found when the block does not belong to the user', async () => { + it('returns not found when the block does not belong to the license', async () => { blockFindFirst.mockResolvedValue(null); - const result = await updateBlock('u1', 'missing', validInput); + const result = await updateBlock('lk1', 'missing', validInput); expect(result).toEqual({ ok: false, error: 'Block not found.' }); expect(blockUpdate).not.toHaveBeenCalled(); @@ -179,19 +203,22 @@ describe('updateBlock', () => { it('replaces proclamations and excludes itself from overlap checks', async () => { blockFindFirst.mockResolvedValue({ id: 'b1', scheduleId: 's1' }); + scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); blockFindMany.mockResolvedValue([]); blockUpdate.mockResolvedValue({ id: 'b1' }); - const result = await updateBlock('u1', 'b1', validInput); + const result = await updateBlock('lk1', 'b1', validInput); expect(result).toEqual({ ok: true, id: 'b1' }); - expect(blockFindMany).toHaveBeenCalledWith({ - where: { scheduleId: 's1', id: { not: 'b1' } }, - select: { id: true, startMinutes: true, endMinutes: true } - }); + expect(blockFindMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { scheduleId: 's1', id: { not: 'b1' } } + }) + ); expect(blockUpdate).toHaveBeenCalledWith({ where: { id: 'b1' }, data: { + day: null, startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, dynamicAudio: true, @@ -205,10 +232,10 @@ describe('updateBlock', () => { }); describe('deleteBlock', () => { - it('returns not found when the block does not belong to the user', async () => { + it('returns not found when the block does not belong to the license', async () => { blockFindFirst.mockResolvedValue(null); - const result = await deleteBlock('u1', 'missing'); + const result = await deleteBlock('lk1', 'missing'); expect(result).toEqual({ ok: false, error: 'Block not found.' }); expect(blockDelete).not.toHaveBeenCalled(); @@ -218,7 +245,7 @@ describe('deleteBlock', () => { blockFindFirst.mockResolvedValue({ id: 'b1' }); blockDelete.mockResolvedValue({ id: 'b1' }); - const result = await deleteBlock('u1', 'b1'); + const result = await deleteBlock('lk1', 'b1'); expect(result).toEqual({ ok: true, id: 'b1' }); expect(blockDelete).toHaveBeenCalledWith({ where: { id: 'b1' } }); diff --git a/src/lib/server/schedule.ts b/src/lib/server/schedule.ts index 4023d94..8ee5eb8 100644 --- a/src/lib/server/schedule.ts +++ b/src/lib/server/schedule.ts @@ -1,78 +1,281 @@ // Schedule service layer. Owns the database orchestration and enforcement of -// the Block business rules (at most 4 per user, at least one Proclamation, -// no overnight ranges, no overlaps). Pure validation lives in -// `$lib/schedule-utils` so the client modal can reuse it. +// the Block business rules. Pure validation lives in `$lib/schedule-utils` so +// the client can reuse the exact same rules. +// +// A Schedule belongs to exactly one License Key. `type` is "daily" | "weekly" | +// null (null = no schedule). Daily Blocks have day=null; weekly Blocks carry a +// week day. Times are wall-clock minutes from midnight. import { prisma } from '$lib/db'; -import { validateBlock, type BlockInput } from '$lib/schedule-utils'; +import { + MAX_BLOCKS, + WEEK_DAYS, + validateBlockAgainstSchedule, + validateSchedule, + type ScheduleType, + type ValidationBlock +} from '$lib/schedule-utils'; export { MAX_BLOCKS, MINUTES_PER_DAY, SNAP_MINUTES, MINUTE_STEP, + WEEK_DAYS, formatTime, toHm, snapMinutes, findOverlap, - validateBlock + findScheduleOverlap, + validateBlock, + validateBlockAgainstSchedule, + validateBlockShape, + validateSchedule } from '$lib/schedule-utils'; -export type { BlockInput, BlockSibling, ValidationResult } from '$lib/schedule-utils'; +export type { + BlockInput as LegacyBlockInput, + BlockSibling, + ValidationResult, + ValidationBlock, + ScheduleType +} from '$lib/schedule-utils'; + +export type BlockInput = { + day?: string | null; + startMinutes: number; + endMinutes: number; + proclamationIds: string[]; + dynamicAudio?: boolean; +}; export type BlockMutationResult = { ok: true; id?: string } | { ok: false; error: string }; +export type TypeMutationResult = { ok: true } | { ok: false; error: string }; function uniqueIds(ids: string[]): string[] { return [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))]; } -/** - * New Blocks default `dynamicAudio` to true when the field is absent. Existing - * stored Blocks are never rewritten by this default. - */ function dynamicAudioOrDefault(input: BlockInput): boolean { return input.dynamicAudio !== false; } -/** Ensure the user has a Schedule row, creating the singleton on first use. */ -export async function getOrCreateSchedule(userId: string) { - const existing = await prisma.schedule.findUnique({ where: { userId } }); +function isScheduleType(value: unknown): value is ScheduleType { + return value === 'daily' || value === 'weekly'; +} + +/** Normalise the day for a schedule type: daily always uses null. */ +function normaliseDay(type: ScheduleType | null, day: string | null | undefined): string | null { + if (type !== 'weekly') return null; + return day ?? null; +} + +const blockInclude = { + proclamations: { + orderBy: { sortOrder: 'asc' as const }, + include: { + proclamation: { include: { solfeggioFrequency: true } } + } + } +}; + +/** Ensure the License Key has a Schedule row. New rows start with type null. */ +export async function getOrCreateScheduleForLicense(licenseKeyId: string) { + const existing = await prisma.schedule.findUnique({ where: { licenseKeyId } }); if (existing) return existing; - return prisma.schedule.create({ data: { userId } }); + return prisma.schedule.create({ data: { licenseKeyId } }); +} + +/** The Schedule (with Blocks and Proclamations) for a License Key, or null. */ +export async function getScheduleForLicense(licenseKeyId: string) { + return prisma.schedule.findUnique({ + where: { licenseKeyId }, + include: { blocks: { include: blockInclude } } + }); +} + +/** The Schedule for a License Key string, or null. */ +export async function getScheduleForLicenseKey(licenseKey: string) { + const license = await prisma.licenseKey.findUnique({ + where: { key: licenseKey }, + select: { id: true } + }); + if (!license) return { license: null, schedule: null }; + const schedule = await getScheduleForLicense(license.id); + return { license, schedule }; +} + +/** Blocks for a License Key's schedule, ordered by day then start. */ +export async function listBlocksForLicense(licenseKeyId: string) { + return prisma.block.findMany({ + where: { schedule: { licenseKeyId } }, + orderBy: [{ day: 'asc' }, { startMinutes: 'asc' }], + include: blockInclude + }); +} + +function toValidationBlock(block: { + id?: string; + day: string | null; + startMinutes: number; + endMinutes: number; + proclamationIds?: string[]; +}): ValidationBlock { + return { + id: block.id, + day: block.day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + proclamationIds: block.proclamationIds + }; } /** - * The user's Blocks with their selected Proclamations (and frequencies for - * canonical-id mapping), ordered by start time. + * Change (or clear) a Schedule's type. daily→weekly prefills all seven days + * with the daily Blocks; weekly→daily and clearing start empty. Returns an + * error result when confirmation is implicitly required but not given. */ -export async function listBlocks(userId: string) { - return prisma.block.findMany({ - where: { schedule: { userId } }, - orderBy: { startMinutes: 'asc' }, - include: { - proclamations: { - orderBy: { sortOrder: 'asc' }, - include: { - proclamation: { include: { solfeggioFrequency: true } } - } +export async function setScheduleType( + licenseKeyId: string, + type: ScheduleType | null, + opts: { confirmed?: boolean } = {} +): Promise { + if (type !== null && !isScheduleType(type)) { + return { ok: false, error: 'Pick a schedule type.' }; + } + + const schedule = await getOrCreateScheduleForLicense(licenseKeyId); + if (schedule.type === type) return { ok: true }; + + const existing = await prisma.block.findMany({ + where: { scheduleId: schedule.id }, + include: { proclamations: { orderBy: { sortOrder: 'asc' } } } + }); + + // Changing an existing, populated schedule requires confirmation. + if ( + (schedule.type === 'daily' || schedule.type === 'weekly') && + type !== null && + !opts.confirmed + ) { + return { ok: false, error: 'confirm' }; + } + + if (type === 'weekly' && schedule.type === 'daily' && existing.length > 0) { + // Prefill all seven days with a copy of the daily Blocks. + await prisma.block.createMany({ + data: existing.flatMap((block) => + WEEK_DAYS.map((day) => ({ + scheduleId: schedule.id, + day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + dynamicAudio: block.dynamicAudio + })) + ) + }); + const clones = await prisma.block.findMany({ + where: { scheduleId: schedule.id, day: { not: null } } + }); + for (const clone of clones) { + const source = existing.find( + (b) => + b.startMinutes === clone.startMinutes && + b.endMinutes === clone.endMinutes && + b.dynamicAudio === clone.dynamicAudio + ); + if (source) { + await prisma.blockProclamations.createMany({ + data: source.proclamations.map((p, index) => ({ + blockId: clone.id, + proclamationId: p.proclamationId, + sortOrder: index + })), + skipDuplicates: true + }); } } - }); + } else if (type !== 'weekly') { + // weekly→daily, daily→null, weekly→null: clear the Blocks. + await prisma.block.deleteMany({ where: { scheduleId: schedule.id } }); + } + + await prisma.schedule.update({ where: { id: schedule.id }, data: { type } }); + return { ok: true }; } -export async function createBlock(userId: string, input: BlockInput): Promise { - const schedule = await getOrCreateSchedule(userId); - const siblings = await prisma.block.findMany({ - where: { scheduleId: schedule.id }, - select: { id: true, startMinutes: true, endMinutes: true } +async function ensureValid( + licenseKeyId: string, + day: string | null, + candidate: BlockInput, + excludeBlockId?: string +): Promise { + const schedule = await getOrCreateScheduleForLicense(licenseKeyId); + const type: ScheduleType | null = isScheduleType(schedule.type) ? schedule.type : null; + if (type === null) { + return { ok: false, error: 'Set a schedule type first.' }; + } + if (type === 'weekly' && (day === null || !(WEEK_DAYS as readonly string[]).includes(day))) { + return { ok: false, error: 'Pick a valid day of the week.' }; + } + + const existing = await prisma.block.findMany({ + where: { + scheduleId: schedule.id, + ...(excludeBlockId ? { id: { not: excludeBlockId } } : {}) + }, + select: { + id: true, + day: true, + startMinutes: true, + endMinutes: true, + proclamations: { select: { proclamationId: true } } + } }); - const validation = validateBlock(input, siblings, { isCreate: true }); + const siblings: ValidationBlock[] = existing.map((b) => + toValidationBlock({ + id: b.id, + day: b.day, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + proclamationIds: b.proclamations.map((p) => p.proclamationId) + }) + ); + + const candidateBlock = toValidationBlock({ + day, + startMinutes: candidate.startMinutes, + endMinutes: candidate.endMinutes, + proclamationIds: uniqueIds(candidate.proclamationIds) + }); + + const validation = validateBlockAgainstSchedule(type, candidateBlock, siblings); if (!validation.ok) return validation; + const sameDayCount = siblings.filter((b) => normaliseDay(type, b.day) === day).length; + if (!excludeBlockId && sameDayCount >= MAX_BLOCKS) { + return { ok: false, error: `You can have at most ${MAX_BLOCKS} blocks per day.` }; + } + + return { ok: true, scheduleId: schedule.id, type }; +} + +export async function createBlock( + licenseKeyId: string, + input: BlockInput +): Promise { + const schedule = await getOrCreateScheduleForLicense(licenseKeyId); + const type: ScheduleType | null = isScheduleType(schedule.type) ? schedule.type : null; + const day = normaliseDay(type, input.day); + + const checked = await ensureValid(licenseKeyId, day, input); + if (!checked.ok) return { ok: false, error: checked.error }; + const proclamationIds = uniqueIds(input.proclamationIds); const block = await prisma.block.create({ data: { scheduleId: schedule.id, + day, startMinutes: input.startMinutes, endMinutes: input.endMinutes, dynamicAudio: dynamicAudioOrDefault(input), @@ -84,33 +287,33 @@ export async function createBlock(userId: string, input: BlockInput): Promise { const block = await prisma.block.findFirst({ - where: { id: blockId, schedule: { userId } }, + where: { id: blockId, schedule: { licenseKeyId } }, select: { id: true, scheduleId: true } }); if (!block) return { ok: false, error: 'Block not found.' }; - const siblings = await prisma.block.findMany({ - where: { scheduleId: block.scheduleId, id: { not: blockId } }, - select: { id: true, startMinutes: true, endMinutes: true } - }); + const schedule = await prisma.schedule.findUnique({ where: { id: block.scheduleId } }); + const type: ScheduleType | null = + schedule && isScheduleType(schedule.type) ? schedule.type : null; + const day = normaliseDay(type, input.day); - const validation = validateBlock(input, siblings); - if (!validation.ok) return validation; + const checked = await ensureValid(licenseKeyId, day, input, blockId); + if (!checked.ok) return { ok: false, error: checked.error }; const proclamationIds = uniqueIds(input.proclamationIds); await prisma.block.update({ where: { id: blockId }, data: { + day, startMinutes: input.startMinutes, endMinutes: input.endMinutes, dynamicAudio: dynamicAudioOrDefault(input), @@ -123,13 +326,15 @@ export async function updateBlock( } } }); - return { ok: true, id: blockId }; } -export async function deleteBlock(userId: string, blockId: string): Promise { +export async function deleteBlock( + licenseKeyId: string, + blockId: string +): Promise { const block = await prisma.block.findFirst({ - where: { id: blockId, schedule: { userId } }, + where: { id: blockId, schedule: { licenseKeyId } }, select: { id: true } }); if (!block) return { ok: false, error: 'Block not found.' }; @@ -137,3 +342,32 @@ export async function deleteBlock(userId: string, blockId: string): Promise { + return validateSchedule(type, blocks); +} + +/** Flatten a loaded Schedule's Blocks (with Proclamations) for serialization. */ +export function scheduleBlocksToInput( + blocks: { + id: string; + day: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamations: { proclamation: { id: string } }[]; + }[] +) { + return blocks.map((block) => ({ + id: block.id, + day: block.day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + dynamicAudio: block.dynamicAudio, + proclamationIds: block.proclamations.map((entry) => entry.proclamation.id) + })); +} diff --git a/src/lib/server/v2-schedule.spec.ts b/src/lib/server/v2-schedule.spec.ts new file mode 100644 index 0000000..28d2d08 --- /dev/null +++ b/src/lib/server/v2-schedule.spec.ts @@ -0,0 +1,169 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { buildSchedulePayload, type ScheduleBlockInput } from './v2-serializers'; + +const FIXTURES = resolve(process.cwd(), 'docs/fixtures'); + +function load(name: string): { + type: 'daily' | 'weekly' | null; + daily: { + id: string; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamationIds: string[]; + }[]; + blocks: { + id: string; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamationIds: string[]; + }[]; + weekly: Record< + string, + { + id: string; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamationIds: string[]; + }[] + >; +} { + return JSON.parse(readFileSync(resolve(FIXTURES, name), 'utf8')); +} + +/** Convert a fixture payload back into DB-shaped blocks. */ +function fixtureBlocks(name: string): { + type: 'daily' | 'weekly' | null; + blocks: ScheduleBlockInput[]; +} { + const fixture = load(name); + const blocks: ScheduleBlockInput[] = []; + if (fixture.type === 'daily') { + for (const b of fixture.daily) { + blocks.push({ + id: b.id, + day: null, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamationIds: b.proclamationIds + }); + } + } else if (fixture.type === 'weekly') { + for (const [day, list] of Object.entries(fixture.weekly)) { + for (const b of list) { + blocks.push({ + id: b.id, + day, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamationIds: b.proclamationIds + }); + } + } + } + return { type: fixture.type, blocks }; +} + +describe('buildSchedulePayload against fixtures', () => { + it('round-trips the daily fixture content field-for-field', () => { + const { type, blocks } = fixtureBlocks('schedule-daily.json'); + const payload = buildSchedulePayload(type, blocks); + const fixture = load('schedule-daily.json'); + + expect(payload.type).toBe('daily'); + expect(payload.daily).toEqual(fixture.daily); + expect(payload.blocks).toEqual(fixture.blocks); + expect(payload.weekly).toEqual({}); + }); + + it('populates all seven weekly keys and leaves blocks empty for weekly', () => { + const { type, blocks } = fixtureBlocks('schedule-weekly.json'); + const payload = buildSchedulePayload(type, blocks); + const fixture = load('schedule-weekly.json'); + + expect(payload.type).toBe('weekly'); + expect(Object.keys(payload.weekly).sort()).toEqual([ + 'fri', + 'mon', + 'sat', + 'sun', + 'thu', + 'tue', + 'wed' + ]); + expect(payload.weekly.sun).toEqual(fixture.weekly.sun); + expect(payload.weekly.tue).toEqual([]); + expect(payload.blocks).toEqual([]); + expect(payload.daily).toEqual([]); + }); + + it('produces type null with empty lists for the none fixture', () => { + const payload = buildSchedulePayload(null, []); + + expect(payload.type).toBeNull(); + expect(payload.daily).toEqual([]); + expect(payload.weekly).toEqual({}); + expect(payload.blocks).toEqual([]); + }); + + it('preserves midnight-crossing and adjacency in the daily flattening', () => { + const pick = (b: { + id: string; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamationIds: string[]; + }) => ({ + id: b.id, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamationIds: b.proclamationIds + }); + for (const name of ['schedule-midnight-crossing.json', 'schedule-adjacent.json']) { + const { type, blocks } = fixtureBlocks(name); + const payload = buildSchedulePayload(type, blocks); + const fixture = load(name); + // The fixture lists blocks in file order; the serializer sorts by start + // time. Compare as an order-independent set of the shared fields. + const byId = (list: ReturnType[]) => + [...list].sort((a, b) => a.id.localeCompare(b.id)).map(pick); + expect(byId(payload.blocks)).toEqual(byId(fixture.blocks)); + } + }); + + it('preserves the dynamicAudio flag per block', () => { + const { type, blocks } = fixtureBlocks('schedule-dynamic-audio.json'); + const payload = buildSchedulePayload(type, blocks); + expect(payload.blocks.map((b) => b.dynamicAudio)).toEqual([true, false]); + }); +}); + +describe('version (ETag)', () => { + it('is stable for identical content', () => { + const { type, blocks } = fixtureBlocks('schedule-daily.json'); + expect(buildSchedulePayload(type, blocks).version).toBe( + buildSchedulePayload(type, blocks).version + ); + }); + + it('changes when the schedule changes', () => { + const { type, blocks } = fixtureBlocks('schedule-daily.json'); + const a = buildSchedulePayload(type, blocks).version; + const changed = blocks.map((b, i) => (i === 0 ? { ...b, endMinutes: b.endMinutes + 5 } : b)); + const b = buildSchedulePayload(type, changed).version; + expect(a).not.toBe(b); + }); + + it('changes with the type', () => { + const daily = buildSchedulePayload('daily', []).version; + const weekly = buildSchedulePayload('weekly', []).version; + expect(daily).not.toBe(weekly); + }); +}); diff --git a/src/lib/server/v2-serializers.ts b/src/lib/server/v2-serializers.ts index 28276f3..d58d2b6 100644 --- a/src/lib/server/v2-serializers.ts +++ b/src/lib/server/v2-serializers.ts @@ -1,4 +1,5 @@ import { blockDuration, isOvernight, toHm } from '$lib/schedule-utils'; +import { createHash } from 'node:crypto'; // Canonical solfeggio id <- Hz, mirroring the kiosk's `frequencyIdForHz()` // map (radiantwave-rebuild src/audio/audio_downloader.cpp). v2 must emit these @@ -190,3 +191,67 @@ export function serializeScheduleBlock( proclamationIds }; } + +/** A Block as read from the database, flattened for serialization. */ +export type ScheduleBlockInput = { + id: string; + day: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio?: boolean; + proclamationIds: string[]; +}; + +export type V2ScheduleType = 'daily' | 'weekly' | null; + +export type V2SchedulePayload = { + version: string; + type: V2ScheduleType; + daily: V2ScheduleBlock[]; + weekly: Record; + blocks: V2ScheduleBlock[]; +}; + +const WEEK_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; + +function serialise(block: ScheduleBlockInput): V2ScheduleBlock { + return serializeScheduleBlock(block, block.proclamationIds); +} + +/** + * Build the target schedule payload. `version` is an opaque content hash that + * doubles as the ETag; it is stable for equal content and changes on any + * mutation. Only the active type's lists are populated. `blocks` is the legacy + * daily flattening (empty for weekly) kept for older Kiosks. + */ +export function buildSchedulePayload( + type: V2ScheduleType, + blocks: ScheduleBlockInput[] +): V2SchedulePayload { + const dailyBlocks: V2ScheduleBlock[] = []; + const weekly: Record = {}; + + if (type === 'daily') { + const ordered = [...blocks].sort((a, b) => a.startMinutes - b.startMinutes); + for (const block of ordered) dailyBlocks.push(serialise(block)); + } else if (type === 'weekly') { + for (const key of WEEK_KEYS) weekly[key] = []; + const ordered = [...blocks].sort((a, b) => a.startMinutes - b.startMinutes); + for (const block of ordered) { + const key = block.day ?? ''; + if (key in weekly) weekly[key].push(serialise(block)); + } + } + + const version = createHash('sha1') + .update(JSON.stringify({ type, daily: dailyBlocks, weekly })) + .digest('hex'); + + return { + version, + type, + daily: dailyBlocks, + weekly, + blocks: type === 'daily' ? dailyBlocks : [] + }; +} diff --git a/src/routes/(protected)/(user)/schedule/+page.server.ts b/src/routes/(protected)/(user)/schedule/+page.server.ts index 10359d1..6b07395 100644 --- a/src/routes/(protected)/(user)/schedule/+page.server.ts +++ b/src/routes/(protected)/(user)/schedule/+page.server.ts @@ -1,11 +1,21 @@ import type { Actions, PageServerLoad } from './$types'; import { error, fail } from '@sveltejs/kit'; import { prisma } from '$lib/db'; -import { createBlock, deleteBlock, listBlocks, updateBlock } from '$lib/server/schedule'; +import { + createBlock, + deleteBlock, + getScheduleForLicense, + setScheduleType, + updateBlock +} from '$lib/server/schedule'; + +const WEEK_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; function parseInput(formData: FormData) { const startMinutes = Number(formData.get('startMinutes')); const endMinutes = Number(formData.get('endMinutes')); + const dayRaw = formData.get('day')?.toString() ?? ''; + const day = WEEK_DAYS.includes(dayRaw) ? dayRaw : null; let proclamationIds: string[] = []; const raw = formData.get('proclamationIds'); if (typeof raw === 'string' && raw.length > 0) { @@ -19,16 +29,29 @@ function parseInput(formData: FormData) { } } const dynamicAudio = formData.get('dynamicAudio') === 'true'; - return { startMinutes, endMinutes, proclamationIds, dynamicAudio }; + return { day, startMinutes, endMinutes, proclamationIds, dynamicAudio }; } -export const load: PageServerLoad = async ({ locals }) => { +/** Ensure the given License Key belongs to the signed-in account. */ +async function ownedLicense(userId: string, licenseKeyId: string) { + const license = await prisma.licenseKey.findFirst({ + where: { id: licenseKeyId, userId }, + select: { id: true, key: true, systemName: true } + }); + return license; +} + +export const load: PageServerLoad = async ({ locals, url }) => { if (!locals.user?.id) { throw error(403, 'not authorized'); } - const [blocks, categories, groups, proclamations] = await Promise.all([ - listBlocks(locals.user.id), + const [licenses, categories, groups, proclamations] = await Promise.all([ + prisma.licenseKey.findMany({ + where: { userId: locals.user.id }, + orderBy: { createdAt: 'asc' }, + select: { id: true, key: true, systemName: true, active: true } + }), prisma.proclamationCategory.findMany({ select: { id: true, name: true }, orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }] @@ -45,27 +68,75 @@ export const load: PageServerLoad = async ({ locals }) => { }) ]); - const blockViews = blocks.map((block) => ({ - id: block.id, - startMinutes: block.startMinutes, - endMinutes: block.endMinutes, - dynamicAudio: block.dynamicAudio, - proclamations: block.proclamations.map((entry) => ({ - id: entry.proclamation.id, - name: entry.proclamation.name, - categoryId: entry.proclamation.categoryId, - groupId: entry.proclamation.groupId - })) - })); + const requested = url.searchParams.get('license'); + const selected = licenses.find((l) => l.id === requested) ?? licenses[0] ?? null; - return { blocks: blockViews, categories, groups, proclamations }; + let type: 'daily' | 'weekly' | null = null; + let blocks: { + id: string; + day: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamations: { id: string; name: string; categoryId: number; groupId: number | null }[]; + }[] = []; + + if (selected) { + const schedule = await getScheduleForLicense(selected.id); + if (schedule) { + type = schedule.type === 'daily' || schedule.type === 'weekly' ? schedule.type : null; + blocks = schedule.blocks.map((block) => ({ + id: block.id, + day: block.day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + dynamicAudio: block.dynamicAudio, + proclamations: block.proclamations.map((entry) => ({ + id: entry.proclamation.id, + name: entry.proclamation.name, + categoryId: entry.proclamation.categoryId, + groupId: entry.proclamation.groupId + })) + })); + } + } + + return { + licenses, + selectedLicenseId: selected?.id ?? null, + type, + blocks, + categories, + groups, + proclamations + }; }; export const actions: Actions = { + setType: async ({ request, locals }) => { + if (!locals.user?.id) throw error(403, 'not authorized'); + const formData = await request.formData(); + const licenseKeyId = formData.get('licenseKeyId')?.toString() ?? ''; + const rawType = formData.get('type')?.toString() ?? ''; + const type = rawType === 'daily' || rawType === 'weekly' ? rawType : null; + const confirm = formData.get('confirm') === 'true'; + + const license = await ownedLicense(locals.user.id, licenseKeyId); + if (!license) return fail(404, { error: 'System not found.' }); + + const result = await setScheduleType(licenseKeyId, type, { confirmed: confirm }); + if (!result.ok) return fail(400, { error: result.error }); + return { success: true }; + }, + createBlock: async ({ request, locals }) => { if (!locals.user?.id) throw error(403, 'not authorized'); - const input = parseInput(await request.formData()); - const result = await createBlock(locals.user.id, input); + const formData = await request.formData(); + const licenseKeyId = formData.get('licenseKeyId')?.toString() ?? ''; + const license = await ownedLicense(locals.user.id, licenseKeyId); + if (!license) return fail(404, { error: 'System not found.' }); + + const result = await createBlock(licenseKeyId, parseInput(formData)); if (!result.ok) return fail(400, { error: result.error }); return { success: true }; }, @@ -73,18 +144,27 @@ export const actions: Actions = { updateBlock: async ({ request, locals }) => { if (!locals.user?.id) throw error(403, 'not authorized'); const formData = await request.formData(); + const licenseKeyId = formData.get('licenseKeyId')?.toString() ?? ''; const id = formData.get('id')?.toString(); + const license = await ownedLicense(locals.user.id, licenseKeyId); + if (!license) return fail(404, { error: 'System not found.' }); if (!id) return fail(400, { error: 'Block not found.' }); - const result = await updateBlock(locals.user.id, id, parseInput(formData)); + + const result = await updateBlock(licenseKeyId, id, parseInput(formData)); if (!result.ok) return fail(400, { error: result.error }); return { success: true }; }, deleteBlock: async ({ request, locals }) => { if (!locals.user?.id) throw error(403, 'not authorized'); - const id = (await request.formData()).get('id')?.toString(); + const formData = await request.formData(); + const licenseKeyId = formData.get('licenseKeyId')?.toString() ?? ''; + const id = formData.get('id')?.toString(); + const license = await ownedLicense(locals.user.id, licenseKeyId); + if (!license) return fail(404, { error: 'System not found.' }); if (!id) return fail(400, { error: 'Block not found.' }); - const result = await deleteBlock(locals.user.id, id); + + const result = await deleteBlock(licenseKeyId, id); if (!result.ok) return fail(400, { error: result.error }); return { success: true }; } diff --git a/src/routes/(protected)/(user)/schedule/page.server.spec.ts b/src/routes/(protected)/(user)/schedule/page.server.spec.ts index 4d6eab0..e289202 100644 --- a/src/routes/(protected)/(user)/schedule/page.server.spec.ts +++ b/src/routes/(protected)/(user)/schedule/page.server.spec.ts @@ -6,23 +6,33 @@ vi.mock('$lib/db', () => ({ prisma: { proclamationCategory: { findMany: vi.fn() }, proclamationGroup: { findMany: vi.fn() }, - proclamation: { findMany: vi.fn() } + proclamation: { findMany: vi.fn() }, + licenseKey: { findMany: vi.fn(), findFirst: vi.fn() } } })); vi.mock('$lib/server/schedule', () => ({ - listBlocks: vi.fn(), + getScheduleForLicense: vi.fn(), + setScheduleType: vi.fn(), createBlock: vi.fn(), updateBlock: vi.fn(), deleteBlock: vi.fn() })); import { prisma } from '$lib/db'; -import { createBlock, deleteBlock, listBlocks } from '$lib/server/schedule'; +import { + createBlock, + deleteBlock, + getScheduleForLicense, + setScheduleType +} from '$lib/server/schedule'; import { actions, load } from './+page.server'; const procFindMany = prisma.proclamation.findMany as ReturnType; -const listBlocksMock = listBlocks as ReturnType; +const licenseFindMany = prisma.licenseKey.findMany as ReturnType; +const licenseFindFirst = prisma.licenseKey.findFirst as ReturnType; +const getScheduleMock = getScheduleForLicense as ReturnType; +const setTypeMock = setScheduleType as ReturnType; const createBlockMock = createBlock as ReturnType; const deleteBlockMock = deleteBlock as ReturnType; @@ -37,18 +47,24 @@ beforeEach(() => { { id: 1, name: 'General' } ]); procFindMany.mockResolvedValue([{ id: 'p1', name: 'Peace', categoryId: 2, groupId: 1 }]); - listBlocksMock.mockResolvedValue([]); + licenseFindMany.mockResolvedValue([ + { id: 'lk1', key: 'AAAA-BBBB-CCCC-DDDD', systemName: 'Living Room', active: true } + ]); + licenseFindFirst.mockResolvedValue({ id: 'lk1' }); + getScheduleMock.mockResolvedValue(null); }); describe('schedule load', () => { it('rejects unauthenticated requests', async () => { - await expect(load({ locals: createLocals() } as never)).rejects.toMatchObject({ + await expect( + load({ locals: createLocals(), url: new URL('http://x/') } as never) + ).rejects.toMatchObject({ status: 403 }); }); it('never selects proclamation content', async () => { - await load({ locals } as never); + await load({ locals, url: new URL('http://x/') } as never); expect(procFindMany).toHaveBeenCalledWith( expect.objectContaining({ @@ -58,34 +74,37 @@ describe('schedule load', () => { }); it('maps blocks to title/category/group only', async () => { - listBlocksMock.mockResolvedValue([ - { - id: 'b1', - startMinutes: 360, - endMinutes: 480, - dynamicAudio: true, - proclamations: [ - { - proclamation: { - id: 'p1', - name: 'Peace', - categoryId: 2, - groupId: 1, - content: '

secret

' + getScheduleMock.mockResolvedValue({ + id: 's1', + type: 'daily', + blocks: [ + { + id: 'b1', + day: null, + startMinutes: 360, + endMinutes: 480, + dynamicAudio: true, + proclamations: [ + { + proclamation: { + id: 'p1', + name: 'Peace', + categoryId: 2, + groupId: 1, + content: '

secret

' + } } - } - ] - } - ]); + ] + } + ] + }); - const result = (await load({ locals } as never)) as { - blocks: { - id: string; - dynamicAudio: boolean; - proclamations: Record[]; - }[]; + const result = (await load({ locals, url: new URL('http://x/') } as never)) as { + type: string | null; + blocks: { id: string; dynamicAudio: boolean; proclamations: Record[] }[]; }; + expect(result.type).toBe('daily'); expect(result.blocks[0].dynamicAudio).toBe(true); expect(result.blocks[0].proclamations[0]).toEqual({ id: 'p1', @@ -112,6 +131,7 @@ describe('schedule actions', () => { const result = await actions.createBlock!({ request: createFormRequest({ + licenseKeyId: 'lk1', startMinutes: '360', endMinutes: '480', proclamationIds: '[]' @@ -131,6 +151,8 @@ describe('schedule actions', () => { const result = await actions.createBlock!({ request: createFormRequest({ + licenseKeyId: 'lk1', + day: 'mon', startMinutes: '360', endMinutes: '480', proclamationIds: JSON.stringify(['p1', 'p2']), @@ -140,7 +162,8 @@ describe('schedule actions', () => { } as never); expect(result).toEqual({ success: true }); - expect(createBlockMock).toHaveBeenCalledWith('user-1', { + expect(createBlockMock).toHaveBeenCalledWith('lk1', { + day: 'mon', startMinutes: 360, endMinutes: 480, proclamationIds: ['p1', 'p2'], @@ -148,27 +171,23 @@ describe('schedule actions', () => { }); }); - it('createBlock defaults dynamic audio to false when the field is absent', async () => { - createBlockMock.mockResolvedValue({ ok: true, id: 'b1' }); + it('setType requiring confirmation returns the confirm error', async () => { + setTypeMock.mockResolvedValue({ ok: false, error: 'confirm' }); - await actions.createBlock!({ - request: createFormRequest({ - startMinutes: '360', - endMinutes: '480', - proclamationIds: JSON.stringify(['p1']) - }), + const result = await actions.setType!({ + request: createFormRequest({ licenseKeyId: 'lk1', type: 'weekly' }), locals } as never); - expect(createBlockMock).toHaveBeenCalledWith( - 'user-1', - expect.objectContaining({ dynamicAudio: false }) - ); + expect(isActionFailure(result)).toBe(true); + if (isActionFailure(result)) { + expect(result.data).toEqual({ error: 'confirm' }); + } }); it('deleteBlock returns not found without an id', async () => { const result = await actions.deleteBlock!({ - request: createFormRequest({}), + request: createFormRequest({ licenseKeyId: 'lk1' }), locals } as never); diff --git a/src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts b/src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts new file mode 100644 index 0000000..736903f --- /dev/null +++ b/src/routes/api/v2/licenses/[licenseKey]/schedule/+server.ts @@ -0,0 +1,48 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { prisma } from '$lib/db'; +import { clientIp, rateLimited } from '$lib/server/v2-helpers'; +import { rateLimit } from '$lib/server/v2-rate-limit'; +import { getScheduleForLicense, scheduleBlocksToInput } from '$lib/server/schedule'; +import { buildSchedulePayload, type V2ScheduleType } from '$lib/server/v2-serializers'; + +function scheduleType(value: string | null | undefined): V2ScheduleType { + return value === 'daily' || value === 'weekly' ? value : null; +} + +/** + * Kiosk-facing, key-only schedule read. Returns the target payload with an ETag + * (the content-derived `version`); a matching `If-None-Match` yields 304. An + * unknown License Key is 404; a key with no Schedule returns `type: null`. + */ +export const GET: RequestHandler = async ({ request, params }) => { + const ip = clientIp(request); + const rl = rateLimit({ key: `license-schedule:${ip}`, limit: 10, windowMs: 60_000 }); + if (!rl.ok) { + return rateLimited(rl.retryAfterMs); + } + + const licenseKey = (params.licenseKey ?? '').trim(); + const license = await prisma.licenseKey.findUnique({ + where: { key: licenseKey }, + select: { id: true } + }); + if (!license) { + throw error(404, 'unknown_license'); + } + + const schedule = await getScheduleForLicense(license.id); + const type = scheduleType(schedule?.type); + const blocks = schedule ? scheduleBlocksToInput(schedule.blocks) : []; + const payload = buildSchedulePayload(type, blocks); + const etag = `"${payload.version}"`; + + if (request.headers.get('if-none-match') === etag) { + return new Response(null, { status: 304, headers: { etag } }); + } + + return new Response(JSON.stringify(payload), { + status: 200, + headers: { 'content-type': 'application/json', etag } + }); +}; diff --git a/src/routes/api/v2/licenses/[licenseKey]/schedule/server.spec.ts b/src/routes/api/v2/licenses/[licenseKey]/schedule/server.spec.ts new file mode 100644 index 0000000..0cd0f86 --- /dev/null +++ b/src/routes/api/v2/licenses/[licenseKey]/schedule/server.spec.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { isHttpError } from '@sveltejs/kit'; +import { _clearRateLimits } from '$lib/server/v2-rate-limit'; + +vi.mock('$lib/db', () => ({ + prisma: { + licenseKey: { findUnique: vi.fn() } + } +})); + +vi.mock('$lib/server/schedule', () => ({ + getScheduleForLicense: vi.fn(), + scheduleBlocksToInput: vi.fn() +})); + +import { prisma } from '$lib/db'; +import { getScheduleForLicense, scheduleBlocksToInput } from '$lib/server/schedule'; +import { GET } from './+server'; + +type GETEvent = Parameters[0]; + +const mockFindUnique = prisma.licenseKey.findUnique as ReturnType; +const mockGetSchedule = getScheduleForLicense as ReturnType; +const mockToInput = scheduleBlocksToInput as ReturnType; + +const KEY = 'ABCD-EFGH-IJKL-MNOP'; + +function event(ifNoneMatch?: string): GETEvent { + const headers = new Headers(); + if (ifNoneMatch) headers.set('if-none-match', ifNoneMatch); + return { + request: new Request(`http://localhost/api/v2/licenses/${KEY}/schedule`, { headers }), + params: { licenseKey: KEY } + } as GETEvent; +} + +beforeEach(() => { + vi.clearAllMocks(); + _clearRateLimits(); +}); + +describe('GET /api/v2/licenses/[licenseKey]/schedule', () => { + it('returns 404 for an unknown license', async () => { + mockFindUnique.mockResolvedValue(null); + await expect(GET(event())).rejects.toSatisfy( + (e: unknown) => isHttpError(e) && e.status === 404 && e.body.message === 'unknown_license' + ); + }); + + it('returns type null with empty lists when there is no schedule', async () => { + mockFindUnique.mockResolvedValue({ id: 'lk1' }); + mockGetSchedule.mockResolvedValue(null); + + const res = await GET(event()); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.type).toBeNull(); + expect(body.daily).toEqual([]); + expect(body.blocks).toEqual([]); + expect(body.version).toBeTruthy(); + expect(res.headers.get('etag')).toBe(`"${body.version}"`); + }); + + it('returns the daily payload with an ETag', async () => { + mockFindUnique.mockResolvedValue({ id: 'lk1' }); + mockGetSchedule.mockResolvedValue({ type: 'daily' }); + mockToInput.mockReturnValue([ + { + id: 'b1', + day: null, + startMinutes: 360, + endMinutes: 420, + dynamicAudio: true, + proclamationIds: ['p1'] + } + ]); + + const res = await GET(event()); + expect(res.status).toBe(200); + const body = (await res.json()) as { type: string; daily: unknown[]; blocks: unknown[] }; + expect(body.type).toBe('daily'); + expect(body.daily).toHaveLength(1); + expect(body.blocks).toHaveLength(1); + }); + + it('returns 304 when If-None-Match matches the ETag', async () => { + mockFindUnique.mockResolvedValue({ id: 'lk1' }); + mockGetSchedule.mockResolvedValue(null); + + const first = await GET(event()); + const etag = first.headers.get('etag') as string; + const second = await GET(event(etag)); + + expect(second.status).toBe(304); + expect(second.headers.get('etag')).toBe(etag); + }); + + it('returns 200 when If-None-Match does not match', async () => { + mockFindUnique.mockResolvedValue({ id: 'lk1' }); + mockGetSchedule.mockResolvedValue(null); + + const res = await GET(event('"stale"')); + expect(res.status).toBe(200); + }); +}); diff --git a/src/routes/api/v2/schedule/+server.ts b/src/routes/api/v2/schedule/+server.ts index da4d212..b0eff46 100644 --- a/src/routes/api/v2/schedule/+server.ts +++ b/src/routes/api/v2/schedule/+server.ts @@ -3,16 +3,24 @@ import type { RequestHandler } from './$types'; import { prisma } from '$lib/db'; import { clientIp, jsonOk, rateLimited } from '$lib/server/v2-helpers'; import { rateLimit } from '$lib/server/v2-rate-limit'; -import { listBlocks } from '$lib/server/schedule'; -import { serializeScheduleBlock } from '$lib/server/v2-serializers'; +import { getScheduleForLicense, scheduleBlocksToInput } from '$lib/server/schedule'; +import { buildSchedulePayload } from '$lib/server/v2-serializers'; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +function scheduleType(value: string | null | undefined) { + return value === 'daily' || value === 'weekly' ? value : null; +} + /** * Machine-to-app endpoint used by the RadiantWave kiosk. Authenticates with * `email` + `licenseKey` query parameters and validates only that the pair * belong together — license active/entitlement is deliberately not checked * here, since an invalid license would already lock the kiosk out. + * + * Extended additively with `version`, `type`, `daily`, and `weekly`. `email` + * and `blocks` keep their exact previous meaning (daily flattening; empty for + * weekly), so old Kiosks are unaffected. No ETag/304 here. */ export const GET: RequestHandler = async ({ request, url }) => { const ip = clientIp(request); @@ -50,14 +58,10 @@ export const GET: RequestHandler = async ({ request, url }) => { throw error(400, "this key doesn't appear to belong to you"); } - const blocks = await listBlocks(user.id); + const schedule = await getScheduleForLicense(storedKey.id); + const type = scheduleType(schedule?.type); + const blocks = schedule ? scheduleBlocksToInput(schedule.blocks) : []; + const payload = buildSchedulePayload(type, blocks); - const serialized = blocks.map((block) => - serializeScheduleBlock( - block, - block.proclamations.map((entry) => entry.proclamation.id) - ) - ); - - return jsonOk({ email, blocks: serialized }); + return jsonOk({ email, ...payload }); }; diff --git a/src/routes/api/v2/schedule/schedule.spec.ts b/src/routes/api/v2/schedule/schedule.spec.ts index cf26dee..228e8ae 100644 --- a/src/routes/api/v2/schedule/schedule.spec.ts +++ b/src/routes/api/v2/schedule/schedule.spec.ts @@ -3,19 +3,24 @@ import { _clearRateLimits } from '$lib/server/v2-rate-limit'; vi.mock('$lib/db', () => ({ prisma: { - schedule: { findUnique: vi.fn(), create: vi.fn() }, - block: { findMany: vi.fn() }, licenseKey: { findUnique: vi.fn() }, user: { findUnique: vi.fn() } } })); +vi.mock('$lib/server/schedule', () => ({ + getScheduleForLicense: vi.fn(), + scheduleBlocksToInput: vi.fn() +})); + import { prisma } from '$lib/db'; +import { getScheduleForLicense, scheduleBlocksToInput } from '$lib/server/schedule'; import { GET } from './+server'; const licenseKeyFindUnique = prisma.licenseKey.findUnique as ReturnType; const userFindUnique = prisma.user.findUnique as ReturnType; -const blockFindMany = prisma.block.findMany as ReturnType; +const getScheduleMock = getScheduleForLicense as ReturnType; +const toInputMock = scheduleBlocksToInput as ReturnType; function call(params: Record) { const url = new URL('http://localhost/api/v2/schedule'); @@ -29,7 +34,8 @@ const KEY = 'AAAA-BBBB-CCCC-DDDD'; beforeEach(() => { vi.clearAllMocks(); _clearRateLimits(); - blockFindMany.mockResolvedValue([]); + getScheduleMock.mockResolvedValue(null); + toInputMock.mockReturnValue([]); }); describe('GET /api/v2/schedule auth', () => { @@ -50,7 +56,7 @@ describe('GET /api/v2/schedule auth', () => { }); it('rejects an unknown user', async () => { - licenseKeyFindUnique.mockResolvedValue({ key: KEY, userId: 'user-1', email: null }); + licenseKeyFindUnique.mockResolvedValue({ id: 'lk1', key: KEY, userId: 'user-1', email: null }); userFindUnique.mockResolvedValue(null); await expect(call({ email: 'jane@example.com', licenseKey: KEY })).rejects.toMatchObject({ @@ -60,7 +66,12 @@ describe('GET /api/v2/schedule auth', () => { }); it('rejects a key that does not belong to the email account', async () => { - licenseKeyFindUnique.mockResolvedValue({ key: KEY, userId: 'someone-else', email: null }); + licenseKeyFindUnique.mockResolvedValue({ + id: 'lk1', + key: KEY, + userId: 'someone-else', + email: null + }); userFindUnique.mockResolvedValue({ id: 'user-1', email: 'jane@example.com' }); await expect(call({ email: 'jane@example.com', licenseKey: KEY })).rejects.toMatchObject({ @@ -88,52 +99,62 @@ describe('GET /api/v2/schedule auth', () => { describe('GET /api/v2/schedule success', () => { it('returns only proclamation IDs, marking an overnight block, even when the key is inactive', async () => { licenseKeyFindUnique.mockResolvedValue({ + id: 'lk1', key: KEY, userId: 'user-1', email: null, active: false }); userFindUnique.mockResolvedValue({ id: 'user-1', email: 'jane@example.com' }); - blockFindMany.mockResolvedValue([ + getScheduleMock.mockResolvedValue({ type: 'daily' }); + toInputMock.mockReturnValue([ { id: 'block-1', + day: null, startMinutes: 21 * 60, endMinutes: 9 * 60, dynamicAudio: true, - proclamations: [{ proclamation: { id: 'p1' } }, { proclamation: { id: 'p2' } }] + proclamationIds: ['p1', 'p2'] } ]); const response = await call({ email: 'jane@example.com', licenseKey: KEY }); expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ - email: 'jane@example.com', - blocks: [ - { - id: 'block-1', - startTime: '21:00', - startMinutes: 1260, - endTime: '09:00', - endMinutes: 540, - overnight: true, - durationMinutes: 720, - dynamicAudio: true, - proclamationIds: ['p1', 'p2'] - } - ] - }); + const body = (await response.json()) as { email: string; blocks: unknown[]; type: string }; + expect(body.email).toBe('jane@example.com'); + expect(body.blocks).toEqual([ + { + id: 'block-1', + startTime: '21:00', + startMinutes: 1260, + endTime: '09:00', + endMinutes: 540, + overnight: true, + durationMinutes: 720, + dynamicAudio: true, + proclamationIds: ['p1', 'p2'] + } + ]); + // Additive fields for new Kiosks. + expect(body.type).toBe('daily'); + expect(body).toHaveProperty('version'); + expect(body).toHaveProperty('daily'); + expect(body).toHaveProperty('weekly'); }); it('returns IDs for every selected proclamation regardless of frequency', async () => { - licenseKeyFindUnique.mockResolvedValue({ key: KEY, userId: 'user-1', email: null }); + licenseKeyFindUnique.mockResolvedValue({ id: 'lk1', key: KEY, userId: 'user-1', email: null }); userFindUnique.mockResolvedValue({ id: 'user-1', email: 'jane@example.com' }); - blockFindMany.mockResolvedValue([ + getScheduleMock.mockResolvedValue({ type: 'daily' }); + toInputMock.mockReturnValue([ { id: 'block-1', + day: null, startMinutes: 0, endMinutes: 60, - proclamations: [{ proclamation: { id: 'p1' } }] + dynamicAudio: false, + proclamationIds: ['p1'] } ]); @@ -157,6 +178,7 @@ describe('GET /api/v2/schedule success', () => { it('accepts the legacy `key` query alias and an email-only license', async () => { licenseKeyFindUnique.mockResolvedValue({ + id: 'lk1', key: KEY, userId: null, email: 'jane@example.com' @@ -166,6 +188,9 @@ describe('GET /api/v2/schedule success', () => { const response = await call({ email: 'jane@example.com', key: KEY }); expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ email: 'jane@example.com', blocks: [] }); + const body = (await response.json()) as { email: string; blocks: unknown[]; type: null }; + expect(body.email).toBe('jane@example.com'); + expect(body.blocks).toEqual([]); + expect(body.type).toBeNull(); }); }); From 9a2888af3684acca7d71703b34ca0ddef26e6fa8 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:28:05 -0500 Subject: [PATCH 10/25] Portal: per-system daily/weekly schedule editor System picker lists the account's License Keys labelled by System Name (fallback: key). Schedule type selector (Daily|Weekly) with a confirmation before switching a populated schedule (daily->weekly prefills all seven days; otherwise clears). Weekly shows a per-day editor. Blocks use TimeInput, allow adjacent/midnight-crossing ranges (labelled 'ends next day'), block empty blocks client-side, default dynamic audio on, and show title/category/group only. Adds gap helper text and an informational kiosk-timezone note (browser fallback; no external/paid dependency). --- src/lib/components/schedule/BlockModal.svelte | 23 +- .../schedule/BlockModal.svelte.spec.ts | 2 +- .../components/schedule/DaySchedule.svelte | 157 +++++----- src/lib/schedule-utils.ts | 6 +- .../(protected)/(user)/schedule/+page.svelte | 294 ++++++++++++++++-- 5 files changed, 371 insertions(+), 111 deletions(-) diff --git a/src/lib/components/schedule/BlockModal.svelte b/src/lib/components/schedule/BlockModal.svelte index 239e0c0..d2cb4f1 100644 --- a/src/lib/components/schedule/BlockModal.svelte +++ b/src/lib/components/schedule/BlockModal.svelte @@ -8,7 +8,7 @@ hmToMinutes, isOvernight, toHm, - validateBlock, + validateBlockAgainstSchedule, type BlockSibling } from '$lib/schedule-utils'; @@ -25,10 +25,13 @@ open = false, mode = 'create', blockId, + day = null, + licenseKeyId = '', initialStartMinutes = 9 * 60, initialEndMinutes = 10 * 60, initialProclamationIds = [], initialDynamicAudio = true, + scheduleType = 'daily' as 'daily' | 'weekly' | null, categories = [], groups = [], proclamations = [], @@ -42,10 +45,13 @@ open?: boolean; mode?: 'create' | 'edit'; blockId?: string; + day?: string | null; + licenseKeyId?: string; initialStartMinutes?: number; initialEndMinutes?: number; initialProclamationIds?: string[]; initialDynamicAudio?: boolean; + scheduleType?: 'daily' | 'weekly' | null; categories?: BrowserCategory[]; groups?: BrowserGroup[]; proclamations?: BrowserProclamation[]; @@ -105,10 +111,11 @@ function validate(): string | null { if (!startValid || !endValid) return 'Enter a valid start and end time.'; - const result = validateBlock( - { startMinutes, endMinutes, proclamationIds: selectedIds }, - siblings, - { isCreate: mode === 'create' } + if (scheduleType === null) return 'Set a schedule type first.'; + const result = validateBlockAgainstSchedule( + scheduleType, + { day, startMinutes, endMinutes, proclamationIds: selectedIds }, + siblings ); return result.ok ? null : result.error; } @@ -193,6 +200,10 @@ {#if mode === 'edit' && blockId} {/if} + + {#if day} + + {/if} @@ -280,7 +291,7 @@

Review

{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight - ? ' (next day)' + ? ' (ends next day)' : ''}

{#if selectedTitles.length === 0} diff --git a/src/lib/components/schedule/BlockModal.svelte.spec.ts b/src/lib/components/schedule/BlockModal.svelte.spec.ts index 79fb486..a07ce81 100644 --- a/src/lib/components/schedule/BlockModal.svelte.spec.ts +++ b/src/lib/components/schedule/BlockModal.svelte.spec.ts @@ -100,7 +100,7 @@ describe('BlockModal', () => { it('marks an overnight block in the review', async () => { mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 9 * 60 }); - await expect.element(page.getByText('9:00 PM – 9:00 AM (next day)')).toBeInTheDocument(); + await expect.element(page.getByText('9:00 PM – 9:00 AM (ends next day)')).toBeInTheDocument(); }); it('rejects an overlapping time range with the specific error', async () => { diff --git a/src/lib/components/schedule/DaySchedule.svelte b/src/lib/components/schedule/DaySchedule.svelte index f8e9c1c..144cfcf 100644 --- a/src/lib/components/schedule/DaySchedule.svelte +++ b/src/lib/components/schedule/DaySchedule.svelte @@ -10,6 +10,16 @@ type ScheduleBlockView = { id: string; + day?: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamations: { id: string; name: string }[]; + }; + + type OpenBlock = { + id: string; + day: string | null; startMinutes: number; endMinutes: number; dynamicAudio: boolean; @@ -18,12 +28,14 @@ let { blocks = [], + label, onCreate, onEdit }: { blocks?: ScheduleBlockView[]; + label?: string; onCreate?: (startMinutes: number, endMinutes: number) => void; - onEdit?: (block: ScheduleBlockView) => void; + onEdit?: (block: OpenBlock) => void; } = $props(); const hourMarks = Array.from({ length: 24 }, (_, i) => i); @@ -97,77 +109,82 @@ ); -
- - - {/each} - {/each} +
- {#if blocks.length === 0} -

- Click or drag to add a block. -

- {/if} +
diff --git a/src/lib/schedule-utils.ts b/src/lib/schedule-utils.ts index 8738873..9fa2682 100644 --- a/src/lib/schedule-utils.ts +++ b/src/lib/schedule-utils.ts @@ -177,7 +177,7 @@ export function validateSchedule(type: ScheduleType, blocks: ValidationBlock[]): } const overlap = findScheduleOverlap(type, blocks); if (overlap) { - return { ok: false, error: overlapError(overlap.a, overlap.b) }; + return { ok: false, error: overlapError(overlap.a) }; } return { ok: true }; } @@ -195,12 +195,12 @@ export function validateBlockAgainstSchedule( if (!shape.ok) return shape; const overlap = findScheduleOverlap(type, [...siblings, candidate]); if (overlap) { - return { ok: false, error: overlapError(overlap.a, overlap.b) }; + return { ok: false, error: overlapError(overlap.a) }; } return { ok: true }; } -function overlapError(a: ValidationBlock, b: ValidationBlock): string { +function overlapError(a: ValidationBlock): string { return `This overlaps with your ${formatTime(a.startMinutes)}–${formatTime(a.endMinutes)} block.`; } diff --git a/src/routes/(protected)/(user)/schedule/+page.svelte b/src/routes/(protected)/(user)/schedule/+page.svelte index 0ecc0df..f9981e2 100644 --- a/src/routes/(protected)/(user)/schedule/+page.svelte +++ b/src/routes/(protected)/(user)/schedule/+page.svelte @@ -1,23 +1,109 @@ +{#if typeForm === undefined}{/if} + +
@@ -60,14 +153,23 @@ Schedule

- Build your daily listening schedule. Up to 4 blocks per day. + Build a listening schedule for a system. Up to 4 blocks per day.

- - {data.blocks.length} of 4 blocks - + {#if data.licenses.length > 0} + + {/if}
{#if successMessage} @@ -77,24 +179,154 @@ {successMessage}
{/if} + {#if errorMessage} + + {/if} -
- -
+ {#if !selectedLicense} +
+

+ You don't have any systems yet. A system appears here once a Kiosk validates its License + Key. +

+
+ {:else} + +
+
+ System +

+ {licenseLabel} + {#if !selectedLicense.systemName} + (unnamed system) + {/if} +

+
+
+ Schedule type +
+ + +
+ {#if type !== null} + + {/if} +
+
- + {#if type === null} +
+

+ No schedule. Choose Daily or Weekly to start. +

+
+
+ Times run in the Kiosk's own timezone ({selectedLicense.systemName ?? selectedLicense.key} + sets its own timezone). They are not tied to your browser's timezone. +
+ {:else if type === 'daily'} +
+ openCreate(null, s, e)} + onEdit={openEdit} + /> +
+ {:else} +
+ {#each WEEK_DAYS as day (day.key)} + openCreate(day.key, s, e)} + onEdit={openEdit} + /> + {/each} +
+ {/if} + +

+ Gaps between blocks are fine: the Kiosk plays its locally selected Proclamations during a gap. + Times run in the Kiosk's own timezone ({selectedLicense.systemName ?? selectedLicense.key}), + not your browser's. +

+ + + {/if} + +{#if showTypeConfirm} +
+
+

Change schedule type?

+

+ Switching type replaces the current blocks. + {#if typePending === 'weekly'} + The Daily blocks will be copied to all seven days. + {:else} + The existing blocks will be cleared. + {/if} +

+
+ + +
+
+
+{/if} From f6c937b892572570892c83574c4e7b8f90fc89ac Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:35:44 -0500 Subject: [PATCH 11/25] docs: document Phase 3 schedule endpoint, model, and resolved open items --- docs/openapi.yaml | 121 +++++++++++++++++++++++++++++++--- docs/schedule-api-contract.md | 63 +++++++++++------- 2 files changed, 153 insertions(+), 31 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index a7a7a2a..1d70c1a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -245,13 +245,16 @@ paths: pairing. Returns every Block with its start/end times and the ordered list of - selected Proclamation IDs. Overnight blocks (an end clock time before - the start) are supported and flagged with `overnight: true` and a - wrapping `durationMinutes`. Each block also carries `dynamicAudio`; - when true, the kiosk enables dynamic audio for the block's duration, - overriding any audio selections made on the device. Proclamation - content is not included; the kiosk fetches it separately via - `/api/v2/proclamations`. + selected Proclamation IDs (the legacy daily flattening in `blocks`), and + is extended additively with `version`, `type`, `daily`, and `weekly`. + Overnight blocks (an end clock time before the start) are supported and + flagged with `overnight: true` and a wrapping `durationMinutes`. Each + block also carries `dynamicAudio`; when true, the kiosk enables dynamic + audio for the block's duration, overriding any audio selections made on + the device. Proclamation content is not included; the kiosk fetches it + separately via `/api/v2/proclamations`. For `type: "weekly"`, `blocks` is + an empty array (the server cannot flatten weekly in the kiosk's + timezone). This endpoint emits no ETag and never returns 304. operationId: getSchedule parameters: - in: query @@ -309,6 +312,66 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + /api/v2/licenses/{licenseKey}/schedule: + get: + tags: [Schedule] + summary: Get a License Key's schedule (Kiosk-facing, conditional) + description: | + Kiosk-facing, License-Key-only schedule read. Returns the target payload + (`version`, `type`, `daily`, `weekly`, `blocks`). `version` is a + content-derived opaque string and is also the `ETag`; a matching + `If-None-Match` returns `304`. An unknown License Key is `404`; a key + with no Schedule returns `type: null` with empty lists. No + authentication (see the API description) beyond the 10/min/IP limit. + operationId: getLicenseSchedule + parameters: + - in: path + name: licenseKey + required: true + schema: + type: string + pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$' + - in: header + name: If-None-Match + required: false + schema: + type: string + description: Prior ETag; when it matches, returns 304. + responses: + '200': + description: The target schedule payload. + headers: + ETag: + schema: + type: string + description: The quoted `version`. + content: + application/json: + schema: + $ref: '#/components/schemas/SchedulePayload' + '304': + description: Not modified; the caller keeps its cached payload. + headers: + ETag: + schema: + type: string + '404': + description: 'Unknown license (`unknown_license`).' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + '429': + description: Rate limit exceeded (10 requests per minute per IP). + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before retrying. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' /api/v2/licenses/{licenseKey}/system-name: put: tags: [License] @@ -661,9 +724,51 @@ components: email: type: string format: email + version: + type: string + type: + type: string + nullable: true + enum: [daily, weekly, null] + daily: + type: array + items: + $ref: '#/components/schemas/ScheduleBlock' + weekly: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ScheduleBlock' blocks: type: array - maxItems: 4 + items: + $ref: '#/components/schemas/ScheduleBlock' + SchedulePayload: + type: object + required: [version, type, daily, weekly, blocks] + properties: + version: + type: string + description: Opaque content hash; doubles as the ETag. + type: + type: string + nullable: true + enum: [daily, weekly, null] + daily: + type: array + items: + $ref: '#/components/schemas/ScheduleBlock' + weekly: + type: object + description: Present with all seven keys when `type` is weekly. + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ScheduleBlock' + blocks: + type: array + description: Legacy daily flattening; empty for weekly. items: $ref: '#/components/schemas/ScheduleBlock' OnboardingRequest: diff --git a/docs/schedule-api-contract.md b/docs/schedule-api-contract.md index 9e0dda6..91ec87b 100644 --- a/docs/schedule-api-contract.md +++ b/docs/schedule-api-contract.md @@ -495,45 +495,55 @@ later phase. Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here. 2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no - schedule". -3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained; the - brief does not state a cap. Whether weekly allows 4/day or 4 total is - unresolved. + schedule". Implemented in Phase 3. +3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained. + Phase 3 applies it **per day list** (each of the seven weekly days gets its + own 4-block budget). Whether the cap should be lower is unresolved. 4. **Field naming.** The brief's `start`/`end` were mapped to the existing `startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk. -5. **`version` derivation.** Algorithm (content hash vs max `updatedAt` + - shape) deferred to implementation; it must be stable for equal content and - change on mutation. +5. **`version` derivation.** Resolved in Phase 3: a SHA-1 content hash of the + serialized `{type, daily, weekly}` shape. Stable for equal content, changes + on any mutation. 6. **Unauthenticated key-only access.** The new endpoints authenticate by License Key alone (per glossary). Anyone who knows a key could read the schedule or set the name. Mitigated only by the 10/min/IP rate limit. Known limitation. -7. **`dynamicAudio` default flip.** DB default changes `false` → `true` for - **new** Blocks; existing Blocks are left at their stored values (no - backfill assumed). -8. **Per-user → per-License-Key migration.** Accounts with multiple License - Keys: which key inherits the existing per-user Schedule is unresolved; - conservative default is the account's first/oldest key. -9. **Cross-day spillover validation.** Weekly `sun` spillover into `mon` (and - daily same-list spillover) must be validated; exact error messaging is - deferred. +7. **`dynamicAudio` default flip.** DB default changed `false` → `true` for + **new** Blocks in Phase 3; existing Blocks keep their stored values. +8. **Per-user → per-License-Key migration.** Resolved in Phase 3: each existing + per-user Schedule is mapped to that account's **oldest** License Key + (`createdAt ASC`), as a daily schedule. Schedules with no owning + user/license are retained with `licenseKeyId NULL` and reported via a + migration `NOTICE` (not dropped). +9. **Cross-day spillover validation.** Implemented in Phase 3: weekly + `sun`→`mon` and daily self-wrap spillover are rejected as overlaps; the + shared `validateSchedule` is authoritative and mirrored client-side. 10. **Account scope for name uniqueness.** "Per account" = the owning User. - Legacy License Keys with only an `email` and no `userId` are an edge case. -11. **Geoip timezone display.** Informational only, not implemented in the - Portal today. + Legacy License Keys with only an `email` and no `userId` are an edge case + (written without a per-account uniqueness check). +11. **Geoip timezone display.** Phase 3 shows an informational note that times + run in the **Kiosk's** timezone and uses the **browser-resolved** timezone + as a fallback; no geoip dependency (paid or external) was added. The + Portal still stores no timezone. 12. **ETag on the legacy endpoint.** The legacy `GET /api/v2/schedule` emits no ETag and never returns 304, to preserve old behaviour. 13. **`systemNameStatus` on v1.** Not added; v1 is unchanged. 14. **Trim/case representation.** Names are trimmed; uniqueness is case-insensitive; the stored casing is preserved as provided. -15. **Sync cadence.** The brief's boot/network-up/5-minute defaults are - targets; the current Kiosk has no such timer. -16. **Atomic cache file.** Path and format deferred; SQLite is the current - durable store. +15. **Sync cadence.** Resolved in Phase 3: the Kiosk loads its on-disk cache on + boot, then pulls on boot, on a 5-minute timer, and on demand. +16. **Atomic cache file.** Resolved in Phase 3: + `~/.local/radiantwave/schedule_cache.json`, written atomically (temp file + + rename); a corrupt cache is ignored. 17. **`type` encoding for no schedule.** `type: null`; `daily`/`weekly` absent or empty. 18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the legacy schedule endpoint. +19. **Kiosk local toggle.** `proclamation.schedule_enabled` in `config.yaml`, + default **on**, surfaced as System Settings → Schedule. Not synced. When + off, the engine plays local selections but the cache keeps updating. The + older `proclamation.schedule_active` flag is retained in config but is not + consulted by the Phase 3 engine. --- @@ -544,3 +554,10 @@ later phase. (`/api/v2/licenses/...`, additive license-validation fields), sync behavior, System Name rules, dynamic audio, backward compatibility, touchpoints, open items, and fixtures. +- 2026-09-29 — Phase 3. Schedule is now per License Key with daily/weekly and + `Block.day`; the shared `validateSchedule` is authoritative; the key-only + `GET /api/v2/licenses/{licenseKey}/schedule` (ETag/304) ships and the legacy + endpoint is extended additively; the Portal editor is per-system with a + browser-fallback timezone note; the Kiosk adds the conditional client, + atomic cache, schedule engine, and a System Settings On/Off toggle. Open + items 2, 5, 7, 8, 9, 11, 15, and 16 are updated to reflect the changes. From 68a979f1a118b4b57a87dd3564d71c61cfd981c0 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:49:06 -0500 Subject: [PATCH 12/25] Portal: transactional schedule copy service copySchedule copies one License Key's Schedule to one or more other keys by value in a single Prisma transaction (all targets or none), assigning fresh Block IDs and preserving each block's dynamicAudio and Proclamation order. Source/targets are scoped to the caller's account; self-copy and empty target lists are rejected. The resulting schedule is run through the shared validator before commit. Adds tests: daily/weekly copy, overwrite, multiple targets, rollback, cross-account rejection, new IDs, changed version/ETag, copy independence, and Kiosk-payload parity with the shared fixtures. --- src/lib/server/schedule-copy.spec.ts | 414 +++++++++++++++++++++++++++ src/lib/server/schedule.ts | 135 +++++++++ 2 files changed, 549 insertions(+) create mode 100644 src/lib/server/schedule-copy.spec.ts diff --git a/src/lib/server/schedule-copy.spec.ts b/src/lib/server/schedule-copy.spec.ts new file mode 100644 index 0000000..6ae91d2 --- /dev/null +++ b/src/lib/server/schedule-copy.spec.ts @@ -0,0 +1,414 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// In-memory Prisma double shared by the service and the assertions. It supports +// just the operations `copySchedule` uses, plus a shared store so we can read +// back copies and compute their Kiosk-facing payloads/ETags. It is created via +// vi.hoisted so the `vi.mock` factory (which is hoisted above imports) can see +// it. +const store = vi.hoisted(() => { + type Block = { + id: string; + scheduleId: string; + day: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamations: { proclamation: { id: string }; sortOrder: number }[]; + }; + type Schedule = { id: string; licenseKeyId: string | null; type: string | null }; + + const state = { + schedules: [] as Schedule[], + blocks: [] as Block[], + idCounter: 0, + failOnBlockCreate: false + }; + + function nextId(prefix: string) { + state.idCounter += 1; + return `${prefix}-${state.idCounter}`; + } + + const tx = { + schedule: { + findUnique: async ({ where }: { where: { licenseKeyId?: string; id?: string } }) => { + if (where.licenseKeyId !== undefined) { + return state.schedules.find((s) => s.licenseKeyId === where.licenseKeyId) ?? null; + } + return state.schedules.find((s) => s.id === where.id) ?? null; + }, + create: async ({ data }: { data: { licenseKeyId: string; type: string | null } }) => { + const row: Schedule = { id: nextId('sched'), ...data }; + state.schedules.push(row); + return row; + }, + update: async ({ where, data }: { where: { id: string }; data: Partial }) => { + const row = state.schedules.find((s) => s.id === where.id); + if (!row) throw new Error('missing schedule'); + Object.assign(row, data); + return row; + } + }, + block: { + deleteMany: async ({ where }: { where: { scheduleId: string } }) => { + state.blocks = state.blocks.filter((b) => b.scheduleId !== where.scheduleId); + return { count: 0 }; + }, + create: async ({ data }: { data: Record }) => { + if (state.failOnBlockCreate) throw new Error('db down'); + const { proclamations, ...rest } = data as { + proclamations: { create: { proclamationId: string; sortOrder: number }[] }; + } & Record; + const row: Block = { + id: nextId('block'), + scheduleId: rest.scheduleId as string, + day: (rest.day as string | null) ?? null, + startMinutes: rest.startMinutes as number, + endMinutes: rest.endMinutes as number, + dynamicAudio: rest.dynamicAudio as boolean, + proclamations: proclamations.create.map((p) => ({ + proclamation: { id: p.proclamationId }, + sortOrder: p.sortOrder + })) + }; + state.blocks.push(row); + return row; + } + } + }; + + const transaction = vi.fn(async (fn: (txArg: unknown) => Promise) => { + const scheduleSnapshot = state.schedules.map((s) => ({ ...s })); + const blockSnapshot = state.blocks.map((b) => ({ ...b, proclamations: [...b.proclamations] })); + try { + await fn(tx); + } catch (error) { + state.schedules = scheduleSnapshot; + state.blocks = blockSnapshot; + throw error; + } + }); + + const scheduleFindUnique = vi.fn(); + + return { state, tx, transaction, scheduleFindUnique, nextId }; +}); + +vi.mock('$lib/db', () => ({ + prisma: { + schedule: { findUnique: store.scheduleFindUnique }, + $transaction: store.transaction + } +})); + +import { buildSchedulePayload } from './v2-serializers'; +import { copySchedule, scheduleBlocksToInput, type OwnedLicense } from './schedule'; + +const owned: OwnedLicense[] = [ + { id: 'lk-src', systemName: 'Living Room', key: 'AAAA-AAAA-AAAA-AAAA' }, + { id: 'lk-t1', systemName: 'Kitchen', key: 'BBBB-BBBB-BBBB-BBBB' }, + { id: 'lk-t2', systemName: null, key: 'CCCC-CCCC-CCCC-CCCC' } +]; + +/** Seed the source schedule and a full block list. */ +function seedSource( + type: 'daily' | 'weekly', + blockSpecs: Partial<(typeof state.blocks)[number]>[] +) { + store.state.schedules = [{ id: 'sched-src', licenseKeyId: 'lk-src', type }]; + store.state.blocks = blockSpecs.map((spec, i) => ({ + id: `src-block-${i}`, + scheduleId: 'sched-src', + day: spec.day ?? null, + startMinutes: spec.startMinutes ?? 0, + endMinutes: spec.endMinutes ?? 60, + dynamicAudio: spec.dynamicAudio ?? true, + proclamations: spec.proclamations ?? [{ proclamation: { id: 'p1' }, sortOrder: 0 }] + })); + store.scheduleFindUnique.mockImplementation( + async ({ where }: { where: { licenseKeyId: string } }) => { + if (where.licenseKeyId !== 'lk-src') return null; + return { + ...store.state.schedules.find((s) => s.licenseKeyId === 'lk-src'), + blocks: store.state.blocks + .filter((b) => b.scheduleId === 'sched-src') + .map((b) => ({ ...b, proclamations: b.proclamations })) + }; + } + ); +} + +const state = store.state; + +function payloadFor(licenseKeyId: string) { + const schedule = state.schedules.find((s) => s.licenseKeyId === licenseKeyId) ?? null; + const scheduleBlocks = state.blocks.filter((b) => b.scheduleId === schedule?.id); + return buildSchedulePayload( + schedule?.type === 'daily' || schedule?.type === 'weekly' ? schedule.type : null, + scheduleBlocksToInput( + scheduleBlocks.map((b) => ({ + id: b.id, + day: b.day, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamations: b.proclamations.sort((x, y) => x.sortOrder - y.sortOrder).map((p) => p) + })) + ) + ); +} + +beforeEach(() => { + vi.clearAllMocks(); + state.idCounter = 0; + state.failOnBlockCreate = false; + state.schedules = []; + state.blocks = []; +}); + +describe('copySchedule', () => { + it('copies a daily schedule and changes the target version', async () => { + seedSource('daily', [ + { startMinutes: 360, endMinutes: 480, dynamicAudio: true }, + { startMinutes: 1080, endMinutes: 1230, dynamicAudio: false } + ]); + const before = payloadFor('lk-t1'); // no schedule yet -> null payload + + const result = await copySchedule(owned, 'lk-src', ['lk-t1']); + + expect(result.ok).toBe(true); + const after = payloadFor('lk-t1'); + expect(after.type).toBe('daily'); + expect(after.daily).toHaveLength(2); + expect(after.daily[0].dynamicAudio).toBe(true); + expect(after.daily[1].dynamicAudio).toBe(false); + expect(after.version).not.toBe(before.version); + }); + + it('copies a weekly schedule preserving day layout', async () => { + seedSource('weekly', [ + { day: 'mon', startMinutes: 360, endMinutes: 480 }, + { day: 'sun', startMinutes: 1380, endMinutes: 60 } + ]); + + const result = await copySchedule(owned, 'lk-src', ['lk-t1']); + + expect(result.ok).toBe(true); + const after = payloadFor('lk-t1'); + expect(after.type).toBe('weekly'); + expect(Object.keys(after.weekly).sort()).toEqual([ + 'fri', + 'mon', + 'sat', + 'sun', + 'thu', + 'tue', + 'wed' + ]); + expect(after.weekly.mon).toHaveLength(1); + expect(after.weekly.sun).toHaveLength(1); + expect(after.weekly.tue).toEqual([]); + expect(after.blocks).toEqual([]); + }); + + it('overwrites an existing target schedule', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + state.schedules.push({ id: 'sched-t1', licenseKeyId: 'lk-t1', type: 'daily' }); + state.blocks.push({ + id: 'old-block', + scheduleId: 'sched-t1', + day: null, + startMinutes: 1000, + endMinutes: 1030, + dynamicAudio: false, + proclamations: [{ proclamation: { id: 'old' }, sortOrder: 0 }] + }); + + const result = await copySchedule(owned, 'lk-src', ['lk-t1']); + + expect(result.ok).toBe(true); + const target = state.blocks.filter((b) => b.scheduleId === 'sched-t1'); + expect(target).toHaveLength(1); + expect(target[0].startMinutes).toBe(360); + expect(target[0].proclamations).toEqual([{ proclamation: { id: 'p1' }, sortOrder: 0 }]); + }); + + it('copies to multiple targets', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + + const result = await copySchedule(owned, 'lk-src', ['lk-t1', 'lk-t2']); + + expect(result).toMatchObject({ ok: true, copied: 2 }); + expect(payloadFor('lk-t1').daily).toHaveLength(1); + expect(payloadFor('lk-t2').daily).toHaveLength(1); + }); + + it('rolls back every target when one fails', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + state.failOnBlockCreate = true; + + const result = await copySchedule(owned, 'lk-src', ['lk-t1', 'lk-t2']); + + expect(result.ok).toBe(false); + // No target schedule was left behind. + expect(state.schedules.find((s) => s.licenseKeyId === 'lk-t1')).toBeUndefined(); + expect(state.schedules.find((s) => s.licenseKeyId === 'lk-t2')).toBeUndefined(); + expect(state.blocks.filter((b) => b.scheduleId !== 'sched-src')).toHaveLength(0); + }); + + it('rejects a target outside the account', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + const result = await copySchedule(owned, 'lk-src', ['lk-other']); + expect(result).toEqual({ ok: false, error: 'Target system not found.' }); + expect(store.transaction).not.toHaveBeenCalled(); + }); + + it('rejects a target equal to the source', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + const result = await copySchedule(owned, 'lk-src', ['lk-src']); + expect(result.ok).toBe(false); + expect(store.transaction).not.toHaveBeenCalled(); + }); + + it('rejects a source outside the account', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + const result = await copySchedule(owned, 'lk-other', ['lk-t1']); + expect(result).toEqual({ ok: false, error: 'Source system not found.' }); + }); + + it('rejects a source with no schedule', async () => { + state.schedules = []; + state.blocks = []; + store.scheduleFindUnique.mockResolvedValue(null); + const result = await copySchedule(owned, 'lk-src', ['lk-t1']); + expect(result).toEqual({ ok: false, error: 'The source system has no schedule to copy.' }); + }); + + it('assigns new Block IDs on the copy', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + await copySchedule(owned, 'lk-src', ['lk-t1']); + + const copied = state.blocks.filter((b) => b.scheduleId !== 'sched-src'); + expect(copied).toHaveLength(1); + expect(copied[0].id).not.toBe('src-block-0'); + expect(copied[0].id).toMatch(/^block-/); + }); + + it('produces independent copies (source edits do not affect targets)', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + await copySchedule(owned, 'lk-src', ['lk-t1']); + const targetVersion = payloadFor('lk-t1').version; + + // Edit the source in place. + state.blocks = state.blocks.map((b) => + b.scheduleId === 'sched-src' ? { ...b, endMinutes: 999 } : b + ); + + expect(payloadFor('lk-t1').version).toBe(targetVersion); + expect(payloadFor('lk-src').version).not.toBe(targetVersion); + }); + + it('rejects a target list with no targets', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + const result = await copySchedule(owned, 'lk-src', []); + expect(result).toEqual({ ok: false, error: 'Pick at least one system to copy to.' }); + }); + + it('changes the target ETag to match the served payload version', async () => { + seedSource('daily', [{ startMinutes: 360, endMinutes: 480 }]); + await copySchedule(owned, 'lk-src', ['lk-t1']); + + const payload = payloadFor('lk-t1'); + const etag = `"${payload.version}"`; + const expected = `"${createHash('sha1') + .update(JSON.stringify({ type: payload.type, daily: payload.daily, weekly: payload.weekly })) + .digest('hex')}"`; + expect(etag).toBe(expected); + }); +}); + +describe('copied payload is consumed like any Kiosk schedule', () => { + const readFixture = (name: string) => + JSON.parse(readFileSync(resolve(process.cwd(), 'docs/fixtures', name), 'utf8')); + + it('matches the daily fixture shape after copy (IDs differ)', async () => { + const fixture = readFixture('schedule-daily.json'); + // Seed the source from the daily fixture content. + seedSource( + 'daily', + fixture.daily.map((b: Record) => ({ + day: null, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamations: (b.proclamationIds as string[]).map((id, i) => ({ + proclamation: { id }, + sortOrder: i + })) + })) + ); + + const result = await copySchedule(owned, 'lk-src', ['lk-t1']); + expect(result.ok).toBe(true); + + const payload = payloadFor('lk-t1'); + expect(payload.type).toBe('daily'); + // Same times/audio/ids as the fixture; only block IDs differ. + expect( + payload.daily.map((b) => ({ + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamationIds: b.proclamationIds + })) + ).toEqual( + fixture.daily.map((b: Record) => ({ + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamationIds: b.proclamationIds + })) + ); + expect(payload.blocks).toEqual(payload.daily); + }); + + it('matches the weekly fixture day layout after copy', async () => { + const fixture = readFixture('schedule-weekly.json'); + const specs: Record[] = []; + for (const [day, list] of Object.entries( + fixture.weekly as Record[]> + )) { + for (const b of list) { + specs.push({ + day, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + dynamicAudio: b.dynamicAudio, + proclamations: (b.proclamationIds as string[]).map((id, i) => ({ + proclamation: { id }, + sortOrder: i + })) + }); + } + } + seedSource('weekly', specs); + + await copySchedule(owned, 'lk-src', ['lk-t1']); + const payload = payloadFor('lk-t1'); + + expect(payload.type).toBe('weekly'); + expect(payload.blocks).toEqual([]); + const nonEmptyDays = Object.entries(payload.weekly) + .filter(([, list]) => list.length > 0) + .map(([day]) => day) + .sort(); + const fixtureDays = Object.entries(fixture.weekly as Record) + .filter(([, list]) => list.length > 0) + .map(([day]) => day) + .sort(); + expect(nonEmptyDays).toEqual(fixtureDays); + }); +}); diff --git a/src/lib/server/schedule.ts b/src/lib/server/schedule.ts index 8ee5eb8..7a9e4db 100644 --- a/src/lib/server/schedule.ts +++ b/src/lib/server/schedule.ts @@ -371,3 +371,138 @@ export function scheduleBlocksToInput( proclamationIds: block.proclamations.map((entry) => entry.proclamation.id) })); } + +// ─── Copy a Schedule between License Keys ────────────────────────────── + +export type CopyScheduleResult = + | { ok: true; copied: number; targets: { id: string; label: string }[] } + | { ok: false; error: string }; + +/** A License Key row as needed for ownership checks and labels. */ +export type OwnedLicense = { id: string; systemName: string | null; key: string }; + +function licenseLabel(license: { systemName: string | null; key: string }): string { + return license.systemName ?? license.key; +} + +/** + * Copy one License Key's Schedule to one or more other License Keys, by value, + * in a single transaction: all targets succeed or none does. Targets get the + * source's `type` and fresh Block IDs (so every target's content-derived + * `version`/ETag changes and its Kiosk refetches). `dynamicAudio` and + * Proclamation references are preserved. + * + * `ownedLicenses` must be the caller's account License Keys; the source and + * every target are checked against it. The source must have a Schedule with a + * non-null type. Each resulting Schedule is validated before commit. + */ +export async function copySchedule( + ownedLicenses: OwnedLicense[], + sourceId: string, + targetIds: string[] +): Promise { + const byId = new Map(ownedLicenses.map((l) => [l.id, l])); + const source = byId.get(sourceId); + if (!source) return { ok: false, error: 'Source system not found.' }; + + const uniqueTargets = [...new Set(targetIds)].filter((id) => id.length > 0); + if (uniqueTargets.length === 0) { + return { ok: false, error: 'Pick at least one system to copy to.' }; + } + if (uniqueTargets.some((id) => id === sourceId)) { + return { ok: false, error: "A schedule can't be copied onto its own system." }; + } + for (const id of uniqueTargets) { + if (!byId.has(id)) return { ok: false, error: 'Target system not found.' }; + } + + const sourceSchedule = await prisma.schedule.findUnique({ + where: { licenseKeyId: sourceId }, + include: { blocks: { include: blockInclude } } + }); + const sourceType: ScheduleType | null = + sourceSchedule && isScheduleType(sourceSchedule.type) ? sourceSchedule.type : null; + if (!sourceSchedule || sourceType === null) { + return { ok: false, error: 'The source system has no schedule to copy.' }; + } + + // Snapshot the source content (order preserved). + const sourceBlocks = sourceSchedule.blocks.map((block) => ({ + day: block.day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + dynamicAudio: block.dynamicAudio, + proclamationIds: block.proclamations + .sort((a, b) => a.sortOrder - b.sortOrder) + .map((entry) => entry.proclamation.id) + })); + + // Validate the resulting schedule once (it is identical for every target). + const validation = validateSchedule( + sourceType, + sourceBlocks.map((b) => + toValidationBlock({ + day: b.day, + startMinutes: b.startMinutes, + endMinutes: b.endMinutes, + proclamationIds: b.proclamationIds + }) + ) + ); + if (!validation.ok) { + return { ok: false, error: `The source schedule is invalid: ${validation.error}` }; + } + + try { + await prisma.$transaction(async (tx) => { + for (const targetId of uniqueTargets) { + // Upsert the target Schedule, replacing its type. + const existing = await tx.schedule.findUnique({ where: { licenseKeyId: targetId } }); + let scheduleId: string; + if (existing) { + await tx.block.deleteMany({ where: { scheduleId: existing.id } }); + await tx.schedule.update({ + where: { id: existing.id }, + data: { type: sourceType } + }); + scheduleId = existing.id; + } else { + const created = await tx.schedule.create({ + data: { licenseKeyId: targetId, type: sourceType } + }); + scheduleId = created.id; + } + + // New Block IDs per target; Proclamation refs + dynamicAudio kept. + for (const block of sourceBlocks) { + await tx.block.create({ + data: { + scheduleId, + day: block.day, + startMinutes: block.startMinutes, + endMinutes: block.endMinutes, + dynamicAudio: block.dynamicAudio, + proclamations: { + create: block.proclamationIds.map((proclamationId, index) => ({ + proclamationId, + sortOrder: index + })) + } + } + }); + } + } + }); + } catch { + return { ok: false, error: 'Copy failed; no systems were changed.' }; + } + + return { + ok: true, + copied: uniqueTargets.length, + targets: uniqueTargets.map((id) => { + const license = byId.get(id) as OwnedLicense; + return { id, label: licenseLabel(license) }; + }) + }; +} From 242be5d2418277311304f2a274134a665533fdb9 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:49:10 -0500 Subject: [PATCH 13/25] Portal: copy action and hasSchedule flag in the schedule loader Adds a session-authenticated copy action that scopes the source and targets to the caller's License Keys and returns a summary of the systems updated. The loader now reports hasSchedule per system so the Copy UI can warn before overwriting. Adds action tests (auth, cross-account, summary, error). --- .../(user)/schedule/+page.server.ts | 49 ++++++++++- .../(user)/schedule/page.server.spec.ts | 82 ++++++++++++++++++- 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/src/routes/(protected)/(user)/schedule/+page.server.ts b/src/routes/(protected)/(user)/schedule/+page.server.ts index 6b07395..9f5d908 100644 --- a/src/routes/(protected)/(user)/schedule/+page.server.ts +++ b/src/routes/(protected)/(user)/schedule/+page.server.ts @@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from './$types'; import { error, fail } from '@sveltejs/kit'; import { prisma } from '$lib/db'; import { + copySchedule, createBlock, deleteBlock, getScheduleForLicense, @@ -68,8 +69,23 @@ export const load: PageServerLoad = async ({ locals, url }) => { }) ]); + // Which License Keys already have a (typed) Schedule — used by the Copy UI + // to warn before overwriting a target. + const typedSchedules = await prisma.schedule.findMany({ + where: { licenseKeyId: { in: licenses.map((l) => l.id) }, type: { not: null } }, + select: { licenseKeyId: true } + }); + const scheduledIds = new Set(typedSchedules.map((s) => s.licenseKeyId)); + const licenseViews = licenses.map((l) => ({ + id: l.id, + key: l.key, + systemName: l.systemName, + active: l.active, + hasSchedule: scheduledIds.has(l.id) + })); + const requested = url.searchParams.get('license'); - const selected = licenses.find((l) => l.id === requested) ?? licenses[0] ?? null; + const selected = licenseViews.find((l) => l.id === requested) ?? licenseViews[0] ?? null; let type: 'daily' | 'weekly' | null = null; let blocks: { @@ -102,7 +118,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { } return { - licenses, + licenses: licenseViews, selectedLicenseId: selected?.id ?? null, type, blocks, @@ -167,5 +183,34 @@ export const actions: Actions = { const result = await deleteBlock(licenseKeyId, id); if (!result.ok) return fail(400, { error: result.error }); return { success: true }; + }, + + copy: async ({ request, locals }) => { + if (!locals.user?.id) throw error(403, 'not authorized'); + const formData = await request.formData(); + const sourceId = formData.get('sourceLicenseKeyId')?.toString() ?? ''; + const targetIds = formData + .getAll('targetIds') + .map((v) => v.toString()) + .filter((id) => id.length > 0); + + // Only the caller's own systems are eligible; ownership is enforced by + // scoping the lookup to the session user. + const owned = await prisma.licenseKey.findMany({ + where: { userId: locals.user.id }, + select: { id: true, systemName: true, key: true } + }); + if (!owned.some((l) => l.id === sourceId)) { + return fail(404, { error: 'System not found.' }); + } + + const result = await copySchedule(owned, sourceId, targetIds); + if (!result.ok) return fail(400, { error: result.error }); + return { + success: true, + action: 'copy', + copied: result.copied, + targets: result.targets + }; } }; diff --git a/src/routes/(protected)/(user)/schedule/page.server.spec.ts b/src/routes/(protected)/(user)/schedule/page.server.spec.ts index e289202..cc6a96e 100644 --- a/src/routes/(protected)/(user)/schedule/page.server.spec.ts +++ b/src/routes/(protected)/(user)/schedule/page.server.spec.ts @@ -7,7 +7,8 @@ vi.mock('$lib/db', () => ({ proclamationCategory: { findMany: vi.fn() }, proclamationGroup: { findMany: vi.fn() }, proclamation: { findMany: vi.fn() }, - licenseKey: { findMany: vi.fn(), findFirst: vi.fn() } + licenseKey: { findMany: vi.fn(), findFirst: vi.fn() }, + schedule: { findMany: vi.fn() } } })); @@ -16,11 +17,13 @@ vi.mock('$lib/server/schedule', () => ({ setScheduleType: vi.fn(), createBlock: vi.fn(), updateBlock: vi.fn(), - deleteBlock: vi.fn() + deleteBlock: vi.fn(), + copySchedule: vi.fn() })); import { prisma } from '$lib/db'; import { + copySchedule, createBlock, deleteBlock, getScheduleForLicense, @@ -31,10 +34,12 @@ import { actions, load } from './+page.server'; const procFindMany = prisma.proclamation.findMany as ReturnType; const licenseFindMany = prisma.licenseKey.findMany as ReturnType; const licenseFindFirst = prisma.licenseKey.findFirst as ReturnType; +const scheduleFindMany = prisma.schedule.findMany as ReturnType; const getScheduleMock = getScheduleForLicense as ReturnType; const setTypeMock = setScheduleType as ReturnType; const createBlockMock = createBlock as ReturnType; const deleteBlockMock = deleteBlock as ReturnType; +const copyScheduleMock = copySchedule as ReturnType; const locals = createLocals(mockUser() as never); @@ -51,6 +56,7 @@ beforeEach(() => { { id: 'lk1', key: 'AAAA-BBBB-CCCC-DDDD', systemName: 'Living Room', active: true } ]); licenseFindFirst.mockResolvedValue({ id: 'lk1' }); + scheduleFindMany.mockResolvedValue([]); getScheduleMock.mockResolvedValue(null); }); @@ -197,4 +203,76 @@ describe('schedule actions', () => { } expect(deleteBlockMock).not.toHaveBeenCalled(); }); + + it('copy rejects unauthenticated requests', async () => { + await expect( + actions.copy!({ + request: createFormRequest({ sourceLicenseKeyId: 'lk1' }), + locals: createLocals() + } as never) + ).rejects.toMatchObject({ status: 403 }); + }); + + it('copy rejects a source not owned by the account', async () => { + licenseFindMany.mockResolvedValue([]); // none owned + + const result = await actions.copy!({ + request: createFormRequest({ sourceLicenseKeyId: 'lk-other' }), + locals + } as never); + + expect(isActionFailure(result)).toBe(true); + if (isActionFailure(result)) expect(result.status).toBe(404); + expect(copyScheduleMock).not.toHaveBeenCalled(); + }); + + it('copy passes the owned systems and targets and returns a summary', async () => { + licenseFindMany.mockResolvedValue([ + { id: 'lk1', systemName: 'Living Room', key: 'AAAA-BBBB-CCCC-DDDD' }, + { id: 'lk2', systemName: 'Kitchen', key: 'EEEE-FFFF-GGGG-HHHH' } + ]); + copyScheduleMock.mockResolvedValue({ + ok: true, + copied: 1, + targets: [{ id: 'lk2', label: 'Kitchen' }] + }); + + const form = new FormData(); + form.append('sourceLicenseKeyId', 'lk1'); + form.append('targetIds', 'lk2'); + const request = new Request('http://localhost', { method: 'POST', body: form }); + + const result = await actions.copy!({ request, locals } as never); + + expect(result).toEqual({ + success: true, + action: 'copy', + copied: 1, + targets: [{ id: 'lk2', label: 'Kitchen' }] + }); + expect(copyScheduleMock).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ id: 'lk1' }), + expect.objectContaining({ id: 'lk2' }) + ]), + 'lk1', + ['lk2'] + ); + }); + + it('copy surfaces a service error', async () => { + copyScheduleMock.mockResolvedValue({ ok: false, error: 'Target system not found.' }); + + const form = new FormData(); + form.append('sourceLicenseKeyId', 'lk1'); + form.append('targetIds', 'lk-bad'); + const request = new Request('http://localhost', { method: 'POST', body: form }); + + const result = await actions.copy!({ request, locals } as never); + + expect(isActionFailure(result)).toBe(true); + if (isActionFailure(result)) { + expect(result.data).toEqual({ error: 'Target system not found.' }); + } + }); }); From fcb5db458bb514edc796c4fafc8353ed986c28ce Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:49:15 -0500 Subject: [PATCH 14/25] =?UTF-8?q?Portal:=20Copy=20to=E2=80=A6=20schedule?= =?UTF-8?q?=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Copy to… action on a system's schedule with a multi-select of the account's other systems, labelled by System Name (fallback: key), excluding the source. Targets that already have a schedule are flagged and require an explicit Overwrite & copy confirmation. Shows a summary of the systems updated after success. --- .../(protected)/(user)/schedule/+page.svelte | 167 +++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/src/routes/(protected)/(user)/schedule/+page.svelte b/src/routes/(protected)/(user)/schedule/+page.svelte index f9981e2..5dc3bab 100644 --- a/src/routes/(protected)/(user)/schedule/+page.svelte +++ b/src/routes/(protected)/(user)/schedule/+page.svelte @@ -4,7 +4,7 @@ import DaySchedule from '$lib/components/schedule/DaySchedule.svelte'; import BlockModal from '$lib/components/schedule/BlockModal.svelte'; - let { data }: PageProps = $props(); + let { data, form }: PageProps = $props(); const WEEK_DAYS: { key: string; label: string }[] = [ { key: 'mon', label: 'Monday' }, @@ -55,6 +55,34 @@ let typeForm: HTMLFormElement | undefined = $state(); let closeTimer: ReturnType | undefined; + // Copy-to dialog state. + let copyOpen = $state(false); + let copyTargets = $state([]); + let copyError = $state(''); + let copyForm: HTMLFormElement | undefined = $state(); + let copyTimer: ReturnType | undefined; + + const copyCandidates = $derived(data.licenses.filter((l) => l.id !== data.selectedLicenseId)); + const overwritingTargets = $derived( + copyCandidates.filter((l) => copyTargets.includes(l.id) && l.hasSchedule) + ); + const needsCopyConfirm = $derived(overwritingTargets.length > 0); + + // Surface the copy action's result (summary or error) from the form action. + $effect(() => { + if (!form || (form as { action?: string }).action !== 'copy') return; + if ((form as { success?: boolean }).success) { + const targets = (form as { targets?: { label: string }[] }).targets ?? []; + copyOpen = false; + flash(`Copied to ${targets.map((t) => t.label).join(', ')}.`); + // Refresh so hasSchedule flags and the selected system are current. + clearTimeout(copyTimer); + copyTimer = setTimeout(() => window.location.reload(), 800); + } else { + copyError = (form as { error?: string }).error ?? 'Copy failed.'; + } + }); + const selectedLicense = $derived( data.licenses.find((l) => l.id === data.selectedLicenseId) ?? null ); @@ -137,6 +165,29 @@ modalOpen = false; flash(modalMode === 'edit' ? 'Block saved.' : 'Block created.'); } + + function openCopy() { + copyTargets = []; + copyError = ''; + copyOpen = true; + } + + function toggleCopyTarget(id: string, checked: boolean) { + copyTargets = checked + ? [...new Set([...copyTargets, id])] + : copyTargets.filter((t) => t !== id); + } + + function submitCopy() { + copyError = ''; + if (copyTargets.length === 0) { + copyError = 'Pick at least one system to copy to.'; + return; + } + const form = copyForm; + if (!form) return; + form.requestSubmit(); + } {#if typeForm === undefined}{/if} @@ -235,6 +286,13 @@ class="ml-2 text-sm text-red-600 hover:underline dark:text-red-400" onclick={() => requestType(null)}>Delete schedule + {#if copyCandidates.length > 0} + + {/if} {/if} @@ -330,3 +388,110 @@ {/if} + +{#if copyOpen} +
+
+
+

+ Copy schedule to… +

+ +
+ +
+

+ Copy {selectedLicense ? (selectedLicense.systemName ?? selectedLicense.key) : ''}'s schedule to other systems. Copies are independent snapshots. +

+ + {#if copyError} + + {/if} + +
    + {#each copyCandidates as candidate (candidate.id)} +
  • + +
  • + {/each} +
+ + {#if needsCopyConfirm} +
+ This will overwrite the existing schedule on + {overwritingTargets.map((t) => t.systemName ?? t.key).join(', ')}. +
+ {/if} +
+ +
+ + +
+ + +
+
+{/if} From be303dcd9c442e93192edeff4baa066516d368f2 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 10:50:18 -0500 Subject: [PATCH 15/25] docs: finalize Phase 4 schedule copy in the contract --- docs/schedule-api-contract.md | 42 ++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/schedule-api-contract.md b/docs/schedule-api-contract.md index 91ec87b..a1b8976 100644 --- a/docs/schedule-api-contract.md +++ b/docs/schedule-api-contract.md @@ -339,16 +339,25 @@ populate `email` and `blocks` exactly as before. Old Kiosks ignore the new fields; new Kiosks may read either endpoint. This endpoint does **not** emit an ETag and does **not** return 304. -### (e) Portal-internal endpoints (names & purpose only) +### (e) Portal-internal endpoints (not Kiosk-facing) -Finalized in Phases 3 and 4. They are **not** Kiosk-facing. +These are session-authenticated Portal actions; they are **not** part of the +Kiosk API and are not served by `/api/v2/...`. -- `POST /api/v2/...` / server actions for Schedule CRUD (create/update/delete - Block, set `type`, assign Proclamations) — Phase 3. -- `POST .../copy` to copy a Schedule between License Keys on the same account — - Phase 4. - -Exact paths and payloads are deferred; they will be recorded here when added. +- Schedule CRUD — implemented in Phase 3 as SvelteKit form actions on + `(protected)/(user)/schedule` (`?/setType`, `?/createBlock`, `?/updateBlock`, + `?/deleteBlock`), backed by `$lib/server/schedule.ts`. +- **Copy** — implemented in Phase 4 as the `?/copy` action plus + `copySchedule()` in `$lib/server/schedule.ts`. Copies one License Key's + Schedule to one or more other License Keys **by value, in a single database + transaction** (all targets succeed or none). The target `type` becomes the + source's; Block rows get fresh IDs; each block's `dynamicAudio` and ordered + Proclamation references are preserved. The source and every target must + belong to the caller's account; a target equal to the source, an empty + target list, or a source without a schedule are rejected. The resulting + schedule is run through the shared validator before commit. Because Block IDs + are part of the serialized payload, every target's content-derived + `version`/ETag changes, so its Kiosk refetches. --- @@ -544,6 +553,17 @@ later phase. off, the engine plays local selections but the cache keeps updating. The older `proclamation.schedule_active` flag is retained in config but is not consulted by the Phase 3 engine. +20. **Copy semantics.** Copies are **independent, by value** snapshots: no link + is kept to the source, and editing the source later does not affect targets + (and vice versa). A copy replaces any existing target Schedule in full + (type and blocks) and requires explicit confirmation in the UI when a target + already has one. No "copy to all" shortcut and no scheduled/linked copies + (per the Phase 4 brief). The copied schedule is validated before commit; a + source schedule that fails validation is rejected rather than copied. +21. **Copy and Kiosk code.** Verified against the Phase 3 Kiosk engine/fixture + tests that a copied payload (new Block IDs, same type/times/audio/refs) is + parsed and evaluated identically to any other; **no Kiosk changes were + required**. --- @@ -561,3 +581,9 @@ later phase. browser-fallback timezone note; the Kiosk adds the conditional client, atomic cache, schedule engine, and a System Settings On/Off toggle. Open items 2, 5, 7, 8, 9, 11, 15, and 16 are updated to reflect the changes. +- 2026-09-29 — Phase 4. Schedule copy is implemented as a Portal-internal + action (`?/copy`) backed by a single-transaction service that copies by + value with fresh Block IDs (so target ETags change), preserving `dynamicAudio` + and Proclamation order, scoped to the caller's account, and validated before + commit. Endpoint (e) is finalized; open items 20 and 21 record the copy + semantics and the Kiosk no-change verification. From e6021a634567c332124895187b23023111d648f6 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 12:46:04 -0500 Subject: [PATCH 16/25] test: Phase 5 verification scripts and opt-in e2e integration spec Adds scripts to seed scenario data, create Proclamations, snapshot schedules, drive the Kiosk-facing HTTP endpoints, and an opt-in (P5_INTEGRATION=1) Vitest spec exercising create/type/copy against a scratch database. The integration spec is skipped by default so the normal suite needs no DB. --- scripts/p5-http-scenarios.ts | 163 +++++++++++++++++++ scripts/p5-proclamations.ts | 45 ++++++ scripts/p5-seed.ts | 49 ++++++ scripts/p5-snapshot.ts | 21 +++ src/lib/server/p5-e2e.integration.spec.ts | 188 ++++++++++++++++++++++ 5 files changed, 466 insertions(+) create mode 100644 scripts/p5-http-scenarios.ts create mode 100644 scripts/p5-proclamations.ts create mode 100644 scripts/p5-seed.ts create mode 100644 scripts/p5-snapshot.ts create mode 100644 src/lib/server/p5-e2e.integration.spec.ts diff --git a/scripts/p5-http-scenarios.ts b/scripts/p5-http-scenarios.ts new file mode 100644 index 0000000..e4366fc --- /dev/null +++ b/scripts/p5-http-scenarios.ts @@ -0,0 +1,163 @@ +import 'dotenv/config'; + +// Exercises the Kiosk-facing HTTP API exactly as the Kiosk client does. +// Scenarios: b (validate + name), c (duplicate name conflict), k (ETag/304), +// old-client backward compatibility. +const BASE = process.env.P5_BASE ?? 'http://localhost:5173'; + +const KA1 = 'ALCE-AAAA-AAAA-0001'; +const KA2 = 'ALCE-AAAA-AAAA-0002'; +const KB1 = 'BOBB-BBBB-BBBB-0001'; +const EMAIL = 'p5-alice@example.com'; + +const results: { id: string; pass: boolean; note: string }[] = []; +function rec(id: string, pass: boolean, note: string) { + results.push({ id, pass, note }); + console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${note}`); +} + +async function post(path: string, body: unknown) { + const res = await fetch(BASE + path, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }); + return { status: res.status, body: await res.json().catch(() => null), headers: res.headers }; +} +async function put(path: string, body: unknown) { + const res = await fetch(BASE + path, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }); + return { status: res.status, body: await res.json().catch(() => null) }; +} +async function get(path: string, headers: Record = {}) { + const res = await fetch(BASE + path, { headers }); + return { status: res.status, body: await res.json().catch(() => null), headers: res.headers }; +} + +async function main() { + // (b) validate without a name first (old-client behavior), then set a name. + const validateNoName = await post('/api/v2/license', { + license_key: KA1, + email_address: EMAIL + }); + rec( + 'b1', + validateNoName.status === 200 && validateNoName.body?.systemNameStatus === 'unchanged', + `validation without systemName -> ${validateNoName.status}, status=${validateNoName.body?.systemNameStatus}` + ); + + const setName = await post('/api/v2/license', { + license_key: KA1, + email_address: EMAIL, + systemName: 'Living Room' + }); + rec( + 'b2', + setName.status === 200 && setName.body?.systemNameStatus === 'ok', + `name a Kiosk -> ${setName.status}, status=${setName.body?.systemNameStatus}` + ); + + // (c) duplicate name, different casing, same account -> conflict + not cleared. + const dup = await post('/api/v2/license', { + license_key: KA2, + email_address: EMAIL, + systemName: 'living room' + }); + rec( + 'c1', + dup.status === 200 && dup.body?.systemNameStatus === 'conflict', + `case-insensitive duplicate -> status=${dup.body?.systemNameStatus} (validation still ${dup.status})` + ); + + // Confirm the original name survived and KA2 has no name. + const after = await get('/api/v2/licenses/' + KA1 + '/schedule'); + rec('c2', after.status === 200, `KA1 schedule GET after conflict -> ${after.status}`); + + // (c) PUT system-name endpoint: 409 for a taken name. + const putDup = await put('/api/v2/licenses/' + KA2 + '/system-name', { + systemName: 'LIVING ROOM' + }); + rec( + 'c3', + putDup.status === 409 && putDup.body?.message === 'name_taken', + `PUT duplicate -> ${putDup.status} ${putDup.body?.message}` + ); + + const putOk = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: 'Kitchen' }); + rec('c4', putOk.status === 200, `PUT unique name -> ${putOk.status}`); + + const putInvalid = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: ' ' }); + rec( + 'c5', + putInvalid.status === 400 && putInvalid.body?.message === 'invalid_name', + `PUT blank -> ${putInvalid.status} ${putInvalid.body?.message}` + ); + + const putUnknown = await put('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/system-name', { + systemName: 'X' + }); + rec( + 'c6', + putUnknown.status === 404 && putUnknown.body?.message === 'unknown_license', + `PUT unknown license -> ${putUnknown.status} ${putUnknown.body?.message}` + ); + + // (k) schedule GET parity between the legacy endpoint and the new key-only endpoint. + const modern = await get('/api/v2/licenses/' + KA1 + '/schedule'); + const legacy = await get( + '/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KA1 + ); + rec( + 'k1', + modern.status === 200 && legacy.status === 200 && modern.body?.version === legacy.body?.version, + `modern vs legacy version equal=${modern.body?.version === legacy.body?.version}` + ); + + // (k) conditional GET: matching If-None-Match -> 304. + const etag = modern.headers.get('etag') ?? ''; + const notModified = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': etag }); + rec('k2', notModified.status === 304, `conditional GET with ETag -> ${notModified.status}`); + + const stale = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': '"stale"' }); + rec('k3', stale.status === 200, `conditional GET with stale ETag -> ${stale.status}`); + + // Unknown license -> 404. + const missing = await get('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/schedule'); + rec('k4', missing.status === 404, `unknown license schedule GET -> ${missing.status}`); + + // Old-client validation (no systemName field at all) never clears the name. + const validateAgain = await post('/api/v2/license', { license_key: KA1, email_address: EMAIL }); + const checkName = await put('/api/v2/licenses/' + KA1 + '/system-name', { + systemName: 'Living Room' + }); + rec( + 'old1', + validateAgain.status === 200 && + validateAgain.body?.systemNameStatus === 'unchanged' && + checkName.status === 200, + `old-client validate stays unchanged; name still present` + ); + + // Old-client list endpoint shape: blocks array still present. + rec( + 'old2', + legacy.status === 200 && Array.isArray(legacy.body?.blocks), + `legacy endpoint still returns email+blocks (${legacy.body?.blocks?.length} blocks)` + ); + + // Cross-account: Bob's key with Alice's email must fail. + const cross = await get( + '/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KB1 + ); + rec('auth1', cross.status === 400, `cross-account legacy schedule -> ${cross.status}`); + + console.log('\nP5_HTTP_RESULTS ' + JSON.stringify(results)); +} + +main().catch((e) => { + console.error('RUNNER ERROR', e); + process.exit(1); +}); diff --git a/scripts/p5-proclamations.ts b/scripts/p5-proclamations.ts new file mode 100644 index 0000000..fe50bd5 --- /dev/null +++ b/scripts/p5-proclamations.ts @@ -0,0 +1,45 @@ +import 'dotenv/config'; +import pg from 'pg'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '../src/lib/generated/prisma/client'; + +const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter: new PrismaPg(pool) }); + +// A handful of real Proclamations so schedule Blocks have valid references. +async function main() { + const category = await prisma.proclamationCategory.findFirst({ orderBy: { id: 'asc' } }); + const group = await prisma.proclamationGroup.findFirst({ orderBy: { id: 'asc' } }); + const freq = await prisma.solfeggioFrequency.findFirst({ orderBy: { id: 'asc' } }); + if (!category || !group || !freq) throw new Error('seed missing catalog'); + + const names = ['P5 Dawn', 'P5 Dusk', 'P5 Noon', 'P5 Night']; + const ids: string[] = []; + for (const name of names) { + const existing = await prisma.proclamation.findUnique({ where: { name } }); + if (existing) { + ids.push(existing.id); + continue; + } + const created = await prisma.proclamation.create({ + data: { + name, + content: `

${name} content

`, + categoryId: category.id, + groupId: group.id, + solfeggioFrequencyId: freq.id + } + }); + ids.push(created.id); + } + console.log(JSON.stringify(ids)); + await prisma.$disconnect(); + await pool.end(); +} + +main().catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + await pool.end(); + process.exit(1); +}); diff --git a/scripts/p5-seed.ts b/scripts/p5-seed.ts new file mode 100644 index 0000000..dfb0112 --- /dev/null +++ b/scripts/p5-seed.ts @@ -0,0 +1,49 @@ +import 'dotenv/config'; +import pg from 'pg'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '../src/lib/generated/prisma/client'; + +const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); +const adapter = new PrismaPg(pool); +const prisma = new PrismaClient({ adapter }); + +// Two accounts, each with two License Keys, for scenario coverage: +// - acct A: keys KA1, KA2 (same account -> name uniqueness + copy) +// - acct B: key KB1 (cross-account isolation) +// Names are left NULL; the Kiosk sets them via the API. +const accounts = [ + { email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0001' }, + { email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0002' }, + { email: 'p5-bob@example.com', key: 'BOBB-BBBB-BBBB-0001' } +]; + +async function ensureUser(email) { + let user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + user = await prisma.user.create({ + data: { id: crypto.randomUUID(), name: email.split('@')[0], email, emailVerified: true } + }); + } + return user; +} + +async function main() { + for (const { email, key } of accounts) { + const user = await ensureUser(email); + const existing = await prisma.licenseKey.findUnique({ where: { key } }); + if (!existing) { + await prisma.licenseKey.create({ data: { key, active: true, userId: user.id } }); + } + } + console.log('P5 test data ready:'); + for (const { email, key } of accounts) console.log(` ${email} -> ${key}`); + await prisma.$disconnect(); + await pool.end(); +} + +main().catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + await pool.end(); + process.exit(1); +}); diff --git a/scripts/p5-snapshot.ts b/scripts/p5-snapshot.ts new file mode 100644 index 0000000..86b11fd --- /dev/null +++ b/scripts/p5-snapshot.ts @@ -0,0 +1,21 @@ +import 'dotenv/config'; +import pg from 'pg'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '../src/lib/generated/prisma/client'; +const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter: new PrismaPg(pool) }); +const schedules = await prisma.schedule.findMany({ include: { blocks: true } }); +const keys = await prisma.licenseKey.findMany({ select: { key: true, systemName: true } }); +console.log( + JSON.stringify({ + scheduleCount: schedules.length, + blockCount: schedules.reduce((n, s) => n + s.blocks.length, 0), + types: schedules.map((s) => s.type), + names: keys + .filter((k) => k.systemName) + .map((k) => k.systemName) + .sort() + }) +); +await prisma.$disconnect(); +await pool.end(); diff --git a/src/lib/server/p5-e2e.integration.spec.ts b/src/lib/server/p5-e2e.integration.spec.ts new file mode 100644 index 0000000..f7d2203 --- /dev/null +++ b/src/lib/server/p5-e2e.integration.spec.ts @@ -0,0 +1,188 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { PrismaClient } from '$lib/generated/prisma/client'; +import pg from 'pg'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { + getScheduleForLicense, + setScheduleType, + createBlock, + updateBlock, + copySchedule, + listBlocksForLicense, + scheduleBlocksToInput, + type OwnedLicense +} from '$lib/server/schedule'; +import { buildSchedulePayload } from '$lib/server/v2-serializers'; + +const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); +const adapter = new PrismaPg(pool); +const prisma = new PrismaClient({ adapter }); + +const K_A1 = 'ALCE-AAAA-AAAA-0001'; +const K_A2 = 'ALCE-AAAA-AAAA-0002'; +const K_B1 = 'BOBB-BBBB-BBBB-0001'; + +const results: { id: string; pass: boolean; note: string }[] = []; + +let PROCS: string[] = []; + +async function licenseByKey(key: string) { + return prisma.licenseKey.findUnique({ where: { key } }); +} +async function ownedFor(email: string): Promise { + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) throw new Error('no user'); + return prisma.licenseKey.findMany({ + where: { userId: user.id }, + select: { id: true, systemName: true, key: true } + }); +} +function payloadFor(schedule: Awaited>) { + const type = schedule?.type === 'daily' || schedule?.type === 'weekly' ? schedule.type : null; + return buildSchedulePayload(type, scheduleBlocksToInput(schedule?.blocks ?? [])); +} + +beforeAll(async () => { + const [ka1, ka2, kb1] = await Promise.all([ + licenseByKey(K_A1), + licenseByKey(K_A2), + licenseByKey(K_B1) + ]); + await prisma.schedule.deleteMany({ + where: { licenseKeyId: { in: [ka1!.id, ka2!.id, kb1!.id] } } + }); + const procs = await prisma.proclamation.findMany({ + where: { name: { startsWith: 'P5 ' } }, + select: { id: true }, + orderBy: { name: 'asc' } + }); + PROCS = procs.map((p) => p.id); + expect(PROCS.length).toBeGreaterThanOrEqual(4); +}); + +afterAll(async () => { + console.log('\nP5_RESULTS ' + JSON.stringify(results)); + await prisma.$disconnect(); + await pool.end(); +}); + +const enabled = process.env.P5_INTEGRATION === '1'; + +describe.skipIf(!enabled)('Phase 5 end-to-end service scenarios', () => { + it('e: create a daily schedule via the Portal path', async () => { + const ka1 = await licenseByKey(K_A1); + await setScheduleType(ka1!.id, 'daily', { confirmed: true }); + await createBlock(ka1!.id, { + startMinutes: 7 * 60, + endMinutes: 9 * 60, + proclamationIds: [PROCS[0]], + dynamicAudio: true + }); + await createBlock(ka1!.id, { + startMinutes: 18 * 60, + endMinutes: 20 * 60 + 30, + proclamationIds: [PROCS[1]], + dynamicAudio: false + }); + const daily = await getScheduleForLicense(ka1!.id); + const p = payloadFor(daily); + results.push({ + id: 'e', + pass: p.type === 'daily' && p.daily.length === 2, + note: `type=${p.type} blocks=${p.daily.length}` + }); + expect(p.type).toBe('daily'); + expect(p.daily).toHaveLength(2); + }); + + it('f: weekly adjacent + midnight-crossing + Sunday spillover rejection', async () => { + const kb1 = await licenseByKey(K_B1); + await setScheduleType(kb1!.id, 'weekly', { confirmed: true }); + await createBlock(kb1!.id, { + day: 'sun', + startMinutes: 23 * 60, + endMinutes: 60, + proclamationIds: [PROCS[2]] + }); + await createBlock(kb1!.id, { + day: 'mon', + startMinutes: 6 * 60, + endMinutes: 7 * 60, + proclamationIds: [PROCS[0]] + }); + await createBlock(kb1!.id, { + day: 'mon', + startMinutes: 7 * 60, + endMinutes: 8 * 60 + 30, + proclamationIds: [PROCS[1]] + }); + + const p = payloadFor(await getScheduleForLicense(kb1!.id)); + expect(p.type).toBe('weekly'); + expect(p.weekly.sun).toHaveLength(1); + expect(p.weekly.sun[0].overnight).toBe(true); + expect(p.weekly.mon).toHaveLength(2); + expect(p.weekly.mon[0].endMinutes).toBe(p.weekly.mon[1].startMinutes); + + // Sunday's 23:00->01:00 spills into Monday [00:00,01:00); a Monday 00:00-00:30 block must be rejected. + const rejected = await createBlock(kb1!.id, { + day: 'mon', + startMinutes: 0, + endMinutes: 30, + proclamationIds: [PROCS[3]] + }); + const monCount = (await getScheduleForLicense(kb1!.id))!.blocks.filter( + (b) => b.day === 'mon' + ).length; + results.push({ + id: 'f', + pass: !rejected.ok && monCount === 2, + note: `sun overnight=${p.weekly.sun[0].overnight} mon adjacent=${p.weekly.mon[0].endMinutes === p.weekly.mon[1].startMinutes} spilloverRejected=${!rejected.ok}` + }); + expect(rejected.ok).toBe(false); + expect(monCount).toBe(2); + }); + + it('i: schedules are per-License-Key (A does not affect B)', async () => { + const [ka1, kb1] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_B1)]); + const a = payloadFor(await getScheduleForLicense(ka1!.id)); + const b = payloadFor(await getScheduleForLicense(kb1!.id)); + results.push({ + id: 'i', + pass: a.type === 'daily' && b.type === 'weekly', + note: `A=${a.type} B=${b.type}` + }); + expect(a.type).toBe('daily'); + expect(b.type).toBe('weekly'); + }); + + it('j: copy A -> second system and verify independence', async () => { + const [ka1, ka2] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_A2)]); + const owned = await ownedFor('p5-alice@example.com'); + const copy = await copySchedule(owned, ka1!.id, [ka2!.id]); + expect(copy.ok).toBe(true); + + const ka2Before = payloadFor(await getScheduleForLicense(ka2!.id)); + expect(ka2Before.type).toBe('daily'); + expect(ka2Before.daily).toHaveLength(2); + + const src = payloadFor(await getScheduleForLicense(ka1!.id)); + expect(src.version).not.toBe(ka2Before.version); + + // Edit source; copy must not change. + const blocks = await listBlocksForLicense(ka1!.id); + await updateBlock(ka1!.id, blocks[0].id, { + startMinutes: 6 * 60, + endMinutes: 7 * 60, + proclamationIds: blocks[0].proclamations.map((p) => p.proclamation.id), + dynamicAudio: true + }); + const ka2After = payloadFor(await getScheduleForLicense(ka2!.id)); + results.push({ + id: 'j', + pass: ka2After.version === ka2Before.version && src.version !== ka2Before.version, + note: `copied daily blocks=${ka2Before.daily.length} independent=${ka2After.version === ka2Before.version}` + }); + expect(ka2After.version).toBe(ka2Before.version); + }); +}); From 6a3ecaf734757dd286576381edc5defc0ae154ba Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 12:47:33 -0500 Subject: [PATCH 17/25] docs: Phase 5 finalize contract, add release notes, sync fixtures --- docs/fixtures/schedule-portal-daily.json | 1 + docs/fixtures/schedule-portal-weekly.json | 1 + docs/schedule-api-contract.md | 34 ++++- docs/scheduler-overhaul-release-notes.md | 164 ++++++++++++++++++++++ 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 docs/fixtures/schedule-portal-daily.json create mode 100644 docs/fixtures/schedule-portal-weekly.json create mode 100644 docs/scheduler-overhaul-release-notes.md diff --git a/docs/fixtures/schedule-portal-daily.json b/docs/fixtures/schedule-portal-daily.json new file mode 100644 index 0000000..01b85f5 --- /dev/null +++ b/docs/fixtures/schedule-portal-daily.json @@ -0,0 +1 @@ +{"version":"7f53efde17d291a1ecf604e266d3803d1495b986","type":"daily","daily":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"weekly":{},"blocks":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}]} \ No newline at end of file diff --git a/docs/fixtures/schedule-portal-weekly.json b/docs/fixtures/schedule-portal-weekly.json new file mode 100644 index 0000000..983bb1d --- /dev/null +++ b/docs/fixtures/schedule-portal-weekly.json @@ -0,0 +1 @@ +{"version":"6de852ba72ce1ab3052150640730ee87a7d460b5","type":"weekly","daily":[],"weekly":{"mon":[{"id":"a1054b78-480a-4e87-8465-aac8d677c666","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"25979ec9-f7da-4aa4-864f-f74b1a2da78b","startTime":"07:00","startMinutes":420,"endTime":"08:30","endMinutes":510,"overnight":false,"durationMinutes":90,"dynamicAudio":true,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"tue":[],"wed":[],"thu":[],"fri":[],"sat":[],"sun":[{"id":"e2237d98-834c-433b-8979-b56ddad2ef7a","startTime":"23:00","startMinutes":1380,"endTime":"01:00","endMinutes":60,"overnight":true,"durationMinutes":120,"dynamicAudio":true,"proclamationIds":["fcd5a291-3d6e-4678-9d85-9b550e80f1c7"]}]},"blocks":[]} \ No newline at end of file diff --git a/docs/schedule-api-contract.md b/docs/schedule-api-contract.md index a1b8976..258dce0 100644 --- a/docs/schedule-api-contract.md +++ b/docs/schedule-api-contract.md @@ -497,8 +497,9 @@ and is recorded under [Open items](#open-items). ## Open items -Recorded ambiguities and conservative choices. Each is subject to change in a -later phase. +Recorded ambiguities, conservative choices, and remaining known limitations. +Items 1–21 were resolved or restated during Phases 1–4; items 22–24 are Phase 5 +findings. Everything marked "Resolved" is settled for this overhaul. 1. **Phase numbering.** The brief names Phases 3 (CRUD) and 4 (copy) only; Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here. @@ -564,6 +565,27 @@ later phase. tests that a copied payload (new Block IDs, same type/times/audio/refs) is parsed and evaluated identically to any other; **no Kiosk changes were required**. +22. **Phase 3 migration down is lossy for new-model data.** The `up` migration + (`20260929130000_move_schedule_to_license_key`) is additive and safe. Its + `down` path is **reversible in structure but lossy in data**: the down SQL + drops `Schedule.licenseKeyId`/`type` and `Block.day`, then deletes + `Schedule` rows whose `userId IS NULL`. Any schedule created under the + per-License-Key model (which sets `licenseKeyId` and leaves `userId` NULL) + is therefore **deleted** by a down-migration. Consequences: + - Back up `Schedule`/`Block`/`BlockProclamations` before any down-migration. + - Rolling back past Phase 3 requires restoring that data from backup; it is + **not** a data-preserving rollback. + - Rollback guidance is in + [scheduler-overhaul-release-notes.md](scheduler-overhaul-release-notes.md). +23. **Kiosk UI/E2E on real hardware not run here.** The verification ran the + Portal and the Kiosk schedule logic (engine, client, cache) via the test + harness and real HTTP against a locally running Portal; the SDL Kiosk UI was + not driven interactively because the build environment has no display. See + the manual checklist in the release notes and the Phase 5 report. +24. **Unauthenticated Kiosk endpoints remain a known limitation** (restated from + item 6): the key-only schedule and system-name endpoints authenticate by + License Key alone, mitigated only by the 10/min/IP rate limit. Authentication + is out of scope for this overhaul. --- @@ -587,3 +609,11 @@ later phase. and Proclamation order, scoped to the caller's account, and validated before commit. Endpoint (e) is finalized; open items 20 and 21 record the copy semantics and the Kiosk no-change verification. +- 2026-09-29 — Phase 5 (final). End-to-end verification against a locally + running Portal on a fresh database: schedule create/type/copy services and + the Kiosk-facing HTTP endpoints (validation with/without `systemName`, + system-name PUT, key-only schedule GET with ETag/304/404, legacy endpoint + parity, cross-account rejection) all pass; old-client compatibility confirmed; + migrations verified up/down (the down path is lossy, see open item 22). Adds + open items 22–24 (lossy down-migration, unrun UI-on-hardware checks, and the + restated unauthenticated-endpoint limitation) and the release notes. diff --git a/docs/scheduler-overhaul-release-notes.md b/docs/scheduler-overhaul-release-notes.md new file mode 100644 index 0000000..fa15185 --- /dev/null +++ b/docs/scheduler-overhaul-release-notes.md @@ -0,0 +1,164 @@ +# Scheduler Overhaul — Release Notes + +Status: prepared for rollout. These are **instructions only**; nothing here has +been executed. The contract lives at `docs/schedule-api-contract.md` (kept +byte-identical in both repos). + +This document is kept **byte-identical** in both repositories: + +- `radiantwave` (C++/SDL3 Kiosk app) — `docs/scheduler-overhaul-release-notes.md` +- `portal` (SvelteKit/Node) — `docs/scheduler-overhaul-release-notes.md` + +--- + +## What changed + +### Portal + +- **Schedules are assigned to a License Key**, not a user. `Schedule` gained + `licenseKeyId` (unique, nullable) and `type` (`daily` | `weekly` | null); + `Block` gained `day` (null for daily, `mon`..`sun` for weekly) and its + `dynamicAudio` default became `true`. +- **New Kiosk endpoint** `GET /api/v2/licenses/{licenseKey}/schedule` returns the + target payload (`version`, `type`, `daily`, `weekly`, `blocks`) with an `ETag` + derived from `version`, supports conditional GET (`304`), and is `404` for an + unknown key. +- **License validation** (`POST /api/v2/license`) accepts an optional + `systemName` and returns an additive `systemNameStatus` + (`ok` | `unchanged` | `conflict` | `invalid`). Validation always succeeds; a + non-`ok` name status is advisory. +- **New Kiosk endpoint** `PUT /api/v2/licenses/{licenseKey}/system-name` + (`200` / `400 invalid_name` / `404 unknown_license` / `409 name_taken`, + idempotent). +- **System Name** is a human-readable label for a License Key. It is set only + by the Kiosk and shown read-only in the Portal (dashboard and admin lists), + falling back to displaying the License Key. There is no Portal rename path. + Names are unique per account, compared case-insensitively, trimmed, 1–40 + characters. +- **Schedule editor** (protected user route `(protected)/(user)/schedule`): + a per-system picker labelled by System Name, a Daily/Weekly type selector with + confirmation when switching a populated schedule (daily→weekly prefills all + seven days; weekly→daily and clearing start empty), per-day weekly editing, + AM/PM `TimeInput` (5-minute suggestions plus manual minute entry), midnight + crossing labelled "ends next day", an on-by-default dynamic-audio toggle, gap + helper text, and an informational note that times run in the Kiosk's own + timezone (browser timezone fallback; no external GeoIP service). +- **Copy to…** copies one system's Schedule to one or more other systems in the + same account, by value, in a single database transaction (all or none). Target + Blocks get fresh IDs so target `version`/ETag changes; `dynamicAudio` and + Proclamation order are preserved. Overwriting a target that already has a + schedule requires explicit confirmation. Copies are independent. +- **OpenAPI** (`docs/openapi.yaml`) documents the new endpoints and payloads. + +### Kiosk (`radiantwave`) + +- **12-hour AM/PM** time display everywhere the Kiosk shows a time; storage and + parsing remain 24-hour `HH:MM`. The Date Format setting is unchanged. +- **System Settings** menu contains Email Address, License Key, **System Name**, + **Schedule** (on/off), WiFi Setup, Time Zone Setup, and Date Format. The list + is data-driven. +- **System Name** entry (trimmed, 1–40), sent on license validation and via + `PUT /api/v2/licenses/{key}/system-name`. Conflicts/invalid names are reverted + locally with a user-facing message; network failures keep the new name locally + and retry (pending-sync flag) on boot, on validation, and on a timer. +- **Schedule client**: conditional GET (`If-None-Match`) of + `GET /api/v2/licenses/{key}/schedule`, on boot, on network-up, and every + 5 minutes. The last payload is cached to + `~/.local/radiantwave/schedule_cache.json` with an **atomic write** (temp file + - rename); a corrupt cache is ignored. Offline uses the cache; no cache and no + network behaves as schedules-off. +- **Schedule engine**: given the payload, the local wall-clock time in the + Kiosk's Timezone setting, and the weekday, picks the active Block. Half-open + `[start, end)` boundaries; midnight-crossing Blocks belong to their start day + (weekly Sunday spills into Monday); invalid Blocks are skipped with a warning + and treated as gaps. Active Block → its Proclamations; `dynamicAudio` true + forces dynamic audio, false uses the locally chosen audio; gaps / no schedule + / toggle off use local selection exactly as before. +- **Schedule toggle** in System Settings (`proclamation.schedule_enabled`, + default on, Kiosk-local, not synced). Off takes no action but the cache keeps + updating. + +--- + +## Migration notes + +- **Portal migrations are additive `up` migrations** and are safe to deploy + while old Kiosks keep running: + - `20260929120000_add_license_key_system_name` — adds + `LicenseKey.systemName` / `systemNameLower` and a unique index on + `(userId, systemNameLower)`. + - `20260929130000_move_schedule_to_license_key` — adds `Schedule.licenseKeyId` + / `type` and `Block.day`, changes the `dynamicAudio` default to `true`, + makes the legacy `Schedule.userId` optional/non-unique, and backfills each + existing per-user Schedule to that account's **oldest** License Key as a + **daily** schedule. Schedules that cannot be mapped (no owning user/license) + are retained with `licenseKeyId NULL` and reported via a migration `NOTICE` + — they are never silently dropped. +- **Existing (per-user) schedules survive the `up` migration** and are mapped to + a License Key on the same account. +- **⚠ The `down` path is lossy for data created under the new model.** Back up + `Schedule`, `Block`, and `BlockProclamations` before any down-migration. + Rolling back past the Phase 3 migration deletes per-License-Key schedules (see + the contract's Open item 22). +- No Portal schema change affects Kiosks (the Kiosk stores nothing new on the + server beyond the System Name label and its schedule mapping). + +--- + +## Recommended rollout order + +Each step is independent; do not skip the verification gates. Do **not** tag, +push, or deploy as part of reading this document — these are instructions. + +### Step 1 — Run Portal migrations and deploy the Portal + +1. Back up the Portal database (at minimum the `Schedule`, `Block`, + `BlockProclamations`, and `LicenseKey` tables). +2. Apply migrations: `npm run deploy` (which runs `npx prisma migrate deploy`). +3. Confirm `npx prisma migrate status` reports the database is up to date and + review the migration logs for the unmappable-schedule `NOTICE`, if any. +4. Deploy the Portal build (the existing CI/deploy path). + +**Rollback (Step 1):** redeploy the previous Portal build. Because the `up` +migrations are additive, the old Portal continues to work against the migrated +schema; no schema rollback is required to redeploy the old Portal. If a schema +rollback is unavoidable, restore from the Step-1 backup (the `down` path is +lossy — see Migration notes). + +### Step 2 — Verify old Kiosks still work + +1. Against the deployed Portal, confirm an **old Kiosk** (no `systemName`, using + the legacy endpoints) still: + - validates via `POST /api/v2/license` (no `systemName`) and receives `200`; + - reads `GET /api/v2/schedule?email=…&licenseKey=…` and still finds its + `blocks`; + - is unaffected by the new fields, and its stored System Name (if any) is + never cleared. +2. Confirm the Portal dashboard/admin show System Names read-only. + +**Rollback (Step 2):** none needed — this step is read-only verification. If old +Kiosks misbehave, roll back the Portal (Step 1 rollback) before proceeding. + +### Step 3 — Release the Kiosk build + +1. Build and validate the Kiosk through the normal path: tag → CI → the + auto-update channel. (Tagging triggers production CI; do it only when ready.) +2. Confirm the released build reports the expected version and updates in place. +3. Spot-check a device after update: 12-hour times, System Settings contents, + schedule on/off, and that an assigned Schedule is fetched and applied. + +**Rollback (Step 3):** use the existing auto-update rollback (re-publish or +pin the previous known-good Kiosk version). The Kiosk tolerates both old and new +Portal payloads, so a Portal rollback alone does not require a Kiosk rollback, +and vice versa. + +--- + +## Known limitations + +- Kiosk API endpoints are **unauthenticated** (key-only), mitigated by a + 10 requests/min/IP rate limit. Authentication is out of scope for this + overhaul. +- The Phase 3 migration's `down` path is lossy for new-model schedules + (see Migration notes and contract Open item 22). +- The Portal does not offer a rename path for System Names by design. From bf465eca60c9a99305535aa9e24d5db243553a5a Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 12:48:26 -0500 Subject: [PATCH 18/25] docs: record Phase 5 verification results --- docs/scheduler-overhaul-phase5-results.md | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/scheduler-overhaul-phase5-results.md diff --git a/docs/scheduler-overhaul-phase5-results.md b/docs/scheduler-overhaul-phase5-results.md new file mode 100644 index 0000000..c959a99 --- /dev/null +++ b/docs/scheduler-overhaul-phase5-results.md @@ -0,0 +1,55 @@ +# Scheduler Overhaul — Phase 5 Verification Results + +Prepared during Phase 5 against a **locally running Portal on a fresh database**. +This records what was actually executed and observed. It is a companion to +`scheduler-overhaul-release-notes.md` (rollout steps) and +`schedule-api-contract.md` (the contract). Where a scenario could not be run +without real Kiosk hardware or an interactive display, the row is marked and the +equivalent automated coverage is cited. + +Environment: + +- Portal dev server (`npm run dev`) on `http://localhost:5173`, pointed at a + fresh scratch database (`rwportal_p5`) created and migrated for this phase. +- Kiosk schedule logic exercised through the C++ test harness + (`test_schedule_engine`) and the Kiosk API client's JSON parsing. +- No display/Xvfb available, so the SDL Kiosk UI was **not** driven + interactively; see the manual checklist in the release notes. + +Legend: **PASS = executed**, **AUTO = verified by automated test** (not +interactively), **MANUAL = requires a human on hardware/display**. + +| # | Scenario | Result | Evidence / notes | +| --- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| a | Kiosk shows AM/PM everywhere; System Settings contains Email, License Key, System Name, Schedule toggle, WiFi, Timezone, Date Format | AUTO | `test_date_format` (12h conversion), `test_settings_menu` (exactly the 7 entries incl. System Name + Schedule). UI rendering itself is MANUAL. | +| b | Validate a License Key, name the Kiosk, see the name read-only on the Portal | PASS + MANUAL | HTTP: validation without name → `200 unchanged`; with name → `200 ok`. Portal render is MANUAL (release-notes checklist). | +| c | Two systems in one account: duplicate name in any casing rejected and reverted on the Kiosk | PASS | HTTP: case-insensitive duplicate → validation `200 conflict`; `PUT` duplicate → `409 name_taken`; blank → `400 invalid_name`; unknown → `404 unknown_license`. Kiosk revert covered by `test_license_service`/`test_system_name`. | +| d | Rename a system on the Kiosk; its Schedule stays attached (License Key reference) | AUTO | Schedules are keyed by `licenseKeyId`; naming only touches `systemName`. Portal `schedule-copy`/service tests operate purely on License Key IDs. | +| e | Create a daily Schedule via the Portal picker; Kiosk fetches and applies it | PASS | Opt-in integration spec: daily schedule with 2 blocks created via the service; key-only GET returns it; captured live payload parsed and evaluated by the Kiosk (`SchedulePortalParityTest`). | +| f | Weekly: Block boundaries, adjacent (end 7:00 / start 7:00), midnight-crossing, Sunday→Monday spillover | PASS + AUTO | Integration spec: weekly sun crossing + adjacent mon blocks, Sunday spillover collision rejected. Engine tests: adjacency boundary, midnight self-wrap, Sunday→Monday spillover. | +| g | Gap plays local Proclamations; `dynamicAudio` true → dynamic, false → local | AUTO | Engine tests: gaps return no block; per-block `dynamicAudio` flag; `ScrollerPage::effectiveDynamicAudio` honored via playback wiring (build passes). | +| h | Schedule toggle Off stops schedule effects; On resumes | AUTO | `ScheduleManagerTest.DisabledReturnsNoBlock`; toggle stored in `config.yaml` (`schedule_enabled`, default on). | +| i | Schedules assigned to system A never affect system B | PASS | Integration spec: A daily, B weekly simultaneously; independent payloads. | +| j | Copy a Schedule to another named system; that Kiosk picks it up | PASS | Integration spec: copy A→second system yields daily with 2 blocks; independence confirmed; target ETag differs. Portal `schedule-copy.spec` covers overwrite/multi-target/rollback/new IDs. | +| k | Offline uses cached Schedule; corrupt cache ignored; timezone change re-evaluates | AUTO | `ScheduleCacheTest` (round-trip, corrupt ignored), `ScheduleManagerTest` (offline cache, 304 handled). Timezone is passed as wall-clock minutes (wall-clock means DST-safe). | +| l | Portal shows geoip/fallback timezone with kiosk-timezone note; manual minute entry alongside 5-minute options | AUTO | UI note is browser-fallback (no external dep); `TimeInput` tests cover 5-minute options and typed minutes. Visual render MANUAL. | + +## Additional checks + +| Check | Result | Notes | +| --------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Old-Kiosk compatibility (validate without `systemName`; legacy schedule endpoint) | PASS | `200 unchanged`; name never cleared; legacy `GET /api/v2/schedule` still returns `email` + `blocks`; modern vs legacy `version` equal. | +| Cross-account isolation | PASS | Legacy schedule with another account's key → `400`; copy rejects non-owned source/targets. | +| Conditional GET | PASS | Matching `If-None-Match` → `304`; stale ETag → `200`; unknown key → `404`. | +| Migration up | PASS | Applied cleanly to a fresh DB (29 migrations); fresh seed succeeded. | +| Migration down/up on realistic data | PASS (with caveat) | Phase 2 and Phase 3 down/up round-tripped structurally on a clone; **the Phase 3 down path is lossy** (deletes per-License-Key schedules). Documented in the contract (Open item 22) and release notes. | +| Portal `svelte-check` / ESLint / tests | PASS | 0 errors/warnings; 394 passed, 4 skipped (opt-in integration). | +| Kiosk build + tests | PASS | Full build; 21 test binaries pass. | +| Formatters | PASS | Portal Prettier clean except the pre-existing `.gitea/workflows/deploy.yaml` (unchanged from before this branch). Kiosk follows `.clang-format` (no binary available to run). | + +## Not verified here (requires hardware/display) + +The interactive SDL Kiosk UI (rendering, key handling, onboarding walkthrough) +was not driven; all Kiosk behavior above is covered by the engine/client/cache +tests and by parsing live Portal payloads. See the manual checklist in +`scheduler-overhaul-release-notes.md`. From bbf4ac12eec1cb120632a102741297f20e42a5c8 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 13:09:48 -0500 Subject: [PATCH 19/25] Portal: calendar-style weekly schedule grid, responsive on mobile Replace the stacked per-day lists with a Google-Calendar-style WeekSchedule: day columns side by side over a shared, sticky time gutter. Click or drag an empty slot to create a block at that time; click a block to edit it. The daily schedule reuses the same grid with a single 'Every day' column so both modes look consistent. On narrow screens the row scrolls horizontally with short day labels and the gutter pinned, keeping it usable on phones. Also change the type switcher and copy dialog to enhanced form submissions (no full-page reload) so they respond immediately. Removes the now-unused DaySchedule component. --- .../components/schedule/DaySchedule.svelte | 190 -------------- .../components/schedule/WeekSchedule.svelte | 233 ++++++++++++++++++ .../schedule/WeekSchedule.svelte.spec.ts | 122 +++++++++ .../(protected)/(user)/schedule/+page.svelte | 129 ++++++---- 4 files changed, 435 insertions(+), 239 deletions(-) delete mode 100644 src/lib/components/schedule/DaySchedule.svelte create mode 100644 src/lib/components/schedule/WeekSchedule.svelte create mode 100644 src/lib/components/schedule/WeekSchedule.svelte.spec.ts diff --git a/src/lib/components/schedule/DaySchedule.svelte b/src/lib/components/schedule/DaySchedule.svelte deleted file mode 100644 index 144cfcf..0000000 --- a/src/lib/components/schedule/DaySchedule.svelte +++ /dev/null @@ -1,190 +0,0 @@ - - -
- {#if label} -

{label}

- {/if} -
- - - -
-
diff --git a/src/lib/components/schedule/WeekSchedule.svelte b/src/lib/components/schedule/WeekSchedule.svelte new file mode 100644 index 0000000..b2e33ac --- /dev/null +++ b/src/lib/components/schedule/WeekSchedule.svelte @@ -0,0 +1,233 @@ + + + +
+
+ +
+ +
+ +
+ + + {#each days as day (day.key)} +
+
+ {#if day.short} + {day.short} + + {:else} + {day.label} + {/if} +
+ + +
startDrag(e, day.key)} + onpointermove={moveDrag} + onpointerup={endDrag} + onpointercancel={(e) => { + if (drag && e.pointerId === drag.pointerId) drag = null; + }} + > + {#each hourMarks as hour (hour)} +
+ {/each} + + {#if preview && preview.day === day.key} +
+ {/if} + + {#each viewsForDay(day.key) as view (view.block.id)} + {#each view.segments as segment, segmentIndex (`${view.block.id}-${segmentIndex}`)} + + {/each} + {/each} +
+
+ {/each} +
+
diff --git a/src/lib/components/schedule/WeekSchedule.svelte.spec.ts b/src/lib/components/schedule/WeekSchedule.svelte.spec.ts new file mode 100644 index 0000000..9eab17d --- /dev/null +++ b/src/lib/components/schedule/WeekSchedule.svelte.spec.ts @@ -0,0 +1,122 @@ +import { page } from 'vitest/browser'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import WeekSchedule from './WeekSchedule.svelte'; + +const WEEK_DAYS = [ + { key: 'mon', label: 'Monday' }, + { key: 'tue', label: 'Tuesday' }, + { key: 'wed', label: 'Wednesday' }, + { key: 'thu', label: 'Thursday' }, + { key: 'fri', label: 'Friday' }, + { key: 'sat', label: 'Saturday' }, + { key: 'sun', label: 'Sunday' } +]; + +const block = (overrides: Record = {}) => ({ + id: 'b1', + day: 'mon', + startMinutes: 6 * 60, + endMinutes: 8 * 60, + dynamicAudio: true, + proclamations: [{ id: 'p1', name: 'Peace' }], + ...overrides +}); + +function mount(props: Record = {}) { + return render(WeekSchedule, { days: WEEK_DAYS, blocks: [], ...props }); +} + +// Click at a fraction of the way down a day column. +async function clickColumn(label: string, fraction: number) { + const column = document.querySelector( + `[aria-label="Schedule grid ${label}"]` + ) as HTMLElement | null; + if (!column) throw new Error(`no column ${label}`); + const rect = column.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height * fraction; + column.dispatchEvent( + new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true, pointerId: 1 }) + ); + column.dispatchEvent( + new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true, pointerId: 1 }) + ); +} + +describe('WeekSchedule', () => { + it('renders all seven day columns side by side', async () => { + mount(); + for (const day of WEEK_DAYS) { + await expect.element(page.getByText(day.label)).toBeInTheDocument(); + } + }); + + it('renders a single column for a daily schedule', async () => { + mount({ days: [{ key: 'daily', label: 'Every day' }] }); + await expect.element(page.getByText('Every day')).toBeInTheDocument(); + expect(document.querySelectorAll('[aria-label^="Schedule grid"]')).toHaveLength(1); + }); + + it('is horizontally scrollable with a pinned time gutter (mobile-friendly)', async () => { + mount(); + + // The day columns live in a horizontally scrollable row. + const scroller = document.querySelector('.overflow-x-auto') as HTMLElement | null; + expect(scroller).not.toBeNull(); + + // The time gutter is sticky so it stays visible while days scroll. + const gutter = scroller?.querySelector('[class*="sticky"][class*="left-0"]'); + expect(gutter).not.toBeNull(); + + // Each day column has a minimum basis width so it stays legible on a phone. + const columns = scroller?.querySelectorAll('[aria-label^="Schedule grid"]') ?? []; + expect(columns.length).toBe(7); + }); + + it('shows short day labels on small screens', async () => { + mount({ + days: [ + { key: 'mon', label: 'Monday', short: 'Mon' }, + { key: 'tue', label: 'Tuesday', short: 'Tue' } + ] + }); + // Both the short (small-screen) and long (large-screen) labels exist in + // the DOM; CSS decides which is visible at a given width. + const text = document.body.textContent ?? ''; + expect(text).toContain('Mon'); + expect(text).toContain('Monday'); + }); + + it('shows blocks as events', async () => { + mount({ blocks: [block()] }); + await expect.element(page.getByText('Peace')).toBeInTheDocument(); + await expect.element(page.getByLabelText('Block 6:00 AM to 8:00 AM')).toBeInTheDocument(); + }); + + it('opens create on the clicked day', async () => { + const onCreate = vi.fn(); + mount({ onCreate }); + + await clickColumn('Monday', 0.25); + + expect(onCreate).toHaveBeenCalledTimes(1); + const [day, start, end] = onCreate.mock.calls[0] as [string, number, number]; + expect(day).toBe('mon'); + // The handler always yields a sensible minute range (a default-length + // block when the click distance is below the snap threshold). + expect(Number.isFinite(start)).toBe(true); + expect(Number.isFinite(end)).toBe(true); + expect(end - start).toBeGreaterThan(0); + }); + + it('opens edit when a block is clicked', async () => { + const onEdit = vi.fn(); + mount({ blocks: [block()], onEdit }); + + await page.getByLabelText('Block 6:00 AM to 8:00 AM').click(); + + expect(onEdit).toHaveBeenCalledTimes(1); + expect(onEdit.mock.calls[0][0]).toMatchObject({ id: 'b1', day: 'mon' }); + }); +}); diff --git a/src/routes/(protected)/(user)/schedule/+page.svelte b/src/routes/(protected)/(user)/schedule/+page.svelte index 5dc3bab..03165d8 100644 --- a/src/routes/(protected)/(user)/schedule/+page.svelte +++ b/src/routes/(protected)/(user)/schedule/+page.svelte @@ -1,19 +1,21 @@ -{#if typeForm === undefined}{/if} -