Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .opencode/context/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- **Product**: Simpler — a self-hosted, **unified ADHD-friendly workspace**: tasks (kanban home), calendar (AI auto-scheduling), notes (markdown capture + Cleanify), and mail (IMAP → task), all organized around the same **Space** concept and served from one shell with one header. Guiding principle: friction-free, everything reachable in as few clicks as possible.
- **Actors**: Single user (ADHD individual) authenticated by one shared `APP_PASSWORD`. External integrators: any OpenAI-compatible LLM API (Anthropic / Mistral / OpenAI / Infomaniak) for task parsing / note cleaning / email-to-task, any ICS-publishing calendar (Google, Outlook) for external events, any IMAP server for mailboxes. The **embedded Chainlit assistant** (first-party, mounted at `/assistant`) acts on the app through the `mcp_server/` sidecar with the `API_TOKEN` bearer credential (mutations audited `actor='agent'`), and code-runs snippets in the isolated **sandbox** (`sandbox/`).
- **Tech stack**: Python 3.11, one ASGI process (`asgi.py`): **FastAPI umbrella** mounting **Chainlit 2** at `/assistant` + Flask 3 at `/` (a2wsgi WSGI bridge). Flask side: app factory + per-domain blueprints + Flask-SQLAlchemy + SQLite (`instance/tasks.db`). Chat history in a second SQLite (`instance/chainlit.db`, owned by `chat/data_layer.py`). `cryptography` (Fernet) for mailbox secrets, Bootstrap 5 + FullCalendar.js + SortableJS + EasyMDE (vanilla JS, CDN, no build step), Docker Compose deploy on port 53000. `migrate_db.py` (repo root) migrates prod app DBs (additive DDL + idempotent data fixups). Active vision: `.opencode/plans/000_PrePRD_unified-adhd-workspace.md` (applied 2026-07).
- **Tech stack**: Python 3.11, one ASGI process (`asgi.py`): **FastAPI umbrella** mounting **Chainlit 2** at `/assistant` + Flask 3 at `/` (a2wsgi WSGI bridge). Flask side: app factory + per-domain blueprints + Flask-SQLAlchemy + SQLite (`instance/tasks.db`). Chat history in a second SQLite (`instance/chainlit.db`, owned by `chat/data_layer.py`). `cryptography` (Fernet) for mailbox secrets, Bootstrap 5 + FullCalendar.js + SortableJS + EasyMDE (vanilla JS, CDN, no build step) with a phone layer bolted on as progressive enhancement (`static/css/mobile.css` + `isMobile()` branches — desktop byte-identical, see the responsive topic), Docker Compose deploy on port 53000. `migrate_db.py` (repo root) migrates prod app DBs (additive DDL + idempotent data fixups). Active vision: `.opencode/plans/000_PrePRD_unified-adhd-workspace.md` (applied 2026-07).

## Architecture

Expand All @@ -30,6 +30,7 @@ Module map:
- `src/calendar_integration.py` — live ICS fetch. `src/mail_integration.py` — live IMAP fetch (transient DTOs). `src/crypto_utils.py` — Fernet from SECRET_KEY.
- `src/config.py` — env + prompt loading, cached at startup: `SYSTEM_PROMPT` (`task_creation.md`), `NOTES_CLEANIFY_PROMPT`, `EMAIL_TO_TASK_PROMPT` (all under `src/prompts/*.md`). Exposes `assistant_url` (== `/assistant/` when `SIMPLER_ASSISTANT_MOUNTED`, else None) so the shell template-conditionalizes the Assistant tab.
- `src/templates/index.html` — THE shell (all destinations + modals incl. shortcuts help). `login.html`.
- `src/static/css/` — `style.css` (the shell; desktop-first, plus legacy 768/900/992px tablet blocks) and `mobile.css` (the phone layer, everything scoped to `@media (max-width: 640px)`, loaded last). Mobile rules never leave that breakpoint — see `topics/responsive-mobile.md`.
- `src/static/js/` — `app.js` (shell nav, shortcuts, board, calendar, overview), `notes.js` (`NotesView`), `mail.js` (`MailView`), `spaces.js` (`SpacesView` — space CRUD + AI context editor), `task_draft_modal.js` (shared draft confirm), `workspace.js` (`WorkspaceView` — the Assistant tab's right file drawer over `/api/workspace/*`, toggle `W`), `assistant_settings.js` (the Assistant settings modal over `/api/assistant/*`, shortcut `,`).
- `chat/` — the embedded first-party Chainlit assistant (see `topics/chat-assistant.md`): `chainlit_app.py` (wiring only), `agent.py` (model⇄tools loop), `toolbox.py` + `mcp_tools.py` + `sandbox_tools.py` + `web_tools.py` + `files.py` + `skills.py` + `commands.py` (tool + UI feature surface), `providers.py` (OpenAI/Anthropic wire formats + reasoning-effort mapping), `modes.py` (chat-bar Mode pickers: model + reasoning, read per message), `assistant_settings.py` (pure path/JSON helpers for `instance/assistant/` — editable `system.md` override, `models.json`, `reasoning.json`, instance skills dir), `auth_bridge.py` (session-cookie identity), `data_layer.py` (SQLite-aware Chainlit history layer), `settings.py` / `workspace.py` (env + context building).
- `instance/assistant/` — gitignored per-instance assistant customization (survives upgrades, files-on-disk only, no DB): `system.md` (base-prompt override, read per message), `models.json` + `reasoning.json` (chat-bar pickers), `skills/` (user-authored skills shadowing the bundled `chat/skills/` by name). Managed from the in-app Assistant settings panel.
Expand All @@ -46,6 +47,7 @@ Module map:
- @.opencode/context/topics/notes.md
- @.opencode/context/topics/mail.md
- @.opencode/context/topics/chat-assistant.md
- @.opencode/context/topics/responsive-mobile.md

## Domain glossary

Expand All @@ -55,6 +57,7 @@ Module map:
- **Space AI context**: `Space.context_markdown` is appended to every AI task prompt (`space_guidance_block()`) wrapped in explicit guide-not-source framing — it steers space choice/priority/deadline/wording but must never be copied into task fields or treated as part of the user's request.
- **space_id vs space**: `Task.space_id` (FK) is canonical; the `Task.space` string column is legacy, unread by code, and backfilled into `space_id` by `migrate_db.py`. `to_dict()` echoes the name from the relation.
- **Unified shell**: one page (`index.html`), one header, destinations switched client-side (`switchDestination`), deep links `#tasks/#notes/#mail/#calendar/#spaces/#assistant`, last destination remembered (unavailable remembered destination falls back to Tasks).
- **Phone layer**: the shell's mobile UX (≤640px) — bottom tab bar, swipeable scroll-snap board with a tap action sheet instead of drag, master-detail for the list views, single-select space dropdown instead of chips. Added as progressive enhancement over the same shell, **never a second frontend**: all of it lives behind one CSS breakpoint (`static/css/mobile.css`) and `isMobile()` guards, so the PC experience is byte-identical by construction (verified by pixel diff). The rationale, the rejected React/Vite rewrite, and the traps are in `topics/responsive-mobile.md` — read it before touching mobile layout or proposing a frontend rewrite.
- **Assistant destination** (always on when `SIMPLER_ASSISTANT_MOUNTED`, i.e. served by `asgi.py`): 6th tab (`6`, `#assistant`) iframing the embedded Chainlit app same-origin at `/assistant` (chat history in `instance/chainlit.db`). The iframe loads lazily; a `#assistantSpaceChips` toolbar prints the active Space scope into prompts. No second login: identity is bridged from the Flask session cookie by signature (`chat/auth_bridge.py`); single-user app ⇒ one fixed Chainlit identifier `"owner"`. When `src/app.py` runs Flask alone, the Assistant tab/views/help row are template-conditionalized away via `assistant_url is None`. The agent acts on the app through the MCP sidecar (`SIMPLER_MCP_URL`), never the iframe.
- **API_TOKEN bearer auth**: optional second auth path in `auth.py::login_required` (`Authorization: Bearer`, `hmac.compare_digest`); sets `g.actor='agent'` which `audit.record_change()` uses as default actor (explicit actors like the parse paths' `'ai'` win). Unset ⇒ byte-identical to session-only.
- **Board / Overview**: the two Tasks subviews — kanban (primary) and grouped-by-space overview (secondary); toggle persisted. The Overview's "Show done" toggle (persisted) lists finished tasks by `completed_at` desc.
Expand Down
47 changes: 47 additions & 0 deletions .opencode/context/topics/responsive-mobile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Responsive / phone layer

> Phone UX for the shell, added 2026-07 as **progressive enhancement over the existing vanilla-JS shell** — not a second frontend. CSS in `src/static/css/mobile.css` (everything inside `@media (max-width: 640px)`), JS in `isMobile()`-gated branches in `src/static/js/app.js` (+ small hooks in `notes.js`). The desktop UI is the project's crown jewel and stays **byte-identical**.

## Why this shape (read before proposing a rewrite)

The obvious framing — "the app is unusable on phone, so we need a proper frontend, maybe React/Vite, and a cleaner front/back split" — was explored and **rejected on evidence**:

- **The front/back split already exists.** All domain logic sits behind the Flask `/api/*` blueprints, and `mcp_server/` already consumes that exact contract as an independent HTTP client over the `API_TOKEN` bearer. A new frontend would "unlock" a boundary we already have, so that argument for rewriting is void.
- **Chainlit was never the problem.** It only backs the Assistant tab, and its own React UI is already responsive. Keeping it costs nothing; only the shell chrome *around* the iframe needed to fit. "Escape Chainlit" is not a reason to rewrite.
- **The desktop-first part is the shell** (`index.html` + `app.js` + `style.css`): fixed-pixel sidebars, a 4-column board, hover-only affordances, modifier-click conventions, ~30 keyboard shortcuts.

So the actual problem was narrow, and a second frontend would have cost a duplicated client (feature drift), a JS build pipeline against the project's deliberate **no-build-step** ethos, and re-implementations of the FullCalendar / EasyMDE / board / mail clients. Progressive enhancement was the cheaper and lower-risk answer.

**A separate mobile app stays a documented escape hatch** if progressive enhancement ever hits a real UX ceiling — but re-argue it from evidence, not from the framing above.

## The invariant (do not break this)

*"Without tampering AT ALL with PC usability"* was the hard requirement, so it is enforced **structurally rather than by care**:

- Every mobile rule lives inside the one phone breakpoint. The only top-level rules in `mobile.css` are `display:none` defaults for elements that **do not exist in the desktop design** (the card action button, the master-detail back buttons, the space dropdown) plus `.mobile-sheet-*` styling, which matches nothing on a desktop page.
- Every mobile JS path is behind `isMobile()`, added as a *new* branch. Desktop code paths were never restructured — on PC the drag init, calendar config and click delegation run the same code as before.
- `body.is-mobile` is toggled reactively (`matchMedia` change listener) for anything that needs the state in JS rather than CSS.

Two failure modes to watch when extending: **a rule escaping the breakpoint**, and **structural selectors shifting** — `display:none` elements still count for `:first-child`/`:nth-child`, so injecting hidden nodes into desktop DOM can silently reflow it. Both were audited; keep auditing.

## Decisions worth knowing

- **The board swipes, it doesn't stack.** Full-width scroll-snap columns preserve the kanban mental model (four named columns you move between); a single stacked list would have dissolved it.
- **Drag is desktop-only on purpose.** Touch-dragging fights both the scroll-snap track and page scroll, so SortableJS is simply *not initialized* on phones. Status changes go through a per-card action sheet instead. This is why the mobile board needs no drag polish — the gesture is deliberately absent, not broken.
- **The action sheet is plain DOM, not a Bootstrap modal.** The shell's global Ctrl+Enter handler and several flows key off `.modal.show`; making the sheet a real modal would have entangled it with that plumbing for no benefit.
- **Modifier-click conventions have no touch equivalent**, so everything they express (done / freeze / advance / edit) is reachable from that sheet as visible buttons. Hover-revealed affordances are pinned visible on coarse pointers for the same reason.
- **Space filter is a dropdown on phone, single-select only.** With ~10 spaces the chip row ate most of the screen. Ctrl+click multi-space and Alt+click exclude stay desktop-only (KISS was the explicit ask), and picking from the dropdown clears exclusions. A filter built on desktop that the dropdown cannot express renders a disabled *"Multiple spaces"* entry — **the UI must not claim "All spaces" while the board is actually filtered**. One shared helper (`syncMobileSpaceSelect`) is called from each of the three chip renderers with an `onPick` mirroring that view's existing plain-click branch, so filter semantics are never duplicated.
- **Master-detail for Notes / Mail / Spaces**, because their fixed 260–380px sidebars have no phone equivalent. The detail overlay is reset on destination switch so you never land back in a stale editor.

## Caveats

- **`mobile.css` cascades on top of the older tablet breakpoints in `style.css` (768/900/992px), which still apply at phone widths.** This bit once: the 992px rule sets `.board { flex-wrap: wrap }`, which stacked the full-width columns vertically until the phone rule set `nowrap` explicitly. When a mobile layout misbehaves, look for an earlier breakpoint before debugging the new rule.
- The Assistant tab's *inside* is Chainlit's own responsive UI — we only fit the surrounding toolbar and hide the workspace drawer. Do not try to restyle the iframe's contents from here; `chat/public/simpler.css` is the seam.
- **PWA / installable / offline capture was deliberately deferred** (manifest + service worker + an offline capture queue). It is a strong fit for on-the-go ADHD capture and the natural next step, but it was scoped out, not forgotten.
- Keyboard shortcuts and the `#helpModal` table are desktop concerns and were left alone; if a mobile interaction ever changes a shortcut, the help table is still the single source of truth (see `CONTEXT.md`).

## Verifying a change here

The regression net for "PC is untouched" is a **pixel diff**: screenshot the desktop at several widths across every destination, before vs after, and require **0 differing pixels** (it has held for every change so far, including with 10 spaces seeded). Pair it with an interactive check of the desktop conventions the change is near — a static render can look identical while a handler is dead. On the mobile side, assert no horizontal overflow (`scrollWidth <= innerWidth`) per destination.

Two environment traps when driving the app headless: the shell's CDN assets (Bootstrap / FullCalendar / SortableJS / EasyMDE / Font Awesome) may be blocked by an egress policy — install the same packages from npm and route the CDN URLs to those local copies, or the board renders empty and the failure looks like a code bug. And a `goto` that only changes the URL hash does **not** reload, so the destination never switches; reload after navigating.
Loading
Loading