You are an advanced assistant specialized in generating Val Town code.
- Ask clarifying questions when requirements are ambiguous
- Provide complete, functional solutions rather than skeleton implementations
- Test your logic against edge cases before presenting the final solution
- Ensure all code follows Val Town's specific platform requirements
- If a section of code that you're working on is getting too complex, consider refactoring it into subcomponents
- Generate code in TypeScript or TSX
- Add appropriate TypeScript types and interfaces for all data structures
- Prefer official SDKs or libraries than writing API calls directly
- Ask the user to supply API or library documentation if you are at all unsure about it
- Never bake in secrets into the code - always use environment variables
- Include comments explaining complex logic (avoid commenting obvious operations)
- Follow modern ES6+ conventions and functional programming practices if possible
- Create web APIs and endpoints
- Handle HTTP requests and responses
- Example structure:
Files that are HTTP triggers have http in their name like foobar.http.tsx
- Run on a schedule
- Use cron expressions for timing
- Example structure:
Files that are Cron triggers have cron in their name like foobar.cron.tsx
- Process incoming emails
- Handle email-based workflows
- Example structure:
Files that are Email triggers have email in their name like foobar.email.tsx
Val Town provides several hosted services and utility functions.
import { blob } from "https://esm.town/v/std/blob";
await blob.setJSON("myKey", { hello: "world" });
let blobDemo = await blob.getJSON("myKey");
let appKeys = await blob.list("app_");
await blob.delete("myKey");
import { sqlite } from "https://esm.town/v/stevekrouse/sqlite";
const TABLE_NAME = 'todo_app_users_2';
// Create table - do this before usage and change table name when modifying schema
await sqlite.execute(`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)`);
// Query data
const result = await sqlite.execute(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [1]);
Note: When changing a SQLite table's schema, change the table's name (e.g., add _2 or _3) to create a fresh table.
import { OpenAI } from "https://esm.town/v/std/openai";
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
messages: [
{ role: "user", content: "Say hello in a creative way" },
],
model: "gpt-4o-mini",
max_tokens: 30,
});
import { email } from "https://esm.town/v/std/email";
// By default emails the owner of the val
await email({
subject: "Hi",
text: "Hi",
html: "<h1>Hi</h1>"
});
Val Town provides several utility functions to help with common project tasks.
Always import utilities with version pins to avoid breaking changes:
import { parseProject, readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts";
For example, in Hono:
// serve all files in frontend/ and shared/
app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*", c => serveFile(c.req.path, import.meta.url));
// Read a file from the project
const fileContent = await readFile("/frontend/index.html", import.meta.url);
This is useful for including info for linking back to a val, ie in "view source" urls:
const projectVal = parseProject(import.meta.url);
console.log(projectVal.username); // Owner of the project
console.log(projectVal.name); // Project name
console.log(projectVal.version); // Version number
console.log(projectVal.branch); // Branch name
console.log(projectVal.links.self.project); // URL to the project page
However, it's extremely importing to note that parseProject and other Standard Library utilities ONLY RUN ON THE SERVER.
If you need access to this data on the client, run it in the server and pass it to the client by splicing it into the HTML page
or by making an API request for it.
- Redirects: Use
return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }})instead ofResponse.redirectwhich is broken - Images: Avoid external images or base64 images. Use emojis, unicode symbols, or icon fonts/libraries instead
- AI Image: To inline generate an AI image use:
<img src="https://maxm-imggenurl.web.val.run/the-description-of-your-image" /> - Storage: DO NOT use the Deno KV module for storage
- Browser APIs: DO NOT use the
alert(),prompt(), orconfirm()methods - Weather Data: Use open-meteo for weather data (doesn't require API keys) unless otherwise specified
- View Source: Add a view source link by importing & using
import.meta.url.replace("ems.sh", "val.town)"(or passing this data to the client) and includetarget="_top"attribute - Error Debugging: Add
<script src="https://esm.town/v/std/catch"></script>to HTML to capture client-side errors - Error Handling: Only use try...catch when there's a clear local resolution; Avoid catches that merely log or return 500s. Let errors bubble up with full context
- Environment Variables: Use
Deno.env.get('keyname')when you need to, but generally prefer APIs that don't require keys - Imports: Use
https://esm.shfor npm and Deno dependencies to ensure compatibility on server and browser - Storage Strategy: Only use backend storage if explicitly required; prefer simple static client-side sites
- React Configuration: When using React libraries, pin versions with
?deps=react@18.2.0,react-dom@18.2.0and start the file with/** @jsxImportSource https://esm.sh/react@18.2.0 */ - Ensure all React dependencies and sub-dependencies are pinned to the same version
- Styling: Default to using TailwindCSS via
<script src="https://cdn.twind.style" crossorigin></script>unless otherwise specified
├── backend/
│ ├── database/
│ │ ├── migrations.ts # Schema definitions
│ │ ├── queries.ts # DB query functions
│ │ └── README.md
│ └── routes/ # Route modules
│ ├── [route].ts
│ └── static.ts # Static file serving
│ ├── index.ts # Main entry point
│ └── README.md
├── frontend/
│ ├── components/
│ │ ├── App.tsx
│ │ └── [Component].tsx
│ ├── favicon.svg
│ ├── index.html # Main HTML template
│ ├── index.tsx # Frontend JS entry point
│ ├── README.md
│ └── style.css
├── README.md
└── shared/
├── README.md
└── utils.ts # Shared types and functions
- Hono is the recommended API framework
- Main entry point should be
backend/index.ts - Static asset serving: Use the utility functions to read and serve project files:
import { readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts"; // serve all files in frontend/ and shared/ app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url)); app.get("/shared/*", c => serveFile(c.req.path, import.meta.url)); // For index.html, often you'll want to bootstrap with initial data app.get("/", async c => { let html = await readFile("/frontend/index.html", import.meta.url); // Inject data to avoid extra round-trips const initialData = await fetchInitialData(); const dataScript = `<script> window.__INITIAL_DATA__ = ${JSON.stringify(initialData)}; </script>`; html = html.replace("</head>", `${dataScript}</head>`); return c.html(html); });
- Create RESTful API routes for CRUD operations
- Always include this snippet at the top-level Hono app to re-throwing errors to see full stack traces:
// Unwrap Hono errors to see original error details app.onError((err, c) => { throw err; });
- Run migrations on startup or comment out for performance
- Change table names when modifying schemas rather than altering
- Export clear query functions with proper TypeScript typing
-
Environment Limitations:
- Val Town runs on Deno in a serverless context, not Node.js
- Code in
shared/must work in both frontend and backend environments - Cannot use
Denokeyword in shared code - Use
https://esm.shfor imports that work in both environments
-
SQLite Peculiarities:
- Limited support for ALTER TABLE operations
- Create new tables with updated schemas and copy data when needed
- Always run table creation before querying
-
React Configuration:
- All React dependencies must be pinned to 18.2.0
- Always include
@jsxImportSource https://esm.sh/react@18.2.0at the top of React files - Rendering issues often come from mismatched React versions
-
File Handling:
- Val Town only supports text files, not binary
- Use the provided utilities to read files across branches and forks
- For files in the project, use
readFilehelpers
-
API Design:
fetchhandler is the entry point for HTTP vals- Run the Hono app with
export default app.fetch // This is the entry point for HTTP vals
- Use the relevant log level based on what you're logging.
- Import
https://www.val.town/x/cricks_unmixed4u/logger/code/logger/main.tsxand uselogInfo,logErrororlogDebug.
Updated pseudo-PRD with “today-only” constraint.
Activity Log MVP – “Today-only form-first tracking”
You log activities with a fixed schema (date, time, mood with score, overstimulation level, “must” activity, pleasant/relaxing activity). You want a tiny app that:
- Always works on “today”
- Makes time-range entry extremely easy for the next activity block
- Lets you refine the structure over time.
-
Fast capture of activity rows for today
- No date picker, no navigation.
- The app is implicitly “today’s log”.
-
Respect the existing schema
-
For each row, capture:
date(implicit: today)time rangemood (with numeric rating)overstimulation levelmust activitypleasant/relaxing activity
-
-
Support both free-text and structured entry
- Free-text description → optional LLM assist to fill fields.
- Full manual control over all fields before saving.
-
Simple persistence + today view
- Store all rows in a simple store.
- Show all entries for today in a table that mirrors the activities form.
-
Be easy to extend later
-
Clear separation between:
- Storage
- LLM parsing
- UI
-
-
User: you, logging only for today.
-
Usage pattern:
- Multiple short entry bursts during today.
- Some retrospective entries within the same day (e.g. “what happened from 08:30–09:15?”).
Out of scope for MVP: viewing or editing past days.
-
App loads with:
- A “New entry for today” panel.
- A table showing all entries already recorded for today.
Date is implied and not editable.
In the “New entry” panel:
- Time range selection (always for today)
The app knows the latest endTime of today’s entries (if any).
Time range modes:
-
Mode A – From last end → now
- Available if at least one previous entry exists today.
- Default mode.
- Start = last entry’s end time (read-only by default).
- End = “now” (auto-filled, but can be overridden).
- One click to accept this range.
-
Mode B – From custom start → now
- For retroactive blocks earlier today.
- User inputs
startTime(e.g.08:30). - End is auto-set to “now” (editable if needed).
-
Mode C – From custom start → custom end
- For explicit blocks within today (e.g.
09:15–10:00). - User inputs both
startTimeandendTime.
- For explicit blocks within today (e.g.
First entry of the day:
- No “last end”; default to Mode B or C with empty time fields.
- You fill the first block explicitly.
- Content entry
Two ways to fill the content columns:
-
Structured fields
Mood with score(single text field, must contain “(x/10)”).Overstimulation level(e.g. free text or low/medium/high).Must activity(text,"-"allowed).Pleasant/relaxing activity(text,"-"allowed).
- Save
Before saving:
- Validate that
startTime < endTime. - Validate that the mood includes a numeric score pattern like
(6/10).
On success:
- Store the entry with
date = today. - Append it to today’s table.
- Update “last end” to this entry’s
endTime.
- A table below the input panel:
Header:
Time | Mood (with rating) | Overstimulation level | Must activity | Pleasant/relaxing activity
-
Each row shows:
TimeasHH:mm–HH:mm- The four content columns.
No navigation to other dates; only today is visible.
Per entry:
id(string or numeric)date(ISO date string, always today in this MVP)startTime("HH:mm")endTime("HH:mm")moodWithScore(string, contains “(n/10)”)overstimulationLevel(string)mustActivity(string,"-"allowed)pleasantActivity(string,"-"allowed)rawInput(optional string; free-text source, if used)createdAt(timestamp)
- HTTP endpoints
-
GET /-
Renders HTML for:
- New-entry panel (today-only).
- Today’s table.
-
-
GET /entries/today- Returns today’s entries as JSON.
-
POST /parse- Input:
{ rawText, startTime?, endTime? }(today is implied). - Output: suggested
moodWithScore,overstimulationLevel,mustActivity,pleasantActivity.
- Input:
-
POST /entries- Input: full entry payload (except
datewhich is set to today on the server).
- Input: full entry payload (except
- Storage
- vltable / JSON keyed by date and time.
- Query: “entries where date = today, sorted by
startTime”.
- LLM helperG
-
Function
parseActivityEntry(rawText, startTime, endTime):- Uses activities-form prompt.
- Returns the four content fields in a strict schema.
- No date navigation or historical views.
- No multi-user support.
- No analytics or charts.
- No complex editing or versioning (append-only for today in MVP).
-
UI scaffolding (Step 1) Build basic UI for creating and listing entries, persisting data in
localStorage. Minimal Tailwind CSS styling, no backend yet. -
Faster data entry (Step 2) Use tap-friendly buttons for
mood(1–5) andoverstimulation(1–3) with no default selection, optimized for mobile. -
Versioned ratings with Zod (Step 3)
- Introduce a versioned
RatingZod schema (dimension,min/max/step,value, optionaluiandnote). - Define an
EntryZod schema using canonical numeric scores plus optional rating objects. - Have the form work with rating objects as primary state; derive numeric scores from them.
- Validate entries with Zod before saving and when loading from
localStorage, including basic migrations. - Keep rating schemas versioned and future-proof.
- Introduce a versioned
-
Shared Zod models (Step 4.1) Move
ratingSchemaandentrySchemainto a shared module used by both frontend and backend so data model and types are defined in one place. -
Database schema extensions (Step 4.2) Extend the database with numeric score columns and JSON columns for rating payloads, keeping existing time/activity fields as-is.
-
Row ↔ Entry mapping (Step 4.3) Define and implement a clear mapping between DB columns and
Entryfields, via helpers likedbRowToEntryandentryToDbParams, so API handlers useEntryobjects only. -
API input validation (Step 4.4) Validate
POST /entrieswith Zod input schema, fillid/createdAton the backend, and ensure rating objects are trusted, with scores derived from them. -
Persist data in backend (Step 5) Start saving entries to the backend database, using a simple user identifier stored in
localStorage(no authentication yet). -
Wake-up check-in (Step 6) On days with no entries yet, show a dedicated “wake-up check-in” flow first. Otherwise, show the normal timeline and new-entry form. Encapsulate this in a helper like
shouldShowWakeCheckIn. -
Refactor for wake block config + simpler time input (Steps 7–8 + early part of 9)
- Introduce
DEFAULT_WAKE_BLOCK_MINUTESused for converting wake-up time into a time range. - Simplify time entry: remove explicit “time mode” concept; always show editable
startTime/endTimewith smart defaults based on the last entry and “now”. - Interpret edits as implicit modes (from last end → now, custom start → now, or custom range).
Add a small static module moodWords.ts (frontend only).
Define four buckets (2×2 model – energy × pleasantness):
highEnergyPleasant: string[]
highEnergyUnpleasant: string[]
lowEnergyPleasant: string[]
lowEnergyUnpleasant: string[]
Keep each list small for MVP (e.g. 6–10 words per bucket).
Ensure all words are simple, everyday language (no jargon).
Add a tiny “Help me describe this” link/button next to the “Mood description” input.
When pressed, open a lightweight helper UI:
Could be inline below the field, not a full-screen dialog (MVP).
Show a simple 2×2 layout, text-only, no graphics needed:
Row labels: High energy, Low energy.
Column labels: Unpleasant, Pleasant.
Under each quadrant label, display a few chips/buttons from the corresponding list:
Example layout:
High energy / Pleasant → chips: “excited”, “curious”, …
High energy / Unpleasant → chips: “angry”, “overwhelmed”, …
Low energy / Pleasant → chips: “calm”, “relieved”, …
Low energy / Unpleasant → chips: “tired”, “sad”, …
On tap of a word chip:
If mood description is empty → set it to that word.
If mood description already has content → append with a comma and space.
Do not auto-close the helper; allow multiple taps.
Add a small “Done” / “Close” link under the helper to hide it.
Add a small helper function to suggest a quadrant based on existing numeric scores:
Use moodScore (pleasant–unpleasant axis).
Use overstimulationScore as a crude energy proxy (low–high).
Use this only to:
Option A (simplest): visually emphasise one quadrant (e.g. show it first or bold header).
Option B (if simpler): do nothing for now; just keep helper independent of scores.
Do not make behavior dependent on this in v1; keep it purely as a hint or future hook.
Ensure mood description field already flows into:
Entry.moodDescription.
moodRating.note.
No additional persistence needed for the helper; it just writes into the existing field.
Keep the helper visually light:
No extra navigation.
Minimal text; mostly labels + chips.
Make all chips large, touch-friendly targets.
11.1 Selection model • Add state: selectedEntryId: string | null for the “New Entry / Edit Entry” panel. • Define two modes in code: "create" vs "edit" (string union or similar). • Derive current mode from selectedEntryId (null → create, non-null → edit).
⸻
11.2 Make today’s entries tappable • Wrap each row (or main cell) in the “Today’s log” list/table in a click/tap handler. • On row click: • Set selectedEntryId to that entry’s id. • Scroll/focus the form at the top if needed (optional).
⸻
11.3 Prefill form when editing • When selectedEntryId changes: • Look up the matching Entry in todayEntries. • Prefill: • startTime / endTime • moodRating (from entry.moodRating) • overstimulationRating • moodDescription • mustActivity • pleasantActivity • Temporarily disable defaulting logic (startTime = lastEndTime, endTime = now) when in edit mode.
⸻
11.4 Adjust form labels + actions • When selectedEntryId === null: • Show title: “New entry for today”. • Primary button text: “Save entry”. • When selectedEntryId !== null: • Show title: “Edit entry”. • Primary button text: “Save changes”. • Add secondary button: “Cancel editing”.
⸻
11.5 Implement “save changes” path • On submit in edit mode: • Run the same validation as for create (time, ratings, etc.). • Build a new Entry object by: • Copying the existing entry’s id, createdAt, date. • Replacing editable fields from the form. • Replace this entry in todayEntries (immutable update). • Persist the updated full list via the existing persistence layer. • Re-sort todayEntries by startTime. • Clear selectedEntryId back to null and reset form to “create” mode (with default start/end logic).
⸻
11.6 Implement “cancel editing” • On “Cancel editing”: • Clear selectedEntryId. • Clear form fields. • Re-apply default times for the “new entry” flow (start = lastEndTime, end = now). • Ensure mood/overstimulation/buttons are reset to unselected.
⸻
11.7 Visual indication of selected row • In the “Today’s log” list, when a row’s id === selectedEntryId: • Apply a subtle highlight (background/border). • Optional small badge, e.g. “Editing…”.
⸻
11.8 First-entry / wake-up special case • Confirm editing the wake-up entry works like any other row: • Tapping the first row should prefill form with wake-up block data. • Saving changes should update that row normally. • No special logic for wake-up in edit mode (MVP).
⸻
11.9 Keep behavior scoped to “today” • Only allow editing entries from today (no history yet). • Ensure todayEntries remains the single source of entries for edit operations.
-
Add
WEEK_START_DAY = "monday"constant (e.g. indateUtils.ts). -
Implement
getWeekStart(date: Date): Date:- Return the Monday of the week containing
date.
- Return the Monday of the week containing
-
Implement
getWeekEnd(weekStart: Date): Date:- Return
weekStart + 6 days(Sunday).
- Return
-
Implement
formatDate(d: Date): string→"YYYY-MM-DD".
-
Add a new “Week overview” route/view/component (e.g.
WeekOverview). -
On the main (today) page, at the very bottom:
- Add a small link/button: “View this week”.
- Clicking it navigates to the week overview.
-
On the week overview page:
- Add a “Back to today” link/button that returns to the main page.
-
Add persistence function:
-
loadEntriesForDateRange(startDate: string, endDate: string): Promise<Entry[]>.
-
-
LocalStorage implementation:
- Load all stored entries.
- Filter where
date >= startDate && date <= endDate. - Sort by
date, thenstartTime.
-
In
WeekOverview:-
On mount:
-
today = new Date(). -
weekStart = getWeekStart(today). -
weekEnd = getWeekEnd(weekStart). -
weekStartDate = formatDate(weekStart). -
weekEndDate = formatDate(weekEnd). - Call
loadEntriesForDateRange(weekStartDate, weekEndDate)and store inweekEntries.
-
-
-
Add helper:
groupEntriesByDate(entries: Entry[]): Record<string, Entry[]>.- Key =
date("YYYY-MM-DD"). - Value = entries for that date, sorted by
startTime.
- Key =
-
In
WeekOverview, compute:-
entriesByDate = groupEntriesByDate(weekEntries).
-
-
For each day from Monday to Sunday:
-
Compute
dayDate = weekStart + offsetDays(0..6). -
dayKey = formatDate(dayDate). -
Render:
-
Header:
"<Weekday name> – YYYY-MM-DD". -
If
entriesByDate[dayKey]is empty:- Show “No entries”.
-
Else:
-
Render a simple list/table with:
- Time:
startTime–endTime. - Mood:
moodScore+moodDescription. - Overstimulation:
overstimulationScore. - Must activity.
- Pleasant activity.
- Time:
-
-
-
-
Make page strictly read-only:
- No tap-to-edit in week view.
- No new entries from this page.
- Keep styling minimal (reuse existing Tailwind spacing/typography).
- Ensure clear separation between days (spacing or subtle divider).
- Keep scroll performance simple: one vertical scroll listing Monday → Sunday.
A very small step to start adding users.
-
Update
createTablesto createactivity_log_users_v1:- Columns:
id(PK),created_at,last_seen_at.
- Columns:
-
Update
createTablesto createactivity_log_passphrase_credentials_v1:- Columns:
id(PK),user_id(FK →activity_log_users_v1.id),passphrase_hash,created_at. - Add unique constraint on
passphrase_hash.
- Columns:
-
Run
createTablesagainst a fresh dev database and verify both tables exist.
- Create
backend/queries/users/index.tsif it does not exist. - Import your DB client / query helper (same pattern as
backend/entries/...files). - Import or define the
ActivityLogUserV1row type (matchingactivity_log_users_v1).
- Define a TypeScript type for users (if not already defined), e.g.
ActivityLogUserV1. - Add a small
mapRowToUser(row: any): ActivityLogUserV1helper if your stack usually does mapping, otherwise skip.
-
Export function
createActivityLogUserV1(): Promise<ActivityLogUserV1>. -
Inside:
-
Generate a new
id(same pattern as other tables). -
Insert into
activity_log_users_v1:-
id -
created_at = now -
last_seen_at = null
-
-
Return the inserted row as
ActivityLogUserV1.
-
- Export function
getActivityLogUserV1ById(id: string): Promise<ActivityLogUserV1 | null>. - Query
activity_log_users_v1whereid = $1. - If no row: return
null. - If row: return it as
ActivityLogUserV1.
-
Export function
touchActivityLogUserV1LastSeen(id: string): Promise<void>. -
Update
activity_log_users_v1:-
SET last_seen_at = nowwhereid = $1.
-
-
Do not throw if zero rows updated (MVP); just no-op.
If you find it useful later:
-
Export helper
ensureActivityLogUserV1(id: string): Promise<ActivityLogUserV1>. -
Implementation idea:
- Call
getActivityLogUserV1ById(id). - If found: return it.
- If not: throw a clear error (“user not found”).
- Call
-
Add a small test suite (wherever your backend tests live) that:
- Calls
createActivityLogUserV1and asserts returnedidandcreated_at. - Calls
getActivityLogUserV1ByIdwith thatidand asserts it returns the same user. - Calls
touchActivityLogUserV1LastSeenand checkslast_seen_atis updated.
- Calls
IN PROGRESS - Step 15 - Map an existing user_id to a passphrase, without changing the user_id model.
-
Add a short comment in the backend (e.g. near
activity_log_entries_v*model) stating:-
user_idis:- Generated when the user first starts using the app.
- Stored in localStorage under
activity_log_user_id. - Used in all
activity_log_entries_v*rows.
-
-
Add a comment in
activity_log_users_v1model:-
idmust always match an existinguser_idused in entries / localStorage (no separate ID space).
-
-
Update
createActivityLogUserV1(or add a variant) so it can accept an explicitid:- New function:
createActivityLogUserV1WithId(id: string). - It inserts into
activity_log_users_v1using the providedid(no new ID generation).
- New function:
-
Add helper:
ensureActivityLogUserV1ForId(id: string):- Try
getActivityLogUserV1ById(id). - If found → return.
- If not found → call
createActivityLogUserV1WithId(id)and return.
- Try
-
Create
backend/queries/passphraseCredentials/index.ts. -
Add:
-
createPassphraseCredentialV1(userId: string, hash: string). -
findPassphraseCredentialByHashV1(hash: string).
-
-
Ensure
activity_log_passphrase_credentials_v1.user_idis a FK toactivity_log_users_v1.id.
Goal: bind passphrase to current user_id (no new user created).
-
Add
POST /api/activity-log/passphrase/set. -
Request body schema:
{ userId: string, passphrase: string }. -
Handler steps:
-
Validate body (Zod, etc.).
-
Compute
hash = hashPassphrase(passphrase). -
Call
findPassphraseCredentialByHashV1(hash):- If exists and
credential.user_id !== userId→ return409(PASSPHRASE_ALREADY_IN_USE_BY_OTHER_USER).
- If exists and
-
Call
ensureActivityLogUserV1ForId(userId):- This guarantees a row in
activity_log_users_v1with thatid.
- This guarantees a row in
-
If no credential exists for this hash:
- Call
createPassphraseCredentialV1(userId, hash).
- Call
-
Return
{ userId }.
-
(So: passphrase is now mapped to the existing user_id.)
Goal: new device → passphrase → existing user_id.
-
Add
POST /api/activity-log/passphrase/login. -
Request body:
{ passphrase: string }. -
Handler steps:
- Compute
hash = hashPassphrase(passphrase). - Call
findPassphraseCredentialByHashV1(hash). - If not found → return
401(INVALID_PASSPHRASE). - If found → return
{ userId: credential.user_id }.
- Compute
-
Confirm current behavior:
- On first use, app generates an ID (e.g. UUID).
- Saves it to
localStorage["activity_log_user_id"]. - Sends it as
user_idfor all entry writes.
-
Do not change this behavior.
-
On app init:
- Read
activity_log_user_idfrom localStorage. - Keep it in state:
currentUserId.
- Read
-
In the “Sync with a passphrase” UI:
-
Require
currentUserIdto exist (if not, generate it first, like normal). -
On “Set passphrase” click:
- Read
passphrasefrom input. - Call
POST /api/activity-log/passphrase/setwith{ userId: currentUserId, passphrase }.
- Read
-
-
On success:
- Keep
activity_log_user_idas-is (no change). - Optionally mark in local state that passphrase is configured (e.g.
hasPassphrase = true).
- Keep
-
On error:
-
If
PASSPHRASE_ALREADY_IN_USE_BY_OTHER_USER, show clear message:- “This passphrase is already connected to another person’s data. Please choose a different one.”
-
Handle generic failures as “Could not set passphrase. Please try again.”
-
-
On a fresh device (no
activity_log_user_idin localStorage):- Show a “Use existing passphrase” section.
-
On “Use existing passphrase”:
-
Read
passphrase. -
Call
POST /api/activity-log/passphrase/login. -
On success:
- Take
userIdfrom response. - Save it to
localStorage["activity_log_user_id"]. - Set
currentUserIdstate to thatuserId. - Load today/week entries for this
userId(same repos as now).
- Take
-
On
INVALID_PASSPHRASE:- Show: “Passphrase not recognized. Check spelling or try a different one.”
-
-
Confirm:
-
activity_log_entries_v*continues to useuser_idexactly as before. - Day/week queries already filter by
user_id(or update them if needed).
-
-
Verify:
- Device A: existing user → set passphrase.
- Device B: enter same passphrase → receive same
user_id→ see same entries.
There are