diff --git a/docs/specs/2026-07-15-hyperbole-port-design.md b/docs/specs/2026-07-15-hyperbole-port-design.md new file mode 100644 index 0000000..1ebb22a --- /dev/null +++ b/docs/specs/2026-07-15-hyperbole-port-design.md @@ -0,0 +1,292 @@ +# Hyperbole Port Design + +**Date:** 2026-07-15 +**Status:** Draft + +## Motivation + +The current Sis architecture uses a Haskell JSON API backend (Orb/WAI) with a +TypeScript/Mithril.js SPA frontend. The JavaScript framework has issues with +routing and basic functionality. We want to eliminate JavaScript entirely by +porting to [Hyperbole](https://github.com/seanhess/hyperbole), a Haskell +serverside web framework inspired by HTMX, Elm, and Phoenix LiveView. + +After the port, there will be zero application TypeScript/JavaScript in the +source code. Only Hyperbole's client runtime (~40KB gzipped) runs in the +browser, providing WebSocket connectivity and VirtualDOM patching. + +## Key Decisions + +- **API:** Remove the JSON API entirely. All interactivity happens over + Hyperbole's WebSocket channel with serverside-rendered HTML. +- **Build system:** Keep Stack. Add `hyperbole` and its dependencies to + `package.yaml` and `stack.yaml`. +- **Database:** Full effectful effect system. Create a custom `DB` effect + that wraps all SQLite operations, replacing the current raw + `SQL.Connection` passing. +- **Auth:** Use Hyperbole's built-in session mechanism (cookie-based, stored + in memory). Replaces the current custom cookie/SQLite session system. +- **CSS:** Keep Neo Brutalism CDN + thin custom CSS. Use Hyperbole's + `atomic-css` for inline styles where needed, but primarily use Neo + Brutalism class names. +- **UI:** Match the current UI look. No visual redesign. Make UI decisions + independently when questions arise, leaning on Neo Brutalism defaults. + +## Architecture + +Before: +``` +Browser ←[HTTP GET]→ WAI static serving (SPA index.html + JS) +Browser ←[fetch JSON]→ WAI /api/* routes +Mithril SPA: routing, state, rendering all in TypeScript +``` + +After: +``` +Browser ←[HTTP/WebSocket]→ Warp + Hyperbole + - Initial page: full HTML rendered serverside + - Interactions: WebSocket with VirtualDOM diffs + - All routing, state, rendering in Haskell +``` + +Module structure: +``` +sis/ +├── app/Main.hs # Hyperbole app entry, Warp setup, route dispatch +├── src/ +│ ├── Sis.hs # Top-level re-exports +│ ├── Sis/Route.hs # Route ADT with Route instance +│ ├── Sis/Types.hs # Core domain types (Aeson instances removed) +│ ├── Sis/Database.hs # effectful DB effect, SQLite operations +│ ├── Sis/Auth.hs # Hyperbole session-based auth + password hashing +│ ├── Sis/Page/ +│ │ ├── Login.hs # Login page +│ │ ├── Signup.hs # Signup page +│ │ ├── Dashboard.hs # Dashboard with stats + due/completed items +│ │ ├── Chores.hs # Chore list + create/edit form +│ │ ├── Household.hs # Members, invites, household management +│ │ └── Activity.hs # Activity log with pagination +│ ├── Sis/View/ +│ │ ├── Layout.hs # Shell: document head, navbar, page wrapper +│ │ ├── ChoreForm.hs # Create/edit chore form (HyperView) +│ │ ├── ActivityModal.hs # Record activity form (HyperView) +│ │ └── Field.hs # Reusable form field helpers +│ └── Sis/Style.hs # CSS helpers for Neo Brutalism classes +├── frontend/ +│ └── static/ +│ ├── style.css # Thin custom CSS (nav, bg, animations) +│ └── manifest.json # PWA manifest (unchanged) +├── package.yaml # Updated deps (hyperbole, atomic-css, effectful) +├── stack.yaml # Updated resolver + extra-deps +└── Dockerfile # Updated (no npm build step) +``` + +Deleted: +- `frontend/src/` — all TypeScript code +- `src/Sis/Server.hs` — Orb/WAI routing replaced by Hyperbole pages +- Dependencies: orb, beeline-routing, shrubbery, json-fleece-aeson, json-fleece-core + +## Routes + +Flat route structure mapping to the current pages: + +```haskell +data AppRoute + = RouteHome -- redirects based on auth status + | RouteLogin + | RouteSignup + | RouteDashboard + | RouteChores + | RouteHousehold + | RouteActivity +``` + +The `Route` typeclass generates URLs from constructor names: `/login`, +`/dashboard`, etc. No dynamic route segments needed — all interactions +happen within pages via HyperViews, not separate detail pages. + +Router dispatches each route to a page handler: + +```haskell +router RouteLogin = runPage Sis.Page.Login.page +router RouteSignup = runPage Sis.Page.Signup.page +router RouteDashboard = runPage Sis.Page.Dashboard.page +router RouteChores = runPage Sis.Page.Chores.page +router RouteHousehold = runPage Sis.Page.Household.page +router RouteActivity = runPage Sis.Page.Activity.page +``` + +Auth checking happens inside each protected page via `requireAuth`, which +reads user ID from Hyperbole's session. If not authenticated, redirects to +login. + +## Pages + +### Login Page (`Sis.Page.Login`) + +- **`LoginForm`** HyperView with email, password, remember-me fields +- `Action = SubmitLogin Text Text Bool` +- On success: stores user ID in Hyperbole session, redirects to dashboard +- On failure: re-renders form with error message +- Navbar hidden on this page +- Link to signup via `route RouteSignup` + +### Signup Page (`Sis.Page.Signup`) + +- **`SignupForm`** HyperView with display name, email, password, confirm, agree-terms +- `Action = SubmitSignup Text Text Text Text Bool` +- Validates (password length, match, email uniqueness), creates user, creates session +- Link to login via `route RouteLogin` + +### Dashboard (`Sis.Page.Dashboard`) + +- **Single `DashboardPage` HyperView** wrapping all dashboard content + (stats tiles, due items, completed items). A single HyperView ensures + consistency when state changes across sections. +- `Action = RecordActivity OccurrenceId RecordActivityRequest | Refresh` +- Stat tiles: overdue count (red), due today (yellow), done this week (green) +- Due items list: each item shows chore name, assignee, status badge, and + "Check Off" button +- Clicking "Check Off" replaces the item row with an inline record-activity + form (status dropdown, optional note, notify checkbox, Save/Cancel + buttons) +- Completed items: read-only list of today's activities +- Link to full activity log + +### Chores Page (`Sis.Page.Chores`) + +- **`ChoreList` HyperView** — list of all chores for the household +- `Action ChoreList = DeleteChore ChoreId | StartCreate | StartEdit ChoreId` +- Each chore: name, schedule badge, schedule description, Edit/Delete buttons +- "New Chore" button inserts a **`ChoreForm` HyperView** inline +- `ChoreForm` actions: `Submit {fields} | Cancel` +- On submit, sends `pushEvent` to `ChoreList` to refresh +- Delete asks for confirmation via inline confirmation state in the HyperView + (no JS `confirm()`) + +### Household Page (`Sis.Page.Household`) + +- **`HouseholdPage` HyperView** — member list, invite management +- `Action = CreateInvite | RevokeInvite InviteId | CreateHousehold Text` +- Members list: avatar initials, name, email, role badge +- If user has no households: show "Create Your Household" form +- Owner-only actions: create invite link (shows generated code), revoke invites +- Invite codes displayed as `/invite/` text + +### Activity Log (`Sis.Page.Activity`) + +- **`ActivityLog` HyperView** with pagination state +- `Action = GoToPage Int` +- Each entry: status badge, user name, chore name, date, time, optional note +- Previous/Next pagination buttons with current page indicator +- 20 entries per page + +## Domain Types + +`Sis/Types.hs` keeps all current types but **removes all Aeson instances** +(`ToJSON`, `FromJSON`, `ToJSONKey`, `FromJSONKey`). Types become pure +Haskell records and ADTs. New form data types will be added for Hyperbole +form handling (simple records with field names matching form inputs). + +## Database Effect + +A custom `effectful` effect `DB` replaces raw `SQL.Connection` passing and +direct SQL queries in route handlers: + +```haskell +data DB :: Effect where + -- Auth + FindUserByEmail :: Text -> DB m (Maybe User) + CreateUser :: Text -> Text -> Text -> DB m UserId + -- Households + GetUserHouseholds :: UserId -> DB m [Household] + GetHousehold :: UserId -> HouseholdId -> DB m (Maybe Household) + CreateHousehold :: UserId -> Text -> DB m Household + GetMembers :: HouseholdId -> DB m [Membership] + -- Chores + GetChores :: HouseholdId -> DB m [Chore] + CreateChore :: HouseholdId -> CreateChoreRequest -> DB m Chore + UpdateChore :: ChoreId -> UpdateChoreRequest -> DB m Chore + DeleteChore :: ChoreId -> DB m () + -- Dashboard & Activities + GetDashboard :: HouseholdId -> Day -> DB m Dashboard + GenerateOccurrences :: Chore -> DB m () + RecordActivity :: OccurrenceId -> UserId -> RecordActivityRequest -> DB m Activity + GetActivityLog :: HouseholdId -> Int -> Int -> DB m ActivityLogPage + -- Invites + CreateInvite :: HouseholdId -> Maybe Text -> DB m Invite + GetInvites :: HouseholdId -> DB m [Invite] + RevokeInvite :: InviteId -> DB m () + AcceptInvite :: UserId -> Text -> DB m Household + -- Seed + Seed :: DB m () +``` + +The handler `runDB :: SQL.Connection -> Eff (DB : es) a -> IO (Eff es a)` +runs all DB operations against SQLite. + +## Auth + +Hyperbole's built-in `Session` effect stores per-session data keyed by +cookie: + +- **Login:** Validate credentials, store `userId` in session +- **requireAuth:** Read `userId` from session, redirect to login if absent +- **Logout:** Delete session data +- **Password hashing:** Keep `Sis/Auth.hs` using `crypton`, same as current + +## CSS & Styling + +Neo Brutalism CSS loaded via CDN `` in the document head. Thin custom +CSS (`frontend/static/style.css`) for: + +- CSS variables (colors: red, yellow, green, orange) +- Body background (dot pattern) +- Navbar styling (border, shadow, layout, responsive) +- Modal overlay animation (fade-in) +- List item dividers +- Responsive breakpoints + +Hyperbole views use `atomic-css` combinators for any additional inline +styles. Neo Brutalism class names set via attributes where needed. + +## Implementation Phases + +### Phase 1: Scaffold +- Add `hyperbole`, `atomic-css`, `effectful` to `package.yaml` and `stack.yaml` +- Create `app/Main.hs` with Hyperbole `main`, route definitions, document head +- Strip Aeson instances from `Types.hs` +- Verify a simple page renders + +### Phase 2: Auth + Login/Signup +- Create `Sis/Database.hs` with `DB` effect and SQLite handler +- Implement Hyperbole session-based auth in `Sis/Auth.hs` +- Build Login and Signup pages +- Add `requireAuth` pattern + +### Phase 3: Dashboard +- Build Dashboard page with stats, due items, completed items +- Implement record-activity workflow as inline form + +### Phase 4: Chores +- Build Chore list with create/edit/delete +- Build ChoreForm as nested HyperView + +### Phase 5: Household + Activity Log +- Build Household page with members, invites +- Build Activity Log page with pagination + +### Phase 6: Cleanup +- Delete `frontend/src/` (all TypeScript) +- Remove Orb, beeline, shrubbery, json-fleece deps +- Update scripts, Dockerfile, README.md, AGENTS.md + +## Tests + +Current Haskell tests in `test/Spec.hs` test the API via HTTP. After the +port: +- DB effect operations will be testable in isolation with a test SQLite + connection +- Page rendering can be tested by running Hyperbole pages and checking + output +- Playwright tests (per AGENTS.md agent autonomy) verify end-to-end behavior