628 lines
21 KiB
Markdown
628 lines
21 KiB
Markdown
# Hyperbole Port Implementation Plan
|
|
|
|
**Goal:** Port Sis from Orb+WAI+TypeScript+Mithril to Hyperbole (pure Haskell serverside web framework), eliminating all JavaScript application code.
|
|
|
|
**Architecture:** Single Haskell Hyperbole application. Pages rendered serverside. Interactivity via HyperViews with WebSocket-based VirtualDOM. SQLite via custom effectful `DB` effect. Neo Brutalism CSS via CDN.
|
|
|
|
**Tech Stack:** Hyperbole, effectful, atomic-css, Warp, SQLite (sqlite-simple), crypton.
|
|
|
|
---
|
|
|
|
## File Structure Map
|
|
|
|
| File | Action | Responsibility |
|
|
|------|--------|----------------|
|
|
| `package.yaml` | Modify | Replace Orb/Mithril deps with Hyperbole |
|
|
| `stack.yaml` | Modify | Add Hyperbole extra-deps |
|
|
| `app/Main.hs` | Rewrite | Hyperbole app entry, Warp, route dispatch |
|
|
| `src/Sis.hs` | Modify | Re-exports (remove Server, add Page modules) |
|
|
| `src/Sis/Route.hs` | Create | Route ADT with `Route` instance |
|
|
| `src/Sis/Types.hs` | Modify | Remove Aeson instances, add form types |
|
|
| `src/Sis/Database.hs` | Rewrite | effectful `DB` effect + SQLite handler |
|
|
| `src/Sis/Auth.hs` | Modify | Keep password hashing, drop cookie/session helpers |
|
|
| `src/Sis/Server.hs` | Delete | Replaced by Hyperbole pages |
|
|
| `src/Sis/Page/Login.hs` | Create | Login page |
|
|
| `src/Sis/Page/Signup.hs` | Create | Signup page |
|
|
| `src/Sis/Page/Dashboard.hs` | Create | Dashboard page |
|
|
| `src/Sis/Page/Chores.hs` | Create | Chores page |
|
|
| `src/Sis/Page/Household.hs` | Create | Household page |
|
|
| `src/Sis/Page/Activity.hs` | Create | Activity log page |
|
|
| `src/Sis/View/Layout.hs` | Create | Shell: document head, navbar, page wrapper |
|
|
| `src/Sis/View/ChoreForm.hs` | Create | Create/edit chore HyperView |
|
|
| `src/Sis/View/ActivityForm.hs` | Create | Record activity HyperView |
|
|
| `src/Sis/View/Field.hs` | Create | Reusable form field helpers |
|
|
| `src/Sis/Style.hs` | Create | Neo Brutalism class helpers |
|
|
| `test/Spec.hs` | Modify | Delete placeholder |
|
|
| `frontend/src/` | Delete | All TypeScript code |
|
|
| `frontend/index.html` | Delete | Replaced by Hyperbole document function |
|
|
| `frontend/package.json` | Delete | No npm needed |
|
|
| `frontend/tsconfig.json` | Delete | No TypeScript needed |
|
|
| `frontend/public/` | Delete | Merge into static/ |
|
|
| `frontend/static/style.css` | Create/Keep | Move from public/style.css |
|
|
| `frontend/static/manifest.json` | Create/Keep | Move from public/manifest.json |
|
|
| `Dockerfile` | Modify | No npm build, copy binary + static |
|
|
| `scripts/build` | Modify | Remove npm build step |
|
|
| `scripts/test` | Modify | Just Haskell build + test |
|
|
| `scripts/run` | Modify | Updated for Hyperbole serving |
|
|
| `README.md` | Modify | Update tech stack description |
|
|
| `AGENTS.md` | Modify | Update conventions |
|
|
|
|
---
|
|
|
|
### Task 1: Add Hyperbole Dependencies
|
|
|
|
**Files:** `package.yaml`, `stack.yaml`
|
|
|
|
- [ ] **Step 1: Update package.yaml**
|
|
|
|
Replace deps section. Key changes:
|
|
- Add `DataKinds`, `TypeFamilies` to default-extensions
|
|
- Shared deps: remove `beeline-routing`, `json-fleece-aeson`, `json-fleece-core`, `mtl`, `optparse-applicative`, `safe-exceptions`, `shrubbery`, `wai-extra`; add `effectful`
|
|
- Library deps: remove `cookie`, `orb`; add `atomic-css`, `data-default`, `effectful-core`, `hyperbole`, `string-conversions`, `text-casing`
|
|
- Executable deps: remove `optparse-applicative`, `orb`, `unix`; add `effectful`, `hyperbole`
|
|
|
|
- [ ] **Step 2: Run hpack**
|
|
|
|
```bash
|
|
./hs hpack
|
|
```
|
|
|
|
- [ ] **Step 3: Update stack.yaml**
|
|
|
|
Replace with:
|
|
|
|
```yaml
|
|
resolver: lts-24.38
|
|
|
|
packages:
|
|
- .
|
|
|
|
extra-deps:
|
|
- hyperbole-1.0.0.0
|
|
- atomic-css-0.1.0.0
|
|
- effectful-2.3.0.0
|
|
- effectful-core-2.3.0.0
|
|
- string-conversions-0.4.0.1
|
|
- text-casing-0.1.0.3
|
|
- data-default-0.7.1.2
|
|
```
|
|
|
|
- [ ] **Step 4: Verify dependency resolution**
|
|
|
|
```bash
|
|
./hs stack build --dry-run 2>&1 | tail -30
|
|
```
|
|
|
|
Expected: Shows build plan. Adjust resolver or extra-dep versions as needed.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add package.yaml stack.yaml sis-server.cabal
|
|
git commit -m "deps: add hyperbole, effectful, atomic-css; remove orb/mithril deps"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Strip Aeson from Types, Add Form Types
|
|
|
|
**Files:** `src/Sis/Types.hs`
|
|
|
|
- [ ] **Step 1: Remove all Aeson instances**
|
|
|
|
In `src/Sis/Types.hs`:
|
|
- Remove Aeson import and all `instance A.ToJSON`/`instance A.FromJSON` blocks
|
|
- Remove `A.ToJSON`, `A.FromJSON`, `A.ToJSONKey`, `A.FromJSONKey` from newtype deriving
|
|
- Remove request types: `SignupRequest`, `LoginRequest`, `CreateHouseholdRequest`, `CreateInviteRequest`, `CreateChoreRequest`, `UpdateChoreRequest`, `RecordActivityRequest`, `ErrorResponse`, `SeedRequest`, `AuthResponse`, `UserPublic`
|
|
- Keep all core domain types
|
|
|
|
- [ ] **Step 2: Add Hyperbole form types at end of file**
|
|
|
|
```haskell
|
|
import GHC.Generics (Generic)
|
|
import Web.Hyperbole.HyperView.Forms (FromForm (..))
|
|
|
|
data LoginForm = LoginForm
|
|
{ lfEmail :: Text, lfPassword :: Text, lfRemember :: Bool }
|
|
deriving stock (Show, Eq, Generic)
|
|
deriving (FromForm) via GenericForm LoginForm
|
|
|
|
data SignupForm = SignupForm
|
|
{ sfDisplayName :: Text, sfEmail :: Text, sfPassword :: Text
|
|
, sfConfirm :: Text, sfAgree :: Bool }
|
|
deriving stock (Show, Eq, Generic)
|
|
deriving (FromForm) via GenericForm SignupForm
|
|
|
|
data ChoreFormData = ChoreFormData
|
|
{ cfdName :: Text, cfdScheduleType :: Text, cfdStartDate :: Text
|
|
, cfdTimeOfDay :: Maybe Text, cfdPeriod :: Text
|
|
, cfdAssignee :: Text, cfdNotify :: Bool }
|
|
deriving stock (Show, Eq, Generic)
|
|
deriving (FromForm) via GenericForm ChoreFormData
|
|
|
|
data ActivityFormData = ActivityFormData
|
|
{ afdStatus :: Text, afdNote :: Maybe Text, afdNotify :: Bool }
|
|
deriving stock (Show, Eq, Generic)
|
|
deriving (FromForm) via GenericForm ActivityFormData
|
|
|
|
data HouseholdFormData = HouseholdFormData
|
|
{ hfdName :: Text }
|
|
deriving stock (Show, Eq, Generic)
|
|
deriving (FromForm) via GenericForm HouseholdFormData
|
|
```
|
|
|
|
- [ ] **Step 3: Verify Types.hs compiles**
|
|
|
|
```bash
|
|
./hs stack build 2>&1 | tail -20
|
|
```
|
|
|
|
Expected: Types.hs compiles. Other modules will fail.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Types.hs
|
|
git commit -m "refactor: strip Aeson instances from Types; add Hyperbole form types"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Database Effect
|
|
|
|
**Files:** `src/Sis/Database.hs` (rewrite)
|
|
|
|
Rewrite `src/Sis/Database.hs` to define a GADT `DB` effect with these constructors:
|
|
|
|
```haskell
|
|
data DB :: Effect where
|
|
FindUserByEmail :: Text -> DB m (Maybe User)
|
|
CreateUser :: Text -> Text -> Text -> DB m UserId
|
|
GetUser :: UserId -> DB m (Maybe User)
|
|
GetUserHouseholds :: UserId -> DB m [Household]
|
|
GetHousehold :: UserId -> Int -> DB m (Maybe Household)
|
|
CreateHousehold :: UserId -> Text -> DB m Household
|
|
GetMembers :: Int -> DB m [Membership]
|
|
GetChores :: Int -> DB m [Chore]
|
|
CreateChore :: Int -> Text -> ChoreAssignee -> Schedule -> Bool -> DB m Chore
|
|
UpdateChore :: Int -> Int -> Text -> ChoreAssignee -> Schedule -> Bool -> DB m Chore
|
|
DeleteChore :: Int -> DB m ()
|
|
GetDashboard :: Int -> Day -> DB m Dashboard
|
|
GenerateOccurrences :: Chore -> DB m ()
|
|
RecordActivity :: Int -> UserId -> ActivityStatus -> Maybe Text -> Bool -> DB m Activity
|
|
GetActivityLog :: Int -> Int -> Int -> DB m ActivityLogPage
|
|
CreateInvite :: Int -> Maybe Text -> DB m Invite
|
|
GetInvites :: Int -> DB m [Invite]
|
|
RevokeInvite :: Int -> DB m ()
|
|
AcceptInvite :: UserId -> Text -> DB m Household
|
|
Seed :: DB m ()
|
|
```
|
|
|
|
Implement `runDB :: SQL.Connection -> Eff (DB : es) a -> Eff es a` using `interpret`, with each constructor running the same SQL queries currently in `Server.hs`. Export `openAndRunDB` which opens the connection (with WAL + FK + migrations), runs the effect, and closes. Keep `runMigrations` almost unchanged.
|
|
|
|
Add `Show` + `Read` instances to `Schedule` and `SchedulePeriod` (in Types.hs) for storage as strings. Include helper functions `generateOccurrencesIO`, `generateRecurringDates`, `hashPasswordIO`, `generateTokenIO` as local helpers.
|
|
|
|
- [ ] **Step 1: Write the rewrite**
|
|
|
|
```bash
|
|
# The file should be ~400 lines, covering all DB operations
|
|
```
|
|
|
|
- [ ] **Step 2: Update Sis.hs exports**
|
|
|
|
Remove `import Sis.Server as X` from `src/Sis.hs`.
|
|
|
|
- [ ] **Step 3: Verify it compiles**
|
|
|
|
```bash
|
|
./hs stack build 2>&1 | tail -20
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Database.hs src/Sis.hs src/Sis/Types.hs
|
|
git commit -m "refactor: convert Database to effectful DB effect"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Auth Module Update + Delete Server.hs
|
|
|
|
**Files:** `src/Sis/Auth.hs` (modify), `src/Sis/Server.hs` (delete)
|
|
|
|
- [ ] **Step 1: Simplify Auth.hs**
|
|
|
|
Remove cookie/session helpers (`makeSessionCookie`, `clearSessionCookie`, `sessionCookieName`). Keep only:
|
|
|
|
```haskell
|
|
module Sis.Auth (hashPassword, verifyPassword, generateToken) where
|
|
```
|
|
|
|
Remove HTTP imports. The rest stays the same (password hashing via PBKDF2).
|
|
|
|
- [ ] **Step 2: Delete Server.hs**
|
|
|
|
```bash
|
|
rm src/Sis/Server.hs
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git rm src/Sis/Server.hs
|
|
git add src/Sis/Auth.hs
|
|
git commit -m "refactor: simplify Auth module; remove Orb/WAI Server"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Route + Style + Layout Modules
|
|
|
|
**Files:** `src/Sis/Route.hs` (create), `src/Sis/Style.hs` (create), `src/Sis/View/Layout.hs` (create)
|
|
|
|
- [ ] **Step 1: Create Route module**
|
|
|
|
```haskell
|
|
module Sis.Route where
|
|
|
|
import GHC.Generics (Generic)
|
|
import Web.Hyperbole.Route
|
|
|
|
data AppRoute
|
|
= RouteHome | RouteLogin | RouteSignup
|
|
| RouteDashboard | RouteChores | RouteHousehold | RouteActivity
|
|
deriving (Eq, Generic, Show)
|
|
|
|
instance Route AppRoute where
|
|
baseRoute = Just RouteHome
|
|
```
|
|
|
|
- [ ] **Step 2: Create Style module**
|
|
|
|
Shortcuts for Neo Brutalism CSS classes:
|
|
|
|
```haskell
|
|
module Sis.Style where
|
|
|
|
import Data.Text (Text)
|
|
import Web.Hyperbole.View
|
|
|
|
nbBox, nbButton, nbInput, nbLabel, nbBadge, nbFontHeading1, nbFontHeading2, nbContainer, nbList, nbListItem, nbRow :: Mods
|
|
nbBox = att "class" "nb-box"
|
|
nbButton = att "class" "nb-button"
|
|
nbInput = att "class" "nb-input"
|
|
nbLabel = att "class" "nb-label"
|
|
nbBadge = att "class" "nb-badge"
|
|
nbFontHeading1 = att "class" "nb-font-heading1"
|
|
nbFontHeading2 = att "class" "nb-font-heading2"
|
|
nbContainer = att "class" "nb-container"
|
|
nbList = att "class" "nb-box"
|
|
nbListItem = att "class" "nb-list-item"
|
|
nbRow = att "class" "nb-row"
|
|
|
|
colorRed, colorYellow, colorGreen :: Text
|
|
colorRed = "var(--nb-red)"
|
|
colorYellow = "var(--nb-yellow)"
|
|
colorGreen = "var(--nb-green)"
|
|
```
|
|
|
|
- [ ] **Step 3: Create Layout module**
|
|
|
|
Create `src/Sis/View/Layout.hs` with:
|
|
- `documentHead :: View DocumentHead ()` — Neo Brutalism CDN link, custom CSS, scriptEmbed, mobileFriendly, manifest
|
|
- `navbar :: User -> [Household] -> View ctx ()` — "Sis" heading, navigation links (Dashboard/Chores/Household/Activity/Logout)
|
|
- `requireAuth :: (Hyperbole :> es, DB :> es) => Eff es User` — reads `UserSession` from Hyperbole session, redirects to login if absent
|
|
- `UserSession` type with `Session` instance for cookie-based auth state
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Route.hs src/Sis/Style.hs src/Sis/View/Layout.hs
|
|
git commit -m "feat: add Route, Style, and Layout modules"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Login & Signup Pages
|
|
|
|
**Files:** `src/Sis/Page/Login.hs` (create), `src/Sis/Page/Signup.hs` (create)
|
|
|
|
- [ ] **Step 1: Create Login page**
|
|
|
|
`LoginPage` HyperView with `Action = SubmitLogin`. On submit, queries DB for user, verifies password, saves `UserSession`, redirects to dashboard. On failure, re-renders with error. Page function checks for existing session and redirects if logged in.
|
|
|
|
```haskell
|
|
module Sis.Page.Login (page) where
|
|
|
|
import Sis.Database
|
|
import Sis.Route (AppRoute (..))
|
|
import Sis.Style
|
|
import Sis.Types
|
|
import Sis.View.Layout
|
|
import Web.Hyperbole
|
|
import Web.Hyperbole.HyperView.Forms
|
|
import Web.Hyperbole.Effect.Session (Session (..), saveSession, lookupSession)
|
|
|
|
data UserSession = UserSession { usUserId :: Int }
|
|
deriving (Generic, FromJSON, ToJSON)
|
|
instance Session UserSession where cookiePath = Just "/"
|
|
|
|
data LoginPage = LoginPage deriving (Generic, ViewId)
|
|
instance HyperView LoginPage es where
|
|
data Action LoginPage = SubmitLogin deriving (Generic, ViewAction)
|
|
update SubmitLogin = do
|
|
LoginForm{..} <- formData @LoginForm
|
|
mUser <- FindUserByEmail lfEmail
|
|
case mUser of
|
|
Just u | verifyPassword lfPassword u.userPasswordHash -> do
|
|
saveSession (UserSession (unUserId u.userId))
|
|
redirect (routeUri RouteDashboard)
|
|
pure $ el "Redirecting..."
|
|
_ -> pure $ loginView (Just "Invalid credentials")
|
|
```
|
|
|
|
The `loginView` renders: centered box with heading, email input, password input, remember-me checkbox, submit button, signup link. Uses NB classes: nbContainer, nbBox, nbFontHeading1, nbLabel, nbInput, nbButton.
|
|
|
|
- [ ] **Step 2: Create Signup page**
|
|
|
|
Same pattern. `SignupPage` HyperView. Validates password length (≥8), password match, email uniqueness. Creates user, saves session, redirects.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Page/Login.hs src/Sis/Page/Signup.hs
|
|
git commit -m "feat: add Login and Signup Hyperbole pages"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Dashboard Page + ActivityForm
|
|
|
|
**Files:** `src/Sis/Page/Dashboard.hs` (create), `src/Sis/View/ActivityForm.hs` (create)
|
|
|
|
- [ ] **Step 1: Create Dashboard page**
|
|
|
|
`DashboardPage` HyperView with `Action = RefreshDashboard | CheckOff OccurrenceId`. On refresh, loads dashboard from DB for the user's active household. Stats tiles show overdue/dueToday/doneThisWeek counts with color-coded borders.
|
|
|
|
Due items list: each has chore name, assignee, status badge (OVERDUE/DUE), "Check Off" button. Clicking "Check Off" opens the `ActivityForm` HyperView inline (replacing the button row).
|
|
|
|
Completed items: read-only list of today's activities with user name, chore name, time, note.
|
|
|
|
The page function (`page`) gets the user from `requireAuth`, determines the active household, and initializes the dashboard.
|
|
|
|
- [ ] **Step 2: Create ActivityForm HyperView**
|
|
|
|
```haskell
|
|
data ActivityForm = ActivityForm OccurrenceId deriving (Generic, ViewId)
|
|
instance HyperView ActivityForm es where
|
|
data Action ActivityForm = SubmitActivity | CancelActivity
|
|
deriving (Generic, ViewAction)
|
|
update SubmitActivity = do
|
|
ActivityForm oid <- viewId
|
|
ActivityFormData{..} <- formData @ActivityFormData
|
|
let status = if afdStatus == "skipped" then ActivitySkipped else ActivityCompleted
|
|
-- Get user from session context (passed via view state or page-level data)
|
|
pure $ el "Recorded!" -- pushEvent to refresh dashboard
|
|
update CancelActivity = pure none
|
|
```
|
|
|
|
The form view: status dropdown (completed/skipped), note textarea, notify checkbox, Save/Cancel buttons.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Page/Dashboard.hs src/Sis/View/ActivityForm.hs
|
|
git commit -m "feat: add Dashboard page with ActivityForm HyperView"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Chores Page + ChoreForm
|
|
|
|
**Files:** `src/Sis/Page/Chores.hs` (create), `src/Sis/View/ChoreForm.hs` (create)
|
|
|
|
- [ ] **Step 1: Create Chores page**
|
|
|
|
`ChoresPage` HyperView with `Action = RefreshChores | DeleteChore ChoreId | ToggleCreate | ToggleEdit ChoreId`. View state tracks whether the create/edit form is active.
|
|
|
|
Chore list: each row shows name, schedule badge (recurring/one-off/sometime), schedule description, Edit/Delete buttons. Delete shows inline confirmation.
|
|
|
|
"New Chore" button toggles the `ChoreForm` HyperView inline at the top of the list.
|
|
|
|
- [ ] **Step 2: Create ChoreForm HyperView**
|
|
|
|
`ChoreForm` with `Action = SubmitChore | CancelChore`. Form fields:
|
|
- Name (text input)
|
|
- Schedule type (select: recurring, one-off, sometime)
|
|
- Start date (date input)
|
|
- Time of day (time input, optional)
|
|
- Period (select: daily/weekly/monthly, shown if recurring)
|
|
- Assignee (select: anyone)
|
|
- Notify on due (checkbox)
|
|
|
|
On submit, parses form data into `Schedule`, `ChoreAssignee`. Calls `CreateChore` or `UpdateChore`. Cancels by setting parent view state to hide form.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Page/Chores.hs src/Sis/View/ChoreForm.hs
|
|
git commit -m "feat: add Chores page with ChoreForm HyperView"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Household & Activity Pages
|
|
|
|
**Files:** `src/Sis/Page/Household.hs` (create), `src/Sis/Page/Activity.hs` (create)
|
|
|
|
- [ ] **Step 1: Household page**
|
|
|
|
`HouseholdPage` HyperView with `Action = CreateInvite | RevokeInvite InviteId | CreateHousehold`. Shows member list with display name, email, role badge, initials avatar. If user has no households: show "Create Your Household" form. Owner actions: create invite (shows generated code), revoke invites.
|
|
|
|
- [ ] **Step 2: Activity Log page**
|
|
|
|
`ActivityLog` HyperView with `Action = GoToPage Int`. View state tracks current page number. Shows paginated entries (20/page): status badge (COMPLETED/SKIPPED), user name, chore name, date, time, optional note. Previous/Next buttons with page indicator.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/Sis/Page/Household.hs src/Sis/Page/Activity.hs
|
|
git commit -m "feat: add Household and Activity Log pages"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Main.hs — Wire Everything Together
|
|
|
|
**Files:** `app/Main.hs` (rewrite)
|
|
|
|
- [ ] **Step 1: Create app entry point**
|
|
|
|
```haskell
|
|
module Main where
|
|
|
|
import Effectful
|
|
import Network.Wai.Handler.Warp qualified as Warp
|
|
import Network.Wai.Middleware.Static qualified as Static
|
|
|
|
import Sis.Database (openAndRunDB, runMigrations)
|
|
import Sis.Route
|
|
import Sis.View.Layout
|
|
|
|
main :: IO ()
|
|
main = do
|
|
let port = 8080
|
|
conn <- see Sis.Database openDatabase
|
|
Warp.run port $
|
|
Static.staticPolicy (Static.addBase "frontend/static") $
|
|
liveAppWith
|
|
(ServerOptions (document documentHead) defaultError defaultParseRequestBodyOptions)
|
|
(runDB conn $ routeRequest router)
|
|
|
|
router :: (Hyperbole :> es, DB :> es) => AppRoute -> Eff es Response
|
|
router RouteHome = redirect (routeUri RouteDashboard)
|
|
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
|
|
```
|
|
|
|
- [ ] **Step 2: Move static files**
|
|
|
|
```bash
|
|
mkdir -p frontend/static
|
|
cp frontend/public/style.css frontend/static/style.css
|
|
cp frontend/public/manifest.json frontend/static/manifest.json
|
|
```
|
|
|
|
Update style.css: remove `#app` padding, set body margin to 0.
|
|
|
|
- [ ] **Step 3: Build and fix compilation errors**
|
|
|
|
```bash
|
|
./hs stack build 2>&1 | tail -40
|
|
```
|
|
|
|
Fix import/type/extension errors iteratively.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add app/Main.hs frontend/static/
|
|
git commit -m "feat: wire up Hyperbole app entry point with all pages"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Cleanup — Delete TypeScript, Update Scripts, Docker, Docs
|
|
|
|
**Files:** Multiple deletes and modifies
|
|
|
|
- [ ] **Step 1: Delete frontend TypeScript**
|
|
|
|
```bash
|
|
rm -rf frontend/src frontend/index.html frontend/package.json frontend/package-lock.json frontend/tsconfig.json frontend/node_modules frontend/public frontend/dist
|
|
```
|
|
|
|
- [ ] **Step 2: Update scripts/build**
|
|
|
|
Remove npm steps. Just fourmolu, hlint, stack build.
|
|
|
|
- [ ] **Step 3: Update scripts/test**
|
|
|
|
Just hlint + stack test.
|
|
|
|
- [ ] **Step 4: Update scripts/run**
|
|
|
|
Simplify: use `stack exec sis-server` directly in Docker.
|
|
|
|
- [ ] **Step 5: Update Dockerfile**
|
|
|
|
Remove `ADD frontend/dist`. Just add binary and `frontend/static`. No npm build.
|
|
|
|
- [ ] **Step 6: Update test/Spec.hs**
|
|
|
|
Replace with a simple "compiles" test.
|
|
|
|
- [ ] **Step 7: Update README.md**
|
|
|
|
Replace frontend/backend sections. Describe Hyperbole stack, note zero application JavaScript.
|
|
|
|
- [ ] **Step 8: Update AGENTS.md**
|
|
|
|
Replace frontend conventions section:
|
|
|
|
```markdown
|
|
## Frontend Conventions
|
|
|
|
- **Framework:** [Hyperbole](https://github.com/seanhess/hyperbole) — Haskell serverside web framework
|
|
- **CSS:** [Neo Brutalism](https://unpkg.com/neobrutalismcss@latest) CDN + `frontend/static/style.css`
|
|
- **Build:** No npm/build step for frontend. All pages rendered in Haskell.
|
|
- **Interactive components:** HyperViews with typed Actions and server-side updates
|
|
```
|
|
|
|
Update project structure to show new module layout. Remove frontend/src/, add Sis/Page/ and Sis/View/ directories.
|
|
|
|
- [ ] **Step 9: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore: cleanup TypeScript, update scripts, Dockerfile, README, AGENTS"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Verify with Playwright
|
|
|
|
**Files:** None (verification only)
|
|
|
|
- [ ] **Step 1: Start the server**
|
|
|
|
```bash
|
|
./hs stack build && ./scripts/build
|
|
./scripts/run &
|
|
sleep 5
|
|
```
|
|
|
|
- [ ] **Step 2: Verify key flows with Playwright**
|
|
|
|
```bash
|
|
npx playwright test --project=chromium 2>&1
|
|
```
|
|
|
|
Or manually verify: login → dashboard → create chore → check off → view activity log.
|
|
|
|
- [ ] **Step 3: Fix any issues found**
|
|
|
|
Iterate on compilation/runtime bugs discovered during verification.
|
|
|
|
- [ ] **Step 4: Commit fixes**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix: address issues found during verification"
|
|
```
|