diff --git a/.fleet/design-evidence/weekly-log/after/journal-1440.png b/.fleet/design-evidence/weekly-log/after/journal-1440.png new file mode 100644 index 00000000..8bf20a2d Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/journal-1440.png differ diff --git a/.fleet/design-evidence/weekly-log/after/journal-390.png b/.fleet/design-evidence/weekly-log/after/journal-390.png new file mode 100644 index 00000000..30f59a9c Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/journal-390.png differ diff --git a/.fleet/design-evidence/weekly-log/after/journal-768.png b/.fleet/design-evidence/weekly-log/after/journal-768.png new file mode 100644 index 00000000..646c4213 Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/journal-768.png differ diff --git a/.fleet/design-evidence/weekly-log/after/onboarding-reveal-1440.png b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-1440.png new file mode 100644 index 00000000..64299508 Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-1440.png differ diff --git a/.fleet/design-evidence/weekly-log/after/onboarding-reveal-390.png b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-390.png new file mode 100644 index 00000000..17d4c754 Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-390.png differ diff --git a/.fleet/design-evidence/weekly-log/after/onboarding-reveal-768.png b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-768.png new file mode 100644 index 00000000..ac9938d7 Binary files /dev/null and b/.fleet/design-evidence/weekly-log/after/onboarding-reveal-768.png differ diff --git a/.fleet/design-evidence/weekly-log/before/README.md b/.fleet/design-evidence/weekly-log/before/README.md new file mode 100644 index 00000000..055c5bb9 --- /dev/null +++ b/.fleet/design-evidence/weekly-log/before/README.md @@ -0,0 +1,6 @@ +# Before state + +Captured in git, not pixels: the prior /journal surface was the AM/PM +PersonalPracticeSurface (see `src/components/personal-practice-surfaces.tsx` +at the base commit) and onboarding step 2 was a skippable quote card +("The rest is still unwritten") — no weeks-remaining number was ever shown. diff --git a/.fleet/design-review-archive/calling-dream-loop.json b/.fleet/design-review-archive/calling-dream-loop.json new file mode 100644 index 00000000..016f5689 --- /dev/null +++ b/.fleet/design-review-archive/calling-dream-loop.json @@ -0,0 +1,95 @@ +{ + "$schema": "fleet.design-review.v1", + "version": 1, + "project": "live", + "target": "Live calling dream loop", + "surfaceMode": "operate", + "mode": "preserve", + "register": "product", + "context": { + "product": "PRODUCT.md", + "design": "DESIGN.md" + }, + "direction": { + "references": [], + "probes": [], + "selected": "existing-design", + "approval": "delegated", + "before": ".fleet/design-evidence/open-world-life-atlas/after-1440.png", + "contract": { + "purpose": "Help a returning person move one dream from passive wanting toward a credible first action without shrinking the larger life around it.", + "purposeSource": "../site-health/apps/backend/config/projects.json", + "canonicalPurpose": "Live helps a person turn passive wanting into a life they can deliberately imagine, plan, and experience.", + "purposeAlignment": "match", + "driftNote": "", + "audience": "Returning adults who already have meaningful dreams in Live and need one trustworthy reason to act now.", + "job": "Bring the most recently active dream forward, explain one credible first door, and let another dream be called forward without adding pressure or a new planning model.", + "thesis": "Preserve the sunlit Life Atlas and add one personal waypoint before broad discovery: calling dream, first door, then the wider territories.", + "system": "Keep the existing paper, olive, field-color, serif-landmark, and compact sans-serif-control system. Use one asymmetric blue waypoint as the personal action region rather than adding a dashboard or new navigation layer.", + "signature": "The user's exact calling dream sits at a fork whose next route is visibly native, curated, or wider-world research.", + "risk": "A single focus can feel like another obligation, so calling a dream forward never demotes other in-progress dreams and can be changed from the same surface." + } + }, + "evidence": { + "screenshots": [ + { + "width": 390, + "path": ".fleet/design-evidence/calling-dream-loop/after-390.png" + }, + { + "width": 768, + "path": ".fleet/design-evidence/calling-dream-loop/after-768.png" + }, + { + "width": 1440, + "path": ".fleet/design-evidence/calling-dream-loop/after-1440.png" + } + ], + "projectCheck": { + "command": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", + "status": "pass" + }, + "critique": { + "score": 38, + "maximum": 40 + }, + "audit": { + "score": 17, + "maximum": 20 + }, + "unresolved": { + "p0": 0, + "p1": 0 + }, + "detector": { + "posture": "advisory", + "findings": [] + }, + "comprehension": { + "status": "not-applicable", + "reviewer": "", + "answers": { + "product": "", + "audience": "", + "value": "", + "mechanism": "", + "proof": "", + "nextAction": "" + }, + "purposeScore": { + "product": 0, + "audience": 0, + "value": 0, + "mechanism": 0, + "proof": 0, + "nextAction": 0, + "total": 0 + }, + "mismatches": [] + } + }, + "ownerFeedback": { + "decision": "delegated", + "note": "The owner asked to raise the product to at least 9 without super-drastic changes and delegated the focused coherence pass." + } +} diff --git a/.fleet/design-review.json b/.fleet/design-review.json index 016f5689..8c6f64eb 100644 --- a/.fleet/design-review.json +++ b/.fleet/design-review.json @@ -2,8 +2,8 @@ "$schema": "fleet.design-review.v1", "version": 1, "project": "live", - "target": "Live calling dream loop", - "surfaceMode": "operate", + "target": "Live onboarding weeks-reveal and weekly log", + "surfaceMode": "experience", "mode": "preserve", "register": "product", "context": { @@ -14,47 +14,70 @@ "references": [], "probes": [], "selected": "existing-design", - "approval": "delegated", - "before": ".fleet/design-evidence/open-world-life-atlas/after-1440.png", + "approval": "not-required", + "before": ".fleet/design-evidence/weekly-log/before/README.md", + "library": { + "strategy": "upstream-first", + "primary": "existing-project-system", + "sources": [], + "runtime": "existing", + "customReplacement": { + "used": false, + "authorization": "not-required", + "reason": "" + } + }, "contract": { - "purpose": "Help a returning person move one dream from passive wanting toward a credible first action without shrinking the larger life around it.", - "purposeSource": "../site-health/apps/backend/config/projects.json", - "canonicalPurpose": "Live helps a person turn passive wanting into a life they can deliberately imagine, plan, and experience.", + "purpose": "Live helps a person name the life they want to live, plan toward it, and keep a truthful weekly record of what they actually lived.", + "purposeSource": "Owner direction on 20 September 2026 and PRODUCT.md", + "canonicalPurpose": "Live turns passive wanting into a life the user deliberately imagines, plans, and records — onboarding reveals their own weeks of a ~4,000-week life, and the weekly log asks what they did with them.", "purposeAlignment": "match", - "driftNote": "", - "audience": "Returning adults who already have meaningful dreams in Live and need one trustworthy reason to act now.", - "job": "Bring the most recently active dream forward, explain one credible first door, and let another dream be called forward without adding pressure or a new planning model.", - "thesis": "Preserve the sunlit Life Atlas and add one personal waypoint before broad discovery: calling dream, first door, then the wider territories.", - "system": "Keep the existing paper, olive, field-color, serif-landmark, and compact sans-serif-control system. Use one asymmetric blue waypoint as the personal action region rather than adding a dashboard or new navigation layer.", - "signature": "The user's exact calling dream sits at a fork whose next route is visibly native, curated, or wider-world research.", - "risk": "A single focus can feel like another obligation, so calling a dream forward never demotes other in-progress dreams and can be changed from the same surface." + "driftNote": "The journal's AM/PM cadence was replaced by a weekly reckoning; earlier daily entries remain readable as archive. Onboarding compressed from seven steps to five around the weeks-remaining reveal.", + "audience": "A reflective adult who wants to be more intentional about an unlived life — first-time through a mortality frame, returning weekly.", + "job": "See your own finite weeks, choose what still calls, and record each week honestly — without streaks, scores, or shame.", + "thesis": "Preserve the established warm paper/serif Life Atlas language; make the mortality frame personal (the user's own filled grid) rather than a generic quote.", + "system": "Existing field/cream/purple pastels, serif landmarks, compact sans controls, rounded 1.5rem cards, product-color nav states (journal purple retained for the weekly log).", + "signature": "The user's own life grid rendered inside onboarding with their weeks-lived/weeks-remaining numbers, and a weekly log that pairs one honest question with a gentle 'still calling?' check on quiet dreams.", + "risk": "Removing the AM/PM ritual changes a familiar surface; the archive preserves old entries, and the weekly cadence must not read as a missed-day guilt loop." } }, "evidence": { "screenshots": [ { "width": 390, - "path": ".fleet/design-evidence/calling-dream-loop/after-390.png" + "path": ".fleet/design-evidence/weekly-log/after/journal-390.png" + }, + { + "width": 768, + "path": ".fleet/design-evidence/weekly-log/after/journal-768.png" + }, + { + "width": 1440, + "path": ".fleet/design-evidence/weekly-log/after/journal-1440.png" + }, + { + "width": 390, + "path": ".fleet/design-evidence/weekly-log/after/onboarding-reveal-390.png" }, { "width": 768, - "path": ".fleet/design-evidence/calling-dream-loop/after-768.png" + "path": ".fleet/design-evidence/weekly-log/after/onboarding-reveal-768.png" }, { "width": 1440, - "path": ".fleet/design-evidence/calling-dream-loop/after-1440.png" + "path": ".fleet/design-evidence/weekly-log/after/onboarding-reveal-1440.png" } ], "projectCheck": { - "command": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", + "command": "pnpm lint; pnpm typecheck; pnpm test; pnpm build; axe at 390, 768, and 1440 via e2e/daily.spec.ts a11y sweep", "status": "pass" }, "critique": { - "score": 38, + "score": 36, "maximum": 40 }, "audit": { - "score": 17, + "score": 18, "maximum": 20 }, "unresolved": { @@ -63,7 +86,13 @@ }, "detector": { "posture": "advisory", - "findings": [] + "findings": [ + "Axe reported zero serious violations across /daily, /journal, and /habits after muted helper text moved from #8a8175 to #625b50 (3.83:1 -> >4.5:1).", + "The weeks reveal reuses the shipped LifeGrid component inside the existing dark onboarding step; no new visual language was introduced.", + "The weekly log header uses the journal product purple (#c5abfa) so the nav active state and the surface stay one product color.", + "Stale-dream copy was kept to 'still calling?' with an optional call-forward action; no streak, score, or failure vocabulary ships.", + "The week-start toggle labels were shortened to single words after the 1440px capture showed cramped wrapping." + ] }, "comprehension": { "status": "not-applicable", @@ -89,7 +118,7 @@ } }, "ownerFeedback": { - "decision": "delegated", - "note": "The owner asked to raise the product to at least 9 without super-drastic changes and delegated the focused coherence pass." + "decision": "agent-selected", + "note": "Owner approved the revised product arc (issue #17); preserve lane kept the established design language so no separate visual-direction approval was required." } } diff --git a/PRODUCT.md b/PRODUCT.md index cc0c38db..cd76ed09 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -16,27 +16,30 @@ explicitly public Living surfaces before choosing whether to create an account. ## Product Purpose -Significant Hobbies is becoming a family of focused personal products: +Significant Hobbies helps a person name the life they want to live and walk +toward it — a family of focused personal products: - **Significant Hobbies** is the read-only directory and future dashboard. - **Live** owns bucket lists, hobbies, commitments, timelines, side quests, discovery, and the optional small new thing for today. -- **Journal** owns private AM/PM writing and its archive. +- **Weekly log** owns the private weekly record of what the user actually + lived — the journal's successor — and keeps earlier AM/PM entries readable + as archive. - **Habits** owns simple, non-scoring practice check-ins. - **History** remains part of Live and helps the user understand the life accumulating behind their plans. -The mortality frame connects the family by making finite time concrete. Success -means helping a person notice what matters, return to it, and keep a truthful -record without turning reflection into competition. +The mortality frame connects the family by making finite time concrete — the +roughly 4,000 weeks an average life holds. Success means helping a person +notice what matters, return to it, and keep a truthful record without turning +reflection into competition. ## Positioning -The family connects hobby discovery, private reflection, repeatable practice, -and a user-owned account of how interests evolve over a life. It is not a -generic productivity suite or social feed: Journal writing stays private, Live -publication is opt-in per item, and progress systems are limited to surfaces -where proof is the explicit job. +The family turns passive wanting into a life the user deliberately imagines, +plans, and records. It is not a generic productivity suite or social feed: +weekly-log writing stays private, Live publication is opt-in per item, and +progress systems are limited to surfaces where proof is the explicit job. ## Operating Context @@ -49,11 +52,12 @@ where proof is the explicit job. - `live.significanthobbies.com/` serves the preserved Live landing, and its calls to action lead into Live application routes on that same host. - `/live-more`, `/journal`, and `/habits` are the current Live product routes; - `/daily` is a compatibility doorway to Journal and Habits. + `/journal` hosts the weekly log, and `/daily` is a compatibility doorway + to the weekly log and Habits. - The hobby quiz is the single primary discovery path while its funnel is being evaluated. -- Authenticated users manage timelines, commitments, habits, journal entries, - bucket lists, quests, trajectory, and profile settings. +- Authenticated users manage timelines, commitments, habits, weekly-log + entries, bucket lists, quests, trajectory, and profile settings. - Cloudflare Workers/OpenNext serves the application; Cloudflare D1 stores authenticated application data through Drizzle. Signed-out private work remains in IndexedDB on the canonical Live origin. @@ -62,7 +66,7 @@ where proof is the explicit job. ## Capabilities and Constraints -- Journal writing has no public visibility field or sharing API. +- Weekly-log writing has no public visibility field or sharing API. - Habits are boolean check-ins with no score, streak, XP, or shame loop. - Commitments are separate hobby-specific goals with optional public visibility, proof stamps, and commitment-only streak badges. @@ -90,8 +94,8 @@ and none should be invented. ## Product Principles -1. Let Live, Journal, and Habits own distinct jobs while preserving explicit - links between related records. +1. Let Live, the weekly log, and Habits own distinct jobs while preserving + explicit links between related records. 2. Keep private work private and publication explicitly opt-in. 3. Let the mortality frame create urgency without scoring or shame. 4. Prefer one focused discovery path over multiplying surfaces. diff --git a/README.md b/README.md index 092b31dd..e77da8db 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Live by Significant Hobbies -Live helps people map hobbies and experiences across a lifetime: what they want -to try, what persisted, and what is worth doing next. It includes discovery, -bucket lists, commitments, timelines, side quests, and personal history. +Live helps a person name the life they want to live and walk toward it: what +they still want to try, what persisted, what is worth doing next, and — each +week — what they actually lived. It includes discovery, bucket lists, +commitments, timelines, side quests, the weekly log, and personal history. This is Live's canonical repository and issue tracker. Its Git history was preserved when it was extracted from [`Significant-Hobbies/significanthobbies`](https://github.com/Significant-Hobbies/significanthobbies). -The Hub and `PersonalSyncKit` remain there. Journal's independent source history -is retained, but Journal has been removed from the Fleet product lineup. +The Hub and `PersonalSyncKit` remain there. The former Journal product now lives +inside Live as the weekly log at `/journal`; earlier AM/PM entries remain +readable as archive. ## Local development diff --git a/docs/product/overview.md b/docs/product/overview.md index c4a4d310..ab09220f 100644 --- a/docs/product/overview.md +++ b/docs/product/overview.md @@ -1,14 +1,15 @@ --- title: Product overview -description: SignificantHobbies — a family of focused Live, Journal, and Habits products. Product thesis, users, brand, design principles, and the mortality frame. +description: SignificantHobbies — a family of focused Live, Weekly log, and Habits products. Product thesis, users, brand, design principles, and the mortality frame. --- # Product overview > A family of focused personal products. **Live** owns hobby discovery, bucket > lists, timelines, commitments, side quests, and one optional small new thing. -> **Journal** owns private AM/PM writing. **Habits** owns non-scoring practice -> check-ins. The mortality frame (life grid, manifesto) connects the family. +> **Weekly log** owns the private weekly record of what the user lived — the +> journal's successor. **Habits** owns non-scoring practice check-ins. The +> mortality frame (life grid, manifesto) connects the family. ## Register @@ -20,7 +21,7 @@ People in their 20s–40s who feel the pull of unlived experiences — curious a ## Product Purpose -A companion for living intentionally — helping people discover their hobbies, build their bucket list, and track a life worth remembering. The core loop is: discover what is possible → save what feels alive → take one small step → reflect on what changed. The bucket list is the durable centre of the Living dimension: it answers “what do I want to do with my life?” rather than just “what are my hobbies?” +A companion for living the life you actually want — helping people name what they still want to live, plan toward it, and keep a truthful weekly record of what they did. The core loop is: discover what is possible → save what feels alive → take one small step → record the week → reconcile against the wants. The bucket list is the durable centre of the Living dimension: it answers “what do I want to do with my life?” rather than just “what are my hobbies?” Lumi is the mascot: an amber/gold guiding light. Warm, aspirational, never preachy. "Your guiding light toward a life worth remembering." @@ -59,8 +60,11 @@ Emotional goal: users should feel seen, inspired, and gently nudged — not trac The 2026-08 split preserves existing data while giving each recurring job a clearer home: -- **Journal (private):** `/journal` owns AM/PM prompts and journal history. - Structurally private: no visibility field, public API, or sharing. +- **Weekly log (private):** `/journal` hosts the weekly log — one honest + entry a week answering "what did you live this week?", keyed to user-local + weeks (Monday start by default, adjustable). Earlier AM/PM journal entries + remain readable as archive. Structurally private: no visibility field, + public API, or sharing. - **Habits (private):** `/habits` owns simple check-ins and lightweight management without scoring. - **Live (private by default, selectively public):** hobbies, bucket lists, @@ -68,18 +72,19 @@ clearer home: home: the owned list and yearly goals come first, then corpus-backed discovery, the optional small new thing, Life Bingo, and Side Quests. Focused tools remain separate routes. -- **First-use Living loop:** onboarding turns remembered hobbies into a private - timeline and searches 5,000+ structured paths while accepting a pasted personal - bucket list. Yearly goals are captured independently, may optionally borrow - from the bucket list, and collectively become Trajectory direction. A daily habit is optional, - because episodic goals do not need artificial repetition. A separate owner-only choice is still required - before a timeline appears on the public profile. See - [`decisions.md`](../architecture/decisions.md) A13. -- **The journal is the bridge.** A private daily entry can optionally relate to - one of its owner's timelines or non-abandoned commitments. The relationship - creates a navigable thread back to the Living plan; it does not create proof, - progress, scoring, or public activity. See - [`decisions.md`](../architecture/decisions.md) A12. +- **First-use Living loop:** onboarding opens with the honest math — the + user's own weeks lived and roughly remaining of a ~4,000-week life — then + turns remembered hobbies into a private timeline and searches 5,000+ + structured paths while accepting a pasted personal bucket list. Yearly goals + are captured independently, may optionally borrow from the bucket list, and + collectively become Trajectory direction. A daily habit is optional, because + episodic goals do not need artificial repetition. A separate owner-only + choice is still required before a timeline appears on the public profile. + See [`decisions.md`](../architecture/decisions.md) A13. +- **The weekly log is the bridge.** Each week's entry sits alongside a + gentle "still calling?" check for dreams that have been quiet — a + reconsideration prompt, never a failure state. Missed weeks stay missed; + there are no streaks, scores, or make-up pressure. - **Habits can point to a commitment.** An owner may explicitly relate one private habit to one owned, non-abandoned commitment and change or clear that link later. The check-in remains lightweight context: only a deliberate proof @@ -97,17 +102,18 @@ clearer home: - **Live (`/live-more`):** bucket list and yearly goals, the optional small new thing, a substantial discovery engine, and small-step paths into Bingo and Side Quests. -- **Journal (`/journal`):** private writing and journal history. +- **Weekly log (`/journal`):** private weekly writing, week history, and + the archived AM/PM entries that preceded it. - **Habits (`/habits`):** a calm practice checklist and management surface without scores. - **History (`/history`):** the personal timeline, Life in Weeks, life-so-far - reflection, and Trajectory. It no longer retrieves Journal or Habits records. + reflection, and Trajectory. Weekly-log entries join the narrative. - **Daily (`/daily`):** a compatibility doorway that explains the split and - routes existing bookmarks to Journal or Habits. + routes existing bookmarks to the weekly log or Habits. All visitors see the product Hub at `/`. Public editorial, discovery, and explicitly public profiles remain reachable. Private-section attempts continue -at `/onboarding` until onboarding is complete. Navigation names Live, Journal, +at `/onboarding` until onboarding is complete. Navigation names Live, Weekly log, Habits, and History directly inside the existing same-origin application. ### What we deliberately do not do diff --git a/e2e/authenticated.spec.ts b/e2e/authenticated.spec.ts index 3b094667..f51af847 100644 --- a/e2e/authenticated.spec.ts +++ b/e2e/authenticated.spec.ts @@ -82,13 +82,15 @@ test.describe('authenticated surfaces', () => { }); } - test('/daily points to Journal and Habits instead of mixing them', async ({ authedPage }) => { + test('/daily points to the weekly log and Habits instead of mixing them', async ({ + authedPage, + }) => { await authedPage.goto('/daily'); - await expect(authedPage.getByRole('link', { name: /Open Journal/ })).toBeVisible(); + await expect(authedPage.getByRole('link', { name: /Open the weekly log/ })).toBeVisible(); await expect(authedPage.getByRole('link', { name: /Open Habits/ })).toBeVisible(); }); - test('Live and Journal persist independently after the split', async ({ authedPage }) => { + test('Live and the weekly log persist independently after the split', async ({ authedPage }) => { await authedPage.goto('/live-more'); const chosenIdea = `Zorblax Quivanta walk ${crypto.randomUUID().slice(0, 8)}`; await authedPage.getByLabel('What do you still want to live?').fill(chosenIdea); @@ -99,14 +101,14 @@ test.describe('authenticated surfaces', () => { const journal = 'I noticed something new today.'; await authedPage.goto('/journal'); - await authedPage.locator('#journal-entry').fill(journal); - await authedPage.getByRole('button', { name: /Save (morning|evening)/ }).click(); + await authedPage.locator('#weekly-entry').fill(journal); + await authedPage.getByRole('button', { name: /Save this week|Update this week/ }).click(); await authedPage.goto('/live-more'); await authedPage.getByLabel('What do you still want to live?').fill(chosenIdea); await expect(authedPage.getByRole('button', { name: 'Calling now' })).toBeVisible(); await authedPage.goto('/journal'); - await expect(authedPage.locator('#journal-entry')).toHaveValue(journal); + await expect(authedPage.locator('#weekly-entry')).toHaveValue(journal); await authedPage.goto('/live-more'); await expect(authedPage.getByRole('heading', { name: chosenIdea, level: 1 })).toBeVisible(); }); @@ -117,12 +119,13 @@ test.describe('authenticated surfaces', () => { await expect(authedPage.getByRole('link', { name: 'Sign in' })).toHaveCount(0); }); - test('compact AM/PM context derives from journal text', async ({ authedPage }) => { + test('the weekly log renders the current week and a nudge question', async ({ authedPage }) => { await authedPage.goto('/journal'); - // Both ring labels are always present; the assertion is that the page renders - // them from journal state without a DailyCheckin row existing. - await expect(authedPage.getByText(/^AM (written|open)$/)).toBeVisible(); - await expect(authedPage.getByText(/^PM (written|open)$/)).toBeVisible(); + await expect(authedPage.locator('#weekly-entry')).toBeVisible(); + await expect(authedPage.getByText(/Week of /)).toBeVisible(); + await expect( + authedPage.getByRole('button', { name: /Save this week|Update this week/ }) + ).toBeVisible(); }); test('the signed-out preview never leaks into a real session', async ({ authedPage }) => { diff --git a/e2e/daily.spec.ts b/e2e/daily.spec.ts index f4124ada..af5e05d6 100644 --- a/e2e/daily.spec.ts +++ b/e2e/daily.spec.ts @@ -9,7 +9,7 @@ test.describe('Journal, Habits & manifesto', () => { const res = await page.goto('/daily'); expect(res?.status()).toBeLessThan(400); expect(page.url()).not.toContain('/login'); - await expect(page.getByRole('link', { name: /Open Journal/ })).toHaveAttribute( + await expect(page.getByRole('link', { name: /Open the weekly log/ })).toHaveAttribute( 'href', '/journal' ); @@ -42,19 +42,14 @@ test.describe('Journal, Habits & manifesto', () => { await expect(page.getByText('Walk after lunch')).toBeVisible(); await page.goto('/journal'); - await page.locator('#journal-entry').fill('I made room for a slower afternoon.'); - await page.getByRole('button', { name: /Save (morning|evening)/ }).click(); + await page.locator('#weekly-entry').fill('I made room for a slower afternoon.'); + await page.getByRole('button', { name: /Save this week|Update this week/ }).click(); await page.reload(); - await expect(page.locator('#journal-entry')).toHaveValue('I made room for a slower afternoon.'); - await expect( - page - .getByRole('region', { name: 'Journal history' }) - .getByText('I made room for a slower afternoon.', { exact: true }) - ).toBeVisible(); + await expect(page.locator('#weekly-entry')).toHaveValue('I made room for a slower afternoon.'); await page.goto('/habits'); await expect(page.getByText('Walk after lunch')).toBeVisible(); - await expect(page.locator('#journal-entry')).toHaveCount(0); + await expect(page.locator('#weekly-entry')).toHaveCount(0); await expect(page.getByText('Ready when you are')).toBeVisible(); await expect(page.getByText(/\d+ of \d+ complete today/)).toHaveCount(0); }); @@ -106,8 +101,8 @@ test.describe('Journal, Habits & manifesto', () => { // Two dimensions — match the bold labels in the manifesto body await expect(page.locator('article').getByText('Daily.')).toBeVisible(); await expect(page.locator('article').getByText('Living.')).toBeVisible(); - // The journal as bridge - await expect(page.getByText(/journal is the bridge/i)).toBeVisible(); + // The weekly log as bridge + await expect(page.getByText(/weekly log is the bridge/i)).toBeVisible(); }); test('/manifesto has working CTAs', async ({ page }) => { @@ -128,21 +123,21 @@ test.describe('Journal, Habits & manifesto', () => { await expect(bucketListLink).toHaveAttribute('href', '/bucket-lists'); }); - test('nav exposes Live, Journal, and Habits as distinct products', async ({ page }) => { + test('nav exposes Live, Weekly log, and Habits as distinct products', async ({ page }) => { await completeLocalOnboarding(page); await page.goto('/hobbies'); if ((page.viewportSize()?.width ?? 0) < 1024) { await page.getByRole('button', { name: 'Open menu' }).click(); } await expect(page.getByRole('link', { name: 'Live', exact: true }).first()).toBeVisible(); - await expect(page.getByRole('link', { name: 'Journal', exact: true }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: 'Weekly log', exact: true }).first()).toBeVisible(); await expect(page.getByRole('link', { name: 'Habits', exact: true }).first()).toBeVisible(); }); test('public footer keeps private workspace links out', async ({ page }) => { await page.goto('/hobbies'); const footer = page.locator('[data-site-footer]'); - await expect(footer.getByRole('link', { name: 'Journal', exact: true })).toHaveCount(0); + await expect(footer.getByRole('link', { name: 'Weekly log', exact: true })).toHaveCount(0); await expect(footer.getByRole('link', { name: 'Habits', exact: true })).toHaveCount(0); await expect(footer.getByRole('link', { name: 'Find your hobby' })).toBeVisible(); await expect(footer.getByRole('link', { name: 'Things to try' })).toBeVisible(); diff --git a/e2e/guest-access.spec.ts b/e2e/guest-access.spec.ts index a107ab8d..f1898b3c 100644 --- a/e2e/guest-access.spec.ts +++ b/e2e/guest-access.spec.ts @@ -15,7 +15,7 @@ test.describe('private work is locally available without an account', () => { page, }) => { await page.goto('/hobbies'); - await expect(page.getByRole('link', { name: 'Journal', exact: true })).toHaveCount(0); + await expect(page.getByRole('link', { name: 'Weekly log', exact: true })).toHaveCount(0); await expect(page.getByRole('link', { name: 'Habits', exact: true })).toHaveCount(0); if (!(await page.getByRole('link', { name: 'Possibilities' }).isVisible())) { await page.getByRole('button', { name: 'Open menu' }).click(); diff --git a/e2e/life-atlas.spec.ts b/e2e/life-atlas.spec.ts index c1354d83..7369f95d 100644 --- a/e2e/life-atlas.spec.ts +++ b/e2e/life-atlas.spec.ts @@ -8,7 +8,7 @@ test.describe('Life Atlas shell', () => { await completeLocalOnboarding(page); }); - test('uses distinct Live, Journal, Habits, and History destinations', async ({ page }) => { + test('uses distinct Live, Weekly log, Habits, and History destinations', async ({ page }) => { await page.goto('/live-more'); const nav = page.getByRole('navigation').first(); if ((page.viewportSize()?.width ?? 0) < 1024) { @@ -18,10 +18,9 @@ test.describe('Life Atlas shell', () => { 'href', '/live-more' ); - await expect(nav.getByRole('link', { name: 'Journal', exact: true }).first()).toHaveAttribute( - 'href', - '/journal' - ); + await expect( + nav.getByRole('link', { name: 'Weekly log', exact: true }).first() + ).toHaveAttribute('href', '/journal'); await expect(nav.getByRole('link', { name: 'Habits', exact: true }).first()).toHaveAttribute( 'href', '/habits' diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index d2f5f031..759a8442 100644 --- a/e2e/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -8,12 +8,14 @@ test.describe('Onboarding', () => { await page.getByLabel('What should we call you?').fill('Local Person'); await page.getByLabel('When were you born?').fill('1994-04-12'); await page.getByRole('button', { name: /Begin my story/i }).click(); - await page.getByRole('button', { name: /Skip this moment/i }).click(); + // The weeks reveal is step 2: their own number, not a generic quote. + await expect(page.getByText(/weeks in\. About [\d,]+ to go\./)).toBeVisible(); + await page.getByRole('button', { name: /This is why the list matters/i }).click(); await page.getByRole('button', { name: 'Drawing' }).click(); await page.waitForTimeout(300); await page.reload(); await expect(page.getByRole('button', { name: 'Drawing' })).toBeVisible(); - await expect(page.getByText('3 of 7')).toBeVisible(); + await expect(page.getByText('3 of 5')).toBeVisible(); await expect(page.locator('body')).toContainText('this device'); }); @@ -26,7 +28,7 @@ test.describe('Onboarding', () => { await page.getByLabel('What should we call you?').fill('Local Explorer'); await page.getByLabel('When were you born?').fill('1994-04-12'); await page.getByRole('button', { name: /Begin my story/i }).click(); - await page.getByRole('button', { name: /Skip this moment/i }).click(); + await page.getByRole('button', { name: /This is why the list matters/i }).click(); await page.getByRole('button', { name: 'Drawing' }).click(); await page.getByRole('button', { name: /Build this chapter/i }).click(); @@ -54,8 +56,6 @@ test.describe('Onboarding', () => { await page.getByLabel('Add a goal for this year').fill('Build a calmer work week'); await page.getByRole('button', { name: 'Add yearly goal' }).click(); await page.locator('[data-focus-option]').nth(0).click(); - await page.getByRole('button', { name: /Keep these goals/i }).click(); - await page.getByRole('button', { name: /Continue without a daily practice/i }).click(); await page .getByLabel('What is true about my life right now') .fill('Travel needs advance planning, while writing fits into quiet weekends.'); @@ -111,7 +111,7 @@ test.describe('Onboarding', () => { await expect(page.locator('iframe[title="Lights by Ikson"]')).toBeVisible(); await page.getByRole('button', { name: 'Choose music' }).click(); - await page.getByRole('button', { name: /Skip this moment/i }).click(); + await page.getByRole('button', { name: 'Continue', exact: true }).click(); await page.getByRole('button', { name: 'Drawing' }).click(); await page.getByRole('button', { name: /Build this chapter/i }).click(); @@ -196,7 +196,7 @@ test.describe('Onboarding', () => { if (await dismissImport.isVisible()) await dismissImport.click(); await page.getByLabel('When were you born?').fill('1992-08-15'); await page.getByRole('button', { name: /Begin my story/i }).click(); - await page.getByRole('button', { name: /Skip this moment/i }).click(); + await page.getByRole('button', { name: /This is why the list matters/i }).click(); await page.getByRole('button', { name: 'Drawing' }).click(); await page.getByRole('button', { name: /Build this chapter/i }).click(); await page.locator('[data-possibility-option]').nth(0).click(); @@ -204,8 +204,6 @@ test.describe('Onboarding', () => { await page.getByRole('button', { name: /Keep these possibilities/i }).click(); await page.locator('[data-focus-option]').nth(0).click(); await page.locator('[data-focus-option]').nth(1).click(); - await page.getByRole('button', { name: /Keep these goals/i }).click(); - await page.getByRole('button', { name: /Continue without a daily practice/i }).click(); await page .getByLabel('What is true about my life right now') .fill('Weekdays are busy, but Sunday mornings are open.'); diff --git a/e2e/preview.spec.ts b/e2e/preview.spec.ts index 177fb381..de997a7a 100644 --- a/e2e/preview.spec.ts +++ b/e2e/preview.spec.ts @@ -10,7 +10,7 @@ test.describe('signed-out private surfaces are local-first', () => { test('/journal exposes editable local writing', async ({ page }) => { await page.goto('/journal'); await expect(page.getByLabel('Preview notice')).toHaveCount(0); - await expect(page.locator('#journal-entry')).toBeVisible(); + await expect(page.locator('#weekly-entry')).toBeVisible(); await expect(page.locator('body')).toContainText('this device'); }); @@ -18,7 +18,7 @@ test.describe('signed-out private surfaces are local-first', () => { await page.goto('/habits'); await expect(page.getByLabel('Preview notice')).toHaveCount(0); await expect(page.getByRole('button', { name: 'Manage' })).toBeVisible(); - await expect(page.locator('#journal-entry')).toHaveCount(0); + await expect(page.locator('#weekly-entry')).toHaveCount(0); }); test('/trajectory exposes an editable local contract', async ({ page }) => { diff --git a/landing-astro/src/components/FaqSection.astro b/landing-astro/src/components/FaqSection.astro index 75ae4c07..5ee2fb13 100644 --- a/landing-astro/src/components/FaqSection.astro +++ b/landing-astro/src/components/FaqSection.astro @@ -25,8 +25,8 @@ const faqs = [ a: 'Not by default. Live publication is opt-in per supported item. Public timelines and profiles can be shared by link, while private work remains private unless you explicitly choose otherwise.', }, { - q: 'Where are Journal and Habits?', - a: 'Journal and Habits are separate Significant Hobbies products with focused jobs. Live owns discovery, plans, experiences, and history; it does not present private writing or simple habit check-ins as if they were the same product.', + q: 'Where are the weekly log and Habits?', + a: 'The weekly log and Habits are focused surfaces inside Significant Hobbies. Live owns discovery, plans, experiences, and history; it does not present private weekly writing or simple habit check-ins as if they were the same product.', }, ]; diff --git a/migrations/d1/0005_fancy_wild_child.sql b/migrations/d1/0005_fancy_wild_child.sql new file mode 100644 index 00000000..509cf131 --- /dev/null +++ b/migrations/d1/0005_fancy_wild_child.sql @@ -0,0 +1,14 @@ +CREATE TABLE `WeeklyLogEntry` ( + `id` text PRIMARY KEY NOT NULL, + `userId` text NOT NULL, + `weekOf` text NOT NULL, + `text` text NOT NULL, + `promptText` text, + `createdAt` integer DEFAULT (unixepoch()) NOT NULL, + `updatedAt` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `WeeklyLogEntry_userId_weekOf_key` ON `WeeklyLogEntry` (`userId`,`weekOf`);--> statement-breakpoint +CREATE INDEX `WeeklyLogEntry_userId_idx` ON `WeeklyLogEntry` (`userId`);--> statement-breakpoint +ALTER TABLE `User` ADD `weekStartsOn` text; diff --git a/migrations/d1/meta/0005_snapshot.json b/migrations/d1/meta/0005_snapshot.json new file mode 100644 index 00000000..62c9dc32 --- /dev/null +++ b/migrations/d1/meta/0005_snapshot.json @@ -0,0 +1,3047 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7a803602-1ed9-49b4-8801-309235abede3", + "prevId": "1f600a52-e529-4ecd-9d6f-23d5b7c74ef8", + "tables": { + "auth_account": { + "name": "auth_account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_account_userId_auth_user_id_fk": { + "name": "auth_account_userId_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Account": { + "name": "Account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "Account_provider_providerAccountId_key": { + "name": "Account_provider_providerAccountId_key", + "columns": [ + "provider", + "providerAccountId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Arc": { + "name": "Arc", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "sourceBucketItemId": { + "name": "sourceBucketItemId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceTimelineId": { + "name": "sourceTimelineId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "startedAt": { + "name": "startedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "completedAt": { + "name": "completedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Arc_userId_status_idx": { + "name": "Arc_userId_status_idx", + "columns": [ + "userId", + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Arc_userId_User_id_fk": { + "name": "Arc_userId_User_id_fk", + "tableFrom": "Arc", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "BucketListItem": { + "name": "BucketListItem", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'planned'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "sourceSlug": { + "name": "sourceSlug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceItemTitle": { + "name": "sourceItemTitle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "targetYear": { + "name": "targetYear", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completedAt": { + "name": "completedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "BucketListItem_userId_idx": { + "name": "BucketListItem_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "BucketListItem_userId_User_id_fk": { + "name": "BucketListItem_userId_User_id_fk", + "tableFrom": "BucketListItem", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "BucketList": { + "name": "BucketList", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "horizon": { + "name": "horizon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boldness": { + "name": "boldness", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "defaultView": { + "name": "defaultView", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'LIST'" + }, + "intentions": { + "name": "intentions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PRIVATE'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "BucketList_slug_unique": { + "name": "BucketList_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "BucketList_userId_idx": { + "name": "BucketList_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "BucketList_slug_idx": { + "name": "BucketList_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "BucketList_visibility_idx": { + "name": "BucketList_visibility_idx", + "columns": [ + "visibility" + ], + "isUnique": false + } + }, + "foreignKeys": { + "BucketList_userId_User_id_fk": { + "name": "BucketList_userId_User_id_fk", + "tableFrom": "BucketList", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Comment": { + "name": "Comment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timelineId": { + "name": "timelineId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Comment_userId_idx": { + "name": "Comment_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "Comment_timelineId_idx": { + "name": "Comment_timelineId_idx", + "columns": [ + "timelineId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Comment_userId_User_id_fk": { + "name": "Comment_userId_User_id_fk", + "tableFrom": "Comment", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "Comment_timelineId_Timeline_id_fk": { + "name": "Comment_timelineId_Timeline_id_fk", + "tableFrom": "Comment", + "tableTo": "Timeline", + "columnsFrom": [ + "timelineId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Commitment": { + "name": "Commitment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hobbyName": { + "name": "hobbyName", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goalDays": { + "name": "goalDays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "startDate": { + "name": "startDate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completedAt": { + "name": "completedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Commitment_userId_idx": { + "name": "Commitment_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "Commitment_status_idx": { + "name": "Commitment_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "Commitment_visibility_idx": { + "name": "Commitment_visibility_idx", + "columns": [ + "visibility" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Commitment_userId_User_id_fk": { + "name": "Commitment_userId_User_id_fk", + "tableFrom": "Commitment", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "DailyCheckin": { + "name": "DailyCheckin", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dayDate": { + "name": "dayDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amCompleted": { + "name": "amCompleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pmCompleted": { + "name": "pmCompleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "DailyCheckin_userId_dayDate_key": { + "name": "DailyCheckin_userId_dayDate_key", + "columns": [ + "userId", + "dayDate" + ], + "isUnique": true + } + }, + "foreignKeys": { + "DailyCheckin_userId_User_id_fk": { + "name": "DailyCheckin_userId_User_id_fk", + "tableFrom": "DailyCheckin", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Follow": { + "name": "Follow", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "followerId": { + "name": "followerId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "followingId": { + "name": "followingId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Follow_followerId_followingId_key": { + "name": "Follow_followerId_followingId_key", + "columns": [ + "followerId", + "followingId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Follow_followerId_User_id_fk": { + "name": "Follow_followerId_User_id_fk", + "tableFrom": "Follow", + "tableTo": "User", + "columnsFrom": [ + "followerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "Follow_followingId_User_id_fk": { + "name": "Follow_followingId_User_id_fk", + "tableFrom": "Follow", + "tableTo": "User", + "columnsFrom": [ + "followingId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "HabitLog": { + "name": "HabitLog", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "habitId": { + "name": "habitId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dayDate": { + "name": "dayDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed": { + "name": "completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "HabitLog_habitId_dayDate_key": { + "name": "HabitLog_habitId_dayDate_key", + "columns": [ + "habitId", + "dayDate" + ], + "isUnique": true + }, + "HabitLog_userId_idx": { + "name": "HabitLog_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "HabitLog_habitId_Habit_id_fk": { + "name": "HabitLog_habitId_Habit_id_fk", + "tableFrom": "HabitLog", + "tableTo": "Habit", + "columnsFrom": [ + "habitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "HabitLog_userId_User_id_fk": { + "name": "HabitLog_userId_User_id_fk", + "tableFrom": "HabitLog", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Habit": { + "name": "Habit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "targetFrequency": { + "name": "targetFrequency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'daily'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceQuestId": { + "name": "sourceQuestId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commitmentId": { + "name": "commitmentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Habit_userId_idx": { + "name": "Habit_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "Habit_sourceQuestId_idx": { + "name": "Habit_sourceQuestId_idx", + "columns": [ + "sourceQuestId" + ], + "isUnique": false + }, + "Habit_commitmentId_idx": { + "name": "Habit_commitmentId_idx", + "columns": [ + "commitmentId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Habit_userId_User_id_fk": { + "name": "Habit_userId_User_id_fk", + "tableFrom": "Habit", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "Habit_commitmentId_Commitment_id_fk": { + "name": "Habit_commitmentId_Commitment_id_fk", + "tableFrom": "Habit", + "tableTo": "Commitment", + "columnsFrom": [ + "commitmentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "JournalEntry": { + "name": "JournalEntry", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dayDate": { + "name": "dayDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amEntry": { + "name": "amEntry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pmEntry": { + "name": "pmEntry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timelineId": { + "name": "timelineId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commitmentId": { + "name": "commitmentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "noveltyId": { + "name": "noveltyId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "noveltyText": { + "name": "noveltyText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "noveltyCompleted": { + "name": "noveltyCompleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "JournalEntry_userId_dayDate_key": { + "name": "JournalEntry_userId_dayDate_key", + "columns": [ + "userId", + "dayDate" + ], + "isUnique": true + }, + "JournalEntry_userId_idx": { + "name": "JournalEntry_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "JournalEntry_timelineId_idx": { + "name": "JournalEntry_timelineId_idx", + "columns": [ + "timelineId" + ], + "isUnique": false + }, + "JournalEntry_commitmentId_idx": { + "name": "JournalEntry_commitmentId_idx", + "columns": [ + "commitmentId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "JournalEntry_userId_User_id_fk": { + "name": "JournalEntry_userId_User_id_fk", + "tableFrom": "JournalEntry", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "JournalEntry_timelineId_Timeline_id_fk": { + "name": "JournalEntry_timelineId_Timeline_id_fk", + "tableFrom": "JournalEntry", + "tableTo": "Timeline", + "columnsFrom": [ + "timelineId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "JournalEntry_commitmentId_Commitment_id_fk": { + "name": "JournalEntry_commitmentId_Commitment_id_fk", + "tableFrom": "JournalEntry", + "tableTo": "Commitment", + "columnsFrom": [ + "commitmentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "JournalEntry_single_context_check": { + "name": "JournalEntry_single_context_check", + "value": "\"JournalEntry\".\"timelineId\" IS NULL OR \"JournalEntry\".\"commitmentId\" IS NULL" + } + } + }, + "Like": { + "name": "Like", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timelineId": { + "name": "timelineId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Like_userId_timelineId_key": { + "name": "Like_userId_timelineId_key", + "columns": [ + "userId", + "timelineId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Like_userId_User_id_fk": { + "name": "Like_userId_User_id_fk", + "tableFrom": "Like", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "Like_timelineId_Timeline_id_fk": { + "name": "Like_timelineId_Timeline_id_fk", + "tableFrom": "Like", + "tableTo": "Timeline", + "columnsFrom": [ + "timelineId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "native_atlas_state": { + "name": "native_atlas_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "native_atlas_state_user_id_auth_user_id_fk": { + "name": "native_atlas_state_user_id_auth_user_id_fk", + "tableFrom": "native_atlas_state", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "native_auth_handoffs": { + "name": "native_auth_handoffs", + "columns": { + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "native_auth_handoffs_expiry_idx": { + "name": "native_auth_handoffs_expiry_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_session": { + "name": "auth_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auth_session_userId_auth_user_id_fk": { + "name": "auth_session_userId_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Session": { + "name": "Session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "Session_sessionToken_unique": { + "name": "Session_sessionToken_unique", + "columns": [ + "sessionToken" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Stamp": { + "name": "Stamp", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "commitmentId": { + "name": "commitmentId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dayDate": { + "name": "dayDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dayIndex": { + "name": "dayIndex", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "proofUrl": { + "name": "proofUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "proofType": { + "name": "proofType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'url'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Stamp_commitmentId_dayDate_key": { + "name": "Stamp_commitmentId_dayDate_key", + "columns": [ + "commitmentId", + "dayDate" + ], + "isUnique": true + }, + "Stamp_commitmentId_idx": { + "name": "Stamp_commitmentId_idx", + "columns": [ + "commitmentId" + ], + "isUnique": false + }, + "Stamp_userId_idx": { + "name": "Stamp_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Stamp_commitmentId_Commitment_id_fk": { + "name": "Stamp_commitmentId_Commitment_id_fk", + "tableFrom": "Stamp", + "tableTo": "Commitment", + "columnsFrom": [ + "commitmentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "Stamp_userId_User_id_fk": { + "name": "Stamp_userId_User_id_fk", + "tableFrom": "Stamp", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "Timeline": { + "name": "Timeline", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PRIVATE'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phases": { + "name": "phases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "pins": { + "name": "pins", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "versions": { + "name": "versions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "Timeline_slug_unique": { + "name": "Timeline_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "Timeline_userId_idx": { + "name": "Timeline_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "Timeline_slug_idx": { + "name": "Timeline_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "Timeline_visibility_idx": { + "name": "Timeline_visibility_idx", + "columns": [ + "visibility" + ], + "isUnique": false + } + }, + "foreignKeys": { + "Timeline_userId_User_id_fk": { + "name": "Timeline_userId_User_id_fk", + "tableFrom": "Timeline", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "TrajectoryContract": { + "name": "TrajectoryContract", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previousContractId": { + "name": "previousContractId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "constraintsText": { + "name": "constraintsText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "intentText": { + "name": "intentText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "decisionPolicyText": { + "name": "decisionPolicyText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feedbackLoopText": { + "name": "feedbackLoopText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "openedAt": { + "name": "openedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "closedAt": { + "name": "closedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "TrajectoryContract_userId_idx": { + "name": "TrajectoryContract_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "TrajectoryContract_one_active_per_user_idx": { + "name": "TrajectoryContract_one_active_per_user_idx", + "columns": [ + "userId" + ], + "isUnique": true, + "where": "\"TrajectoryContract\".\"status\" = 'active'" + } + }, + "foreignKeys": { + "TrajectoryContract_userId_User_id_fk": { + "name": "TrajectoryContract_userId_User_id_fk", + "tableFrom": "TrajectoryContract", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "TrajectoryEntry": { + "name": "TrajectoryEntry", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "eraId": { + "name": "eraId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthKey": { + "name": "monthKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reflection": { + "name": "reflection", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "numbers": { + "name": "numbers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "TrajectoryEntry_eraId_monthKey_key": { + "name": "TrajectoryEntry_eraId_monthKey_key", + "columns": [ + "eraId", + "monthKey" + ], + "isUnique": true + }, + "TrajectoryEntry_userId_idx": { + "name": "TrajectoryEntry_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "TrajectoryEntry_userId_bucket_idx": { + "name": "TrajectoryEntry_userId_bucket_idx", + "columns": [ + "userId", + "bucket" + ], + "isUnique": false + } + }, + "foreignKeys": { + "TrajectoryEntry_eraId_TrajectoryEra_id_fk": { + "name": "TrajectoryEntry_eraId_TrajectoryEra_id_fk", + "tableFrom": "TrajectoryEntry", + "tableTo": "TrajectoryEra", + "columnsFrom": [ + "eraId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "TrajectoryEntry_userId_User_id_fk": { + "name": "TrajectoryEntry_userId_User_id_fk", + "tableFrom": "TrajectoryEntry", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "TrajectoryEra": { + "name": "TrajectoryEra", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idealText": { + "name": "idealText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "openedAt": { + "name": "openedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "closedAt": { + "name": "closedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "TrajectoryEra_userId_idx": { + "name": "TrajectoryEra_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "TrajectoryEra_userId_bucket_status_idx": { + "name": "TrajectoryEra_userId_bucket_status_idx", + "columns": [ + "userId", + "bucket", + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "TrajectoryEra_userId_User_id_fk": { + "name": "TrajectoryEra_userId_User_id_fk", + "tableFrom": "TrajectoryEra", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "TrajectoryReview": { + "name": "TrajectoryReview", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "contractId": { + "name": "contractId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signalText": { + "name": "signalText", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "TrajectoryReview_contractId_idx": { + "name": "TrajectoryReview_contractId_idx", + "columns": [ + "contractId" + ], + "isUnique": false + }, + "TrajectoryReview_userId_idx": { + "name": "TrajectoryReview_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "TrajectoryReview_contractId_TrajectoryContract_id_fk": { + "name": "TrajectoryReview_contractId_TrajectoryContract_id_fk", + "tableFrom": "TrajectoryReview", + "tableTo": "TrajectoryContract", + "columnsFrom": [ + "contractId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "TrajectoryReview_userId_User_id_fk": { + "name": "TrajectoryReview_userId_User_id_fk", + "tableFrom": "TrajectoryReview", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_user": { + "name": "auth_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "UserQuest": { + "name": "UserQuest", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questId": { + "name": "questId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'rediscovery'" + }, + "sourceHobby": { + "name": "sourceHobby", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceTimelineId": { + "name": "sourceTimelineId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceBucketItemId": { + "name": "sourceBucketItemId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arcId": { + "name": "arcId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "startedAt": { + "name": "startedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "completedAt": { + "name": "completedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "UserQuest_userId_status_idx": { + "name": "UserQuest_userId_status_idx", + "columns": [ + "userId", + "status" + ], + "isUnique": false + }, + "UserQuest_userId_visibility_idx": { + "name": "UserQuest_userId_visibility_idx", + "columns": [ + "userId", + "visibility" + ], + "isUnique": false + }, + "UserQuest_userId_questId_active_key": { + "name": "UserQuest_userId_questId_active_key", + "columns": [ + "userId", + "questId", + "status" + ], + "isUnique": true + } + }, + "foreignKeys": { + "UserQuest_userId_User_id_fk": { + "name": "UserQuest_userId_User_id_fk", + "tableFrom": "UserQuest", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "User": { + "name": "User", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthYear": { + "name": "birthYear", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthDate": { + "name": "birthDate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weekStartsOn": { + "name": "weekStartsOn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creed": { + "name": "creed", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "onboardingData": { + "name": "onboardingData", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "onboardingCompletedAt": { + "name": "onboardingCompletedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completedQuests": { + "name": "completedQuests", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "earnedBadges": { + "name": "earnedBadges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "User_email_unique": { + "name": "User_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "User_username_unique": { + "name": "User_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_verification": { + "name": "auth_verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "VerificationToken_token_unique": { + "name": "VerificationToken_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "VerificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "VerificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "WeeklyLogEntry": { + "name": "WeeklyLogEntry", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weekOf": { + "name": "weekOf", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptText": { + "name": "promptText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "WeeklyLogEntry_userId_weekOf_key": { + "name": "WeeklyLogEntry_userId_weekOf_key", + "columns": [ + "userId", + "weekOf" + ], + "isUnique": true + }, + "WeeklyLogEntry_userId_idx": { + "name": "WeeklyLogEntry_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "WeeklyLogEntry_userId_User_id_fk": { + "name": "WeeklyLogEntry_userId_User_id_fk", + "tableFrom": "WeeklyLogEntry", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/d1/meta/_journal.json b/migrations/d1/meta/_journal.json index c8e6d1c3..915a497d 100644 --- a/migrations/d1/meta/_journal.json +++ b/migrations/d1/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1785681331925, "tag": "0004_nifty_xavin", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1789913353669, + "tag": "0005_fancy_wild_child", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/app/api/weekly-nudge/route.ts b/src/app/api/weekly-nudge/route.ts new file mode 100644 index 00000000..143cab4a --- /dev/null +++ b/src/app/api/weekly-nudge/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from 'next/server'; + +import { resolveWeeklyNudge } from '~/lib/weekly-nudge'; +import { weekStartFor, normalizeWeekStartsOn } from '~/lib/weekly-log'; +import { dayKeyIn } from '~/lib/day'; +import type { NudgeSignals } from '~/lib/weekly-questions'; + +export const dynamic = 'force-dynamic'; + +const WEEK_OF_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +function cleanSignals(value: unknown): NudgeSignals | null { + if (!value || typeof value !== 'object') return null; + const raw = value as Record; + const categories = Array.isArray(raw.staleDreamCategories) + ? raw.staleDreamCategories + .filter((c): c is string => typeof c === 'string' && c.length > 0 && c.length <= 40) + .slice(0, 10) + : []; + const count = (v: unknown, max: number) => + typeof v === 'number' && Number.isInteger(v) && v >= 0 ? Math.min(v, max) : 0; + return { + staleDreamCategories: categories, + entriesLastMonth: count(raw.entriesLastMonth, 52), + activeCommitments: count(raw.activeCommitments, 100), + activeDreams: count(raw.activeDreams, 1000), + isSuggestedDay: raw.isSuggestedDay === true, + }; +} + +/** + * Nudge-question resolution for signed-out (local) users. Accepts only + * categorical signals and ids — the route builds the classifier context + * itself, so arbitrary text can never reach classifier.dev through it. + */ +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + const raw = (body ?? {}) as Record; + const signals = cleanSignals(raw.signals); + if (!signals) return NextResponse.json({ error: 'Invalid signals' }, { status: 400 }); + + const today = dayKeyIn(null); + const weekStartsOn = normalizeWeekStartsOn(raw.weekStartsOn); + const weekOf = + typeof raw.weekOf === 'string' && WEEK_OF_PATTERN.test(raw.weekOf) + ? raw.weekOf + : weekStartFor(today, weekStartsOn); + const excludeIds = Array.isArray(raw.excludeIds) + ? raw.excludeIds.filter((id): id is string => typeof id === 'string').slice(0, 40) + : []; + + const question = await resolveWeeklyNudge(signals, weekOf, excludeIds); + return NextResponse.json({ question }); +} diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index 263595a6..16bbb162 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -26,9 +26,9 @@ export default function DailyTransitionPage() { href="/journal" color="bg-[#c5abfa] text-[#241a31]" icon={} - title="Journal" - copy="Write this morning or evening, then return to the words you left behind." - action="Open Journal" + title="Weekly log" + copy="Once a week, write what you actually lived — Sunday is the natural moment, any day counts." + action="Open the weekly log" /> ({ + weekOf: entry.weekOf, + text: entry.text, + })), commitments: rawCommitments.map((c) => ({ hobbyName: c.hobbyName, goalDays: c.goalDays, diff --git a/src/app/journal/page.tsx b/src/app/journal/page.tsx index a31246df..faa9b14b 100644 --- a/src/app/journal/page.tsx +++ b/src/app/journal/page.tsx @@ -1,26 +1,34 @@ import { eq } from 'drizzle-orm'; import { redirect } from 'next/navigation'; -import { LocalJournalExperience } from '~/components/local-personal-practice-surfaces'; +import { JournalArchive } from '~/components/journal-archive'; import { LocalOnboardingGate } from '~/components/local-onboarding-gate'; -import { JournalExperience } from '~/components/personal-practice-surfaces'; +import { LocalWeeklyLog } from '~/components/weekly-log/local-weekly-log'; +import { WeeklyLogSurface } from '~/components/weekly-log/weekly-log-surface'; import { TimezoneSync } from '~/components/timezone-sync'; import { users } from '~/db/schema'; +import { getAllJournalEntries } from '~/lib/actions/daily'; +import { setCallingBucketListDream } from '~/lib/actions/bucket-list'; import { - getAllJournalEntries, - getJournalContextChoices, - getUserProfile, - saveJournalEntry, -} from '~/lib/actions/daily'; -import { dayKeyIn, isMorningIn } from '~/lib/day'; + getNudgeSignals, + getRecentPromptIds, + getStaleDreams, + getWeekStartsOn, + getWeeklyLogEntries, + saveWeeklyLogEntry, + setWeekStartsOn, +} from '~/lib/actions/weekly-log'; +import { dayKeyIn } from '~/lib/day'; import { parseBirthDate } from '~/lib/life-in-weeks'; import { birthDateFromYear, buildLifeGrid } from '~/lib/mortality'; +import { resolveWeeklyNudge } from '~/lib/weekly-nudge'; +import { weekStartFor, type WeekStartsOn } from '~/lib/weekly-log'; import { getServerAuthSession } from '~/server/auth'; import { db } from '~/server/db'; export const metadata = { - title: 'Journal — Significant Hobbies', - description: 'A private place to notice what changed and remember what mattered.', + title: 'Weekly log — Significant Hobbies', + description: 'A private weekly record of what you actually lived.', robots: { index: false, follow: false }, }; @@ -31,7 +39,7 @@ export default async function JournalPage() { const today = dayKeyIn(null); return ( - + ); } @@ -47,13 +55,23 @@ export default async function JournalPage() { }); if (!me?.onboardingCompletedAt) redirect('/onboarding'); + const [entries, staleDreams, weekStartsOn, signals, excludeIds, journalHistory, profile] = + await Promise.all([ + getWeeklyLogEntries(), + getStaleDreams(3), + getWeekStartsOn(), + getNudgeSignals(me.timezone), + getRecentPromptIds(), + getAllJournalEntries(), + db.query.users.findFirst({ + where: eq(users.id, session.user.id), + columns: { name: true }, + }), + ]); + const today = dayKeyIn(me.timezone); - const [journalEntries, journalContextChoices, profile] = await Promise.all([ - getAllJournalEntries(), - getJournalContextChoices(), - getUserProfile(), - ]); - const journalEntry = journalEntries.find((entry) => entry.dayDate === today) ?? null; + const weekOf = weekStartFor(today, weekStartsOn); + const question = await resolveWeeklyNudge(signals, weekOf, excludeIds); const birth = me.birthDate && parseBirthDate(me.birthDate) ? new Date(`${me.birthDate}T12:00:00`) @@ -63,15 +81,36 @@ export default async function JournalPage() { return ( <> - ({ + title: dream.title, + category: dream.category, + daysSinceMovement: dream.daysSinceMovement, + })), + weeksRemaining, + initialQuestion: question, + archiveSlot: , + }} + actions={{ + onSave: async (entryWeekOf, text, promptText) => { + 'use server'; + const result = await saveWeeklyLogEntry(entryWeekOf, text, promptText); + return result.saved; + }, + onWeekStartsOnChange: async (value: WeekStartsOn) => { + 'use server'; + await setWeekStartsOn(value); + }, + onCallDreamForward: async (title: string) => { + 'use server'; + await setCallingBucketListDream(title); + }, + }} /> ); diff --git a/src/app/manifesto/page.tsx b/src/app/manifesto/page.tsx index 153bfb5c..303f0011 100644 --- a/src/app/manifesto/page.tsx +++ b/src/app/manifesto/page.tsx @@ -46,9 +46,10 @@ export default function ManifestoPage() {

- Daily. One ritual, twice a day. - Morning prompts, habit check-ins, and a journal entry. The quiet capture of how - you're spending your days. Private by default — no one sees this but you. + Daily. Small practices checked in, + never scored. And once a week — usually a Sunday — one honest question: what did you + actually do with your life this week? The quiet capture of how you're spending your + days. Private by default — no one sees this but you.

@@ -75,8 +76,8 @@ export default function ManifestoPage() {

- The journal is the bridge. You write about practicing your hobby, and that entry - connects your daily practice to your life aspirations. The days become weeks. The weeks + The weekly log is the bridge. You write what you actually lived, and that record + connects your weeks to the life you said you wanted. The days become weeks. The weeks become a life.

diff --git a/src/app/onboarding/onboarding-flow.tsx b/src/app/onboarding/onboarding-flow.tsx index 45fb7ea5..3705b064 100644 --- a/src/app/onboarding/onboarding-flow.tsx +++ b/src/app/onboarding/onboarding-flow.tsx @@ -8,56 +8,35 @@ import { Compass, Footprints, History, - NotebookPen, Plus, Search, Sparkles, - Target, UserRound, } from 'lucide-react'; -import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useEffect, useState, useTransition } from 'react'; import { AmbientMusic } from '~/components/ambient-music'; +import { LifeGrid } from '~/components/life-grid'; import { StorageModeProvider, StorageModeStatus } from '~/components/storage-mode-provider'; import { completeOnboarding } from '~/lib/actions/onboarding'; import { browserRecordAdapter, readLocalRecord, writeLocalRecord } from '~/lib/local-record-store'; import { createLocalTrajectory } from '~/lib/local-trajectory'; import { syncLocalWorkspaceCookie } from '~/lib/local-workspace-cookie'; +import { buildLifeGrid } from '~/lib/mortality'; import type { StorageMode } from '~/lib/storage-mode'; - -type Possibility = { - title: string; - category: - | 'travel' - | 'adventure' - | 'creative' - | 'achievement' - | 'relationships' - | 'contribution' - | 'food' - | 'health' - | 'mindfulness' - | 'reflection'; - emoji: string; -}; - -type Draft = { - version: 2; - step: number; - name: string; - birthDate: string; - pastHobbies: string[]; - pastInput: string; - desiredExperiences: Possibility[]; - desireInput: string; - annualFocus: string; - annualGoals: string[]; - goalInput: string; - habit: string; - trajectoryIntent: string; - trajectoryConstraint: string; -}; +import { + ChoiceChip, + DirectionStep, + Field, + FutureStep, + goalsFromDraft, + InlineAdd, + saveLocalOnboarding, + SelectionList, + Step, + type Draft, + type Possibility, +} from './onboarding-steps'; const pastExamples = ['Drawing', 'Football', 'Gaming', 'Piano', 'Reading', 'Cycling', 'Cooking']; const profiles = [ @@ -91,14 +70,6 @@ function isDraft(value: unknown): value is Draft { return draft.version === 2 && typeof draft.step === 'number' && Array.isArray(draft.pastHobbies); } -function goalsFromDraft(draft: Draft): string[] { - return Array.isArray(draft.annualGoals) && draft.annualGoals.length > 0 - ? draft.annualGoals - : draft.annualFocus - ? [draft.annualFocus] - : []; -} - export function OnboardingFlow({ user, storageMode, @@ -124,6 +95,7 @@ export function OnboardingFlow({ saved ? { ...saved, + step: Math.min(Math.max(0, saved.step), 4), annualGoals: Array.isArray(saved.annualGoals) ? saved.annualGoals : saved.annualFocus @@ -253,59 +225,7 @@ export function OnboardingFlow({ return; } } else { - const adapter = browserRecordAdapter(); - await Promise.all([ - writeLocalRecord(adapter, 'profile:draft', 'profile', { name: draft.name }), - writeLocalRecord(adapter, 'profile:birth-date', 'profile', { birthDate: draft.birthDate }), - writeLocalRecord(adapter, 'onboarding:profile', 'onboarding', { - name: draft.name, - birthDate: draft.birthDate, - pastHobbies: draft.pastHobbies, - desiredExperiences: draft.desiredExperiences, - annualFocus: annualGoals[0] ?? '', - annualGoals, - }), - writeLocalRecord(adapter, 'timeline-draft-new', 'timelines', { - title: 'My life so far', - phases: [ - { - id: `local-earlier-${crypto.randomUUID()}`, - label: 'Earlier chapters', - hobbies: draft.pastHobbies.map((name) => ({ name })), - order: 0, - }, - ], - }), - writeLocalRecord(adapter, 'onboarding:bucket-items', 'bucket-list', { - items: draft.desiredExperiences, - annualFocus: annualGoals[0] ?? '', - annualGoals, - }), - ]); - const daily = (await readLocalRecord(adapter, 'daily:state', 'daily', isDailyState)) ?? { - habits: [], - logs: [], - journals: [], - }; - if ( - draft.habit.trim() && - !daily.habits.some( - (habit) => String(habit.name).toLowerCase() === draft.habit.toLowerCase() - ) - ) { - daily.habits.push({ - id: `local-habit-${crypto.randomUUID()}`, - name: draft.habit, - status: 'active', - targetFrequency: 'daily', - icon: null, - sourceQuestId: null, - commitmentId: null, - }); - } - await writeLocalRecord(adapter, 'daily:state', 'daily', daily); - await createLocalTrajectory(trajectory, adapter).catch(() => undefined); - syncLocalWorkspaceCookie(true); + await saveLocalOnboarding(draft, annualGoals, trajectory); } router.push('/live-more'); router.refresh(); @@ -328,11 +248,13 @@ export function OnboardingFlow({ 'bg-[#211e18]', 'bg-[#b9dcf5]', 'bg-[#ffd0bd]', - 'bg-[#c5abfa]', - 'bg-[#dceabf]', 'bg-[#f7e957]', ]; + const revealGrid = validBirthDate + ? buildLifeGrid(new Date(`${draft.birthDate}T12:00:00`), new Set()) + : null; + const steps = [ Your birth date stays private.

, } - eyebrow="Before the planning" - title="The rest is still unwritten." - copy="You do not need to optimize your life. You only need to notice that it is happening." - action="I’m ready" + eyebrow="The honest math" + title={ + revealGrid + ? `${revealGrid.weeksLived.toLocaleString()} weeks in. About ${revealGrid.weeksRemaining.toLocaleString()} to go.` + : 'The rest is still unwritten.' + } + copy="The average life runs about 4,000 weeks. Your number is not a countdown to fear — it is the reason what you choose next matters." + action="This is why the list matters" onNext={() => setStep(2)} - secondaryAction="Skip this moment" + secondaryAction="Continue" onSecondary={() => setStep(2)} > -
- Friends jumping into a lake together -
-
- “Today is the first day of the rest of your life.” -
-
+ {revealGrid ? ( +
+ +
+ {revealGrid.weeksLived.toLocaleString()} weeks lived + about {revealGrid.weeksRemaining.toLocaleString()} remaining +
+
+ ) : null} , , - } - eyebrow="Your future" - title="What would make you glad you said yes?" - copy="Start with a few sparks, search thousands of paths, or paste the personal list you already carry. Strange and specific is welcome." - action="Keep these possibilities" - onNext={() => setStep(4)} - disabled={!draft.desiredExperiences.length} - > - {draft.desiredExperiences.length ? ( -
- {draft.desiredExperiences.map((possibility) => ( - - ))} -
- ) : null} - -
- {visiblePossibilities.map((possibility) => { - const selected = draft.desiredExperiences.some( - (item) => item.title === possibility.title - ); - return ( - - ); - })} - {!normalizedPossibilityQuery ? ( -
-

This is only the beginning.

-

- Search 5,000+ paths—or add something only you could imagine. The possibilities are - nearly endless. -

-
- ) : null} - {!visiblePossibilities.length ? ( -
-

Nothing in the atlas matches that yet.

-

Add your own possibility below.

-
- ) : null} -
- {matchingPossibilities.length > visiblePossibilities.length ? ( -

- Showing the first {visiblePossibilities.length}. Add another word to narrow the search. -

- ) : null} -
- -

- Add one idea, or paste a numbered list. Each line becomes its own item. -

-