- Added new UI heading utility with specific styles in typography.css. - Updated typography tests to include new UI heading role and ensure proper size and styling. - Documented design system audit findings, focusing on accessibility improvements and component consistency. - Introduced CalendarExamples component to demonstrate calendar functionality with habit tracking. - Created a shared Field component for consistent labeling and hinting of form controls. - Implemented comprehensive accessibility tests for calendar and editor components, ensuring proper focus management and keyboard navigation. - Added structure tests for Field and Card components to verify correct ID generation and heading levels.
262 lines
13 KiB
Markdown
262 lines
13 KiB
Markdown
# Minabot
|
|
|
|
Bun + Hono habit-tracking REST API with Drizzle ORM and local SQLite. The existing
|
|
React Router client currently contains only the design system, ready for a fresh page redesign.
|
|
|
|
See [the REST API reference](docs/API.md) for habits, task recurrence, dated progress,
|
|
combined calendars, historical corrections, daily resets, and optional count carryover.
|
|
|
|
```sh
|
|
bun install
|
|
bun dev
|
|
```
|
|
|
|
Open http://127.0.0.1:3000. Set `PORT` to use a different port.
|
|
|
|
- Client page: `/design-system`. Root and all other client URLs redirect there.
|
|
- Hono endpoint: `GET /api/health`.
|
|
- Unknown API routes return JSON with a 404 status.
|
|
|
|
### Design system preview
|
|
|
|
The library uses four tabs: Foundations, Components, Layout, and Calendars.
|
|
Foundations is the default. Arrow keys, Home, and End navigate the tabs; URL
|
|
hashes and browser back/forward restore the section. Switching tabs preserves
|
|
local example state. Old `#playground` links open `#calendar-states`; removed
|
|
`#editing` and `#views` links resolve to `#foundations`.
|
|
|
|
Open `/design-system#calendar-states` for progress states and three working
|
|
calendar examples: a count target, a manual checkbox, and combined completion.
|
|
Changing water or reading progress updates today's combined score. Each calendar
|
|
supports 3/4/6/12-month ranges, a Custom start/end date range within available
|
|
history, date inspection, and keyboard navigation. Custom dates are inclusive;
|
|
Apply range updates the chart. Dates
|
|
form one continuous weekly chart with shared month and weekday labels and a fixed
|
|
4px gap between dates across every timeframe. Landscape
|
|
views fit the available width without chart scrollbars; narrow portrait views
|
|
retain 24px date targets in a local horizontal scroller. Primary controls use
|
|
44px targets, and custom checkboxes retain native semantics in forced-color mode.
|
|
These examples use a fixed September 4, 2026 demo date and do not call the API;
|
|
reload or Reset examples restores their initial progress.
|
|
|
|
The Playground, Editing, and References showcase tabs have been removed.
|
|
Reusable calendar, editing, color-picker components and reference assets remain
|
|
available for future pages.
|
|
|
|
The previous Landing, Home, About, and Settings pages have been removed. The
|
|
backend API, authentication infrastructure, database, and reusable components remain
|
|
available for the redesigned pages. The design system does not load account data
|
|
or call the API.
|
|
|
|
`styles/globals.css` is the only stylesheet
|
|
entry point; component styles are in Tailwind's components layer so utilities can
|
|
override them predictably. Tailwind Preflight remains omitted.
|
|
|
|
`styles/typography.css` defines reusable Tailwind classes: `type-display` (72/81),
|
|
`type-section` (48/54), `type-title` (32/40), `type-lead` (20/30), `type-body`
|
|
(16/24), `type-small` and `type-label` (14/21), and `type-control` (24/30, symbols).
|
|
Values are font-size/line-height in pixels at the default 16px root, implemented
|
|
in rem. At 640px and below, display steps to 48/54 and section to 36/45; body and
|
|
supporting text never shrink. Use these classes directly or with `@apply`, not
|
|
bespoke font sizes or viewport-based type. See `/design-system#foundations` for
|
|
live specimens. Reuse `SectionHeading`, `Button`, `ButtonLink`, and the shared
|
|
`DiscordSignInButton` for headings and calls to action. All default button variants,
|
|
including text/ghost buttons and links, share 10px vertical and 19px horizontal
|
|
padding. Ghost changes only colors and border visibility; counter buttons retain
|
|
their explicit 44px square sizing.
|
|
Use `type-ui-heading` (18/27, medium system sans) for interface headings, field
|
|
labels, and form legends; it stays visibly larger than 14/21 supporting text at
|
|
every breakpoint. Preserve serif `type-title` and `type-section` for editorial
|
|
headings. Use `type-eyebrow` for 12/18 section markers. `Field` supplies a control ID and hint ID through its
|
|
render callback; apply both to the native control. `Card.headingLevel` defaults
|
|
to 3 and can be set to match its surrounding document hierarchy without changing
|
|
its appearance.
|
|
|
|
Interface colors use the existing neutral palette through `--ds-ink`,
|
|
`--ds-secondary`, `--ds-control-border`, `--ds-rule`, `--ds-paper`,
|
|
`--ds-surface` (the existing #F5F5F5 fill), and `--ds-empty` (the existing #EEEEEE
|
|
calendar fill). Use secondary for muted text and primary hover, surface for
|
|
neutral hover/grouping, and control-border for actionable boundaries. Keep the
|
|
calendar progress ramp and habit identity colors separate. Foundations lists the
|
|
values and roles. Supporting copy should explain a consequence, constraint, or
|
|
interaction; omit prose that merely restates a heading or visible control.
|
|
|
|
```sh
|
|
bun run typecheck
|
|
bun run build
|
|
bun start
|
|
```
|
|
|
|
The build includes the backend and frontend in `dist/`. `bun start` serves that
|
|
production build. Client routes support direct loading and browser history.
|
|
|
|
## Database
|
|
|
|
The backend uses Drizzle ORM with Bun's built-in SQLite driver. The database is
|
|
created at `data/minabot.sqlite`, with WAL mode and foreign keys enabled. Database
|
|
files are ignored by Git. Development and production use the same file.
|
|
|
|
Optionally copy `.env.example` to `.env` and set `DATABASE_PATH` to another local
|
|
file. Environment-specific database paths can be set in `.env.development` and
|
|
`.env.production`. Relative paths are resolved from the project root by the package scripts.
|
|
|
|
Define tables in `src/db/schema.ts`, then generate and apply migrations:
|
|
|
|
```sh
|
|
bun run db:generate --name describe_change
|
|
bun run db:migrate
|
|
```
|
|
|
|
Commit the generated `drizzle/` files alongside schema changes. `bun dev` and
|
|
`bun start` apply pending migrations before starting the server. The initial
|
|
migration creates the users and sessions tables.
|
|
|
|
Import `db` from `src/db/index.ts` in backend code to query the database.
|
|
`GET /api/health` checks the database connection before returning success.
|
|
|
|
## Discord sign-in
|
|
|
|
1. Copy `.env.example` to `.env` for shared defaults, and copy
|
|
`.env.development.example` / `.env.production.example` to `.env.development` /
|
|
`.env.production` if those files do not exist. These local files are Git-ignored.
|
|
2. Create an application in the [Discord Developer Portal](https://discord.com/developers/applications).
|
|
Under OAuth2, copy the client ID and client secret into `DISCORD_CLIENT_ID`
|
|
and `DISCORD_CLIENT_SECRET` in `.env`.
|
|
3. Set `AUTH_COOKIE_SECRET` to a random secret of at least 32 characters, generated
|
|
with `openssl rand -hex 32`. Keep it server-side along with the client secret.
|
|
4. Set `APP_ORIGIN` in `.env.development` to the development browser origin,
|
|
initially `http://127.0.0.1:3000`, and set the same variable in `.env.production`
|
|
to the production origin, such as `https://habits.example.com`. Register **both**
|
|
origins with `/api/auth/discord/callback` appended as redirect URIs in Discord.
|
|
Use the matching hostname throughout each sign-in; `localhost` and `127.0.0.1`
|
|
have different cookie storage.
|
|
5. Run `bun dev`, click **Sign in with Discord**, and authorize the `identify` scope.
|
|
No bot token or email scope is needed. After returning, **View my profile** opens
|
|
`/api/me`. **Sign out** revokes the current browser's session.
|
|
|
|
Without the OAuth settings, public pages and `/api/health` remain usable, `/api/me`
|
|
returns 401, and sign-in displays a configuration message. Restart after editing
|
|
any env file. Use HTTPS for non-local deployments; HTTP is only allowed on loopback
|
|
origins. When using a reverse proxy, preserve the public request host and serve
|
|
both React and `/api` from the selected origin.
|
|
|
|
The package scripts explicitly select the environment before migrations or server
|
|
startup:
|
|
|
|
| Command | Environment file | Mode |
|
|
| --- | --- | --- |
|
|
| `bun dev` | `.env.development` | development |
|
|
| `bun start` | `.env.production` | production |
|
|
| `bun run db:generate` / `bun run db:migrate` | `.env.development` | development |
|
|
| `bun run db:migrate:production` | `.env.production` | production |
|
|
|
|
Each command loads optional `.env` shared defaults, then the selected file.
|
|
Variables exported by the shell or deployment platform take precedence over files.
|
|
The selected `APP_ORIGIN` controls OAuth callbacks, cookie security, and logout
|
|
Origin validation. Production requires an explicit `APP_ORIGIN`; development can
|
|
fall back to `http://127.0.0.1:$PORT` (port 3000 if unset). To test `bun start`
|
|
locally, set `APP_ORIGIN=http://127.0.0.1:3000` in `.env.production`.
|
|
|
|
Automatic env-file loading is disabled in `bunfig.toml` so the outer `bun start`
|
|
process cannot preload development settings into production. Package scripts load
|
|
the files explicitly using Bun's `--env-file` option; `.env.local` files are not
|
|
loaded by these commands. Build, test, and typecheck do not load application env
|
|
files. For custom Bun commands, pass the desired `--env-file` flags explicitly.
|
|
See [Bun environment variables](https://bun.sh/docs/runtime/environment-variables).
|
|
|
|
Use `APP_ORIGIN` in each file; `APP_ORIGIN_DEV` and `APP_ORIGIN_PROD` are no longer
|
|
used. Credentials can stay in `.env` or be overridden separately in each file.
|
|
For deployments sharing a machine, set distinct `DATABASE_PATH` values to keep
|
|
production habit data separate from development data.
|
|
|
|
### Schema and session behavior
|
|
|
|
- `users`: app UUID, unique Discord ID stored as text, username, nullable display
|
|
name and avatar hash, IANA timezone, and creation/update timestamps.
|
|
- `sessions`: SHA-256 token hash, user foreign key with cascading deletion,
|
|
creation timestamp, and expiry timestamp. User and expiry indexes support
|
|
session management and cleanup.
|
|
- Timestamps are Unix milliseconds. Each session expires 30 days after sign-in;
|
|
activity does not extend it. A new sign-in replaces the current browser's session.
|
|
Other browsers retain independent sessions. Expired sessions are removed during
|
|
sign-in or when presented to an authenticated route.
|
|
- Cookies are HttpOnly, SameSite=Lax, and host-only. HTTPS enables Secure and
|
|
`__Host-` cookie names. OAuth state uses a signed cookie with a ten-minute
|
|
lifetime and is cleared on callback. Token exchange and profile retrieval happen
|
|
server-side; Discord access/refresh tokens are never persisted or sent to React.
|
|
- React detects the browser timezone during initial registration. The server
|
|
validates it using `Intl.DateTimeFormat`, falling back to UTC. Later sign-ins
|
|
update the Discord profile without changing the saved timezone. A timezone
|
|
settings UI is deferred; `PATCH /api/me` updates the timezone. Habit timestamps
|
|
remain UTC and daily boundaries follow the saved timezone with preserved historical
|
|
deadlines. See the API reference for travel and DST behavior.
|
|
|
|
### API
|
|
|
|
| Method | Route | Behavior |
|
|
| --- | --- | --- |
|
|
| GET | `/api/health` | Public database health check |
|
|
| GET | `/api/auth/discord?timezone=Europe%2FBelgrade` | Start sign-in |
|
|
| GET | `/api/auth/discord/callback` | Validate OAuth response and create a session |
|
|
| POST | `/api/auth/logout` | Revoke current session; requires matching Origin |
|
|
| GET | `/api/me` | Public user fields for the current session, or 401 |
|
|
|
|
`/api/me` returns the object directly:
|
|
|
|
```json
|
|
{
|
|
"id": "app-generated-uuid",
|
|
"discordId": "123456789012345678",
|
|
"username": "mina",
|
|
"displayName": "Mina",
|
|
"avatarUrl": null,
|
|
"timezone": "Europe/Belgrade"
|
|
}
|
|
```
|
|
|
|
Only the fields declared in `src/shared/user.ts` are returned. Missing/invalid or
|
|
expired sessions receive `401 {"error":"Unauthorized"}`. Authentication endpoints
|
|
and `/api/me` use `Cache-Control: no-store`.
|
|
|
|
In `src/api.ts`, protect another route with the same middleware:
|
|
|
|
```ts
|
|
app.get("/api/example", auth.requireAuth, c => c.json({ userId: c.get("user").id }));
|
|
app.post("/api/example", auth.requireSameOrigin, auth.requireAuth, c => {
|
|
// Validate input and enforce ownership using c.get("user").id.
|
|
return c.json({ ok: true });
|
|
});
|
|
```
|
|
|
|
Use `requireSameOrigin` for all cookie-authenticated mutations. Same-origin browser
|
|
fetches send the cookie automatically; tokens should not be placed in localStorage.
|
|
|
|
```sh
|
|
bun test
|
|
bun run typecheck
|
|
bun run build
|
|
```
|
|
|
|
Tests use an isolated in-memory SQLite database with the real migrations and a
|
|
stubbed Discord HTTP client. They cover registration, profile refresh, timezone
|
|
preservation, cookie security, invalid/expired state, provider failures, session
|
|
expiry/rotation, logout CSRF checks, and the exact `/api/me` response. A real Discord
|
|
consent flow additionally requires your application credentials and browser login.
|
|
|
|
|
|
## Habit API verification
|
|
|
|
```sh
|
|
bun run test:coverage
|
|
bun run typecheck
|
|
bun run test:smoke
|
|
```
|
|
|
|
The smoke command builds and starts the production server against a disposable
|
|
SQLite database, exercises authenticated HTTP requests, then restarts it to verify
|
|
persistence. It does not use a live Discord provider or modify application data.
|
|
Habit tests cover every route, all PRD acceptance behaviors, ownership and Origin
|
|
checks, historical corrections, expiry, timezone boundaries, daily resets, optional
|
|
count carryover, Nivo data adaptation, and migration upgrades. Semantic commits
|
|
separate the database foundation, REST implementation, carryover, and verification.
|