Files
minabot/README.md
syntaxbullet dde5e77317 feat: enhance typography and accessibility in design system
- 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.
2026-09-04 13:44:31 +02:00

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.