syntaxbullet 00729096bf feat: add theme provider and control components for light/dark mode
- Implemented ThemeProvider to manage theme state and preferences.
- Added ThemeControl component for users to switch between system, light, and dark themes.
- Integrated localStorage for theme preference persistence.
- Updated document styles based on theme changes.

test: add tests for permanent deletion of habits and related records

- Created tests to ensure permanent deletion of habits removes all dependent records.
- Verified that deletion requires ownership, authentication, and checks for archived habits.
- Added tests to confirm rollback behavior on deletion failures.

test: add tests for habit palette and progress card functionality

- Implemented tests for habit color progression and theme-based color shading.
- Added tests for progress card calculations and calendar display logic.

feat: define theme types and preferences

- Introduced Theme and ThemePreference types for better type safety.
- Created utility functions for resolving theme based on user preference and system settings.
- Defined theme palettes for light and dark modes to ensure adequate contrast.
2026-09-05 10:07:28 +02:00

Mina Habits

Bun + Hono habit-tracking REST API with Drizzle ORM and local SQLite. The React Router client includes a homepage for signed-out visitors and a personal habit dashboard.

See deployment operations for backup, recovery, and monitoring.

See the REST API reference for habits, task recurrence, dated progress, combined calendars, historical corrections, daily resets, and optional count carryover.

bun install
bun dev

Open http://127.0.0.1:3000. Set PORT to use a different port.

  • Homepage: /. Signed-out visitors see an introduction and interactive examples; signed-in users see their saved habits and todays progress.
  • Component library: /design-system, independent of account loading.
  • Unknown client URLs redirect to /.
  • 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 homepage is composed from the existing design-system components and layouts: shared page shell, WelcomePanel, Card/CardGrid, SectionHeading, HabitChart, CalendarHeatmap, Counter, Checkbox, Field, ScheduleEditor, HabitColorPicker, and DiscordSignInButton. It uses no page-specific styles. The shared page shell also renders the component library. The design system does not load account data or call the API.

The dashboard's welcome panel groups the Discord avatar, greeting, progress, and habit actions on the left, with the account's current date and timezone on the right. It stacks on narrow screens and shows an initial if the avatar is unavailable.

Signed-in users can create manual, count, or task habits, choose a recurrence and color, log todays progress, and inspect their real calendars. Completed-versus-due counts exclude days off. Controls beside habit headings open a modal to edit the name, tracking method, schedule, color, count target/unit, and unfinished-count carryover. Each task can be renamed, rescheduled, or deleted in a modal; Add task also supports its own recurrence. Both habit and task schedules support daily, selected weekdays, day intervals, and week intervals with an editable start date and weekday. Archive asks for confirmation in a modal and preserves earlier history. Archived habits opens the archive modal, with history inspection and restoration starting today. Delete permanently removes an archived habit and all its history after confirmation; it also removes the habit from combined charts and deletes charts left empty. Task deletion uses the same modal confirmation. Tasks remain manageable on days off, while their checkboxes stay disabled. Settings, sharing, new habits, and historical check-ins also open in native dialogs. Dialogs keep the background inactive, contain keyboard focus, support Escape and backdrop dismissal, and return focus to their opener. Long content scrolls beneath a fixed header; closing is disabled during saves. Mutations are saved through the authenticated API; failed saves retain the recorded values and allow retry. Account and calendar failures have retry states. The dashboard refreshes on window focus and every minute while visible, using the servers date and saved account timezone. Select a calendar date and choose Edit selected check-in to correct counts, checkboxes, or task occurrences using that dates saved requirements. A date picker also reaches history outside the displayed year. Future dates and days off are read-only. Combined-chart management is not exposed on this homepage.

Daily check-in experience

The dashboard opens with a compact greeting and a Today filter. All habits includes routines on days off; creating a habit or editing one out of todays schedule keeps it reachable in All habits. Every habit shows its calendar immediately, starting with three months and offering longer ranges. Historical corrections are available by selecting a day. Selecting a square only changes the inspected day; it preserves the date range and number of weeks, including on untracked, unscheduled, and future dates. An external editor date outside a preset window can still bring that date into view.

Each saved habit shares a serif heading, type label, color marker, schedule, and explicit completion status. A visible graph sits beside a neutral completion panel: count goals show their target, unit, and editable counter; check-ins show a labeled checkbox; routines show an open task list and progress. On narrow screens, completion controls stack above the graph. Spacing, typography, and neutral surfaces use the shared design-system tokens; habit color marks identity and progress.

Count totals can be typed directly and saved with Save or Enter. Escape restores the recorded total; plus and minus remain available. Counts may exceed the target. New habits offer editable Reading, Get outside, and Evening routine starters, with eight suggested colors and More colors for the full palette and custom colors.

For browser QA without changing application data, run bun scripts/dashboard-preview.ts and visit http://127.0.0.1:3107/__preview/login. This uses an in-memory database and a separate preview cookie. The signed-out examples use a fixed September 4, 2026 demo date; progress in those examples is never saved.

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.

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:

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. Development also checks for pending migrations when the server entrypoint is hot-reloaded, so new routes do not run against an older schema. After adding only migration files, run bun run db:migrate or restart bun dev. 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. 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, the homepage opens your habit dashboard. 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.

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. Settings lets users change their timezone, download a complete JSON export, and permanently delete their account with typed confirmation. 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.

Progress sharing

Use Share progress on the dashboard to select up to six habits and a 7-day, 30-day, yearly, or custom range (at most 366 days, ending no later than today). The preview, PNG download, and Discord attachment use the same 1200px-wide image. Each habit has a color legend; completion rates exclude unscheduled days. Display name, avatar, habit names, and timezone can be hidden separately. Task names are never included. PNGs are rendered locally in the browser and are not stored by the server.

Set DISCORD_BOT_TOKEN and DISCORD_SHARING_CHANNEL_ID in .env (or the selected environment file), then restart the server. Invite the bot to the Discord server and grant it View Channel, Send Messages, and Attach Files in the sharing channel. Threads additionally require Send Messages in Threads. The app displays the configured destination automatically; all signed-in users share to that channel. Send to Discord posts only the previewed PNG as the bot, with mentions disabled. The bot token stays in the server environment and is never stored in SQLite or returned to the browser. No Gateway connection or message-content intent is needed. Delivery IDs and Discord message nonces protect against duplicate posts; ambiguous network failures ask the user to check the channel. Webhook setup has been retired, and the migration removes saved webhook connections while retaining delivery history.

The authenticated /api/sharing routes require the configured Origin for mutations:

Method Route Behavior
POST /preview {habitIds, from, to} → owned calendar data, totals, and legends
GET /discord Bot sharing status, channel name, and channel link; no credentials
POST /discord/send Multipart image PNG (up to 4 MB) and UUID deliveryId

For isolated browser QA, SHARE_PREVIEW=1 bun scripts/dashboard-preview.ts creates sample history and a local Discord transport stub on port 3107. Its login route is /__preview/login; /__preview/shared-image returns the last attachment for inspection. This mode never contacts Discord or opens the application database.

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:

{
  "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:

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.

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

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.

Account data

GET /api/account/export downloads versioned JSON containing all owned profile, habit, revision, dated progress, task, audit, calendar, chart, and delivery records. It excludes session tokens and credentials. DELETE /api/account requires matching Origin and { "confirmation": "DELETE" }; it atomically removes owned records and revokes every session. Discord posts and retained operator backups are not erased by this action.

Discord sharing reserves persistent quotas before posting: one new delivery per Discord user per 60 seconds, and ten per channel per 60 seconds. Rechecking an existing delivery does not consume quota. Rejected attempts consume quota too; 429 responses include Retry-After. Optional DISCORD_SHARING_ALLOWED_USER_IDS (comma-separated Discord IDs) restricts posting to community members you approve. An empty list permits all signed-in accounts. Restricted users can still preview and download PNGs. Quota rows expire and are pruned after 24 hours.

Daily reminders

Open Settings → Daily reminders to opt into one private Discord DM when habits remain unfinished. Choose a local time and quiet hours; times follow the account timezone. Quiet-hour start is inclusive, end is exclusive, and equal times disable quiet hours. If the scheduled time falls in quiet hours, delivery waits until they end that same local day. A reminder missed for the whole day is not carried forward.

The production server checks once a minute. DISCORD_BOT_TOKEN enables delivery; a sharing channel is not required for reminders. Users must be reachable by the bot (normally a shared Discord server and enabled DMs). A message contains only the unfinished-habit count and app link. Settings show the most recent delivery result, including blocked DMs or unconfirmed delivery. Uncheck the option and save to opt out. No reminders are enabled by default.

GET /api/reminders returns settings and last delivery status. PUT /api/reminders requires authentication, matching Origin, and {enabled, time, quietStart, quietEnd} with HH:MM times. Per-user/date reservations persist across restarts and concurrent workers. Discord 429 retry deadlines are persisted; ambiguous message sends are not retried that day. Delivery records expire after 90 days. Restore disables reminders in the recovered database until users opt in again.

The disposable SHARE_PREVIEW=1 bun scripts/dashboard-preview.ts mode also runs a reminder worker against a local transport stub. Set a reminder to 14:00 (its fixed Europe/Belgrade time), leave a habit unfinished, and inspect delivery status in settings or /__preview/reminder. No real Discord messages are sent in this mode.

Description
Minimal Bun, Hono, and React Router app with shadcn and Tailwind CSS v4
Readme 4.2 MiB
Languages
TypeScript 87.9%
CSS 11.9%
HTML 0.2%