mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 12:06:57 +02:00
ffde151b755956381c766589578496320cbbf5bc
Closes the UI gap in issue #77. The endpoint, resetPassword() and the notification email have all existed since the feature was built; only the control was missing, which is why the docs promised something the screen did not offer. The control renders on EVERY row, the admin's own included. It is enabled only for accounts that sign in with a password and that are not the signed-in admin; otherwise it is presented as unavailable WITH A VISIBLE REASON. A control that simply vanishes is the confusion this replaces, so silence was not an option. No new control uses the `disabled` attribute. Unavailable is expressed with aria-disabled, the element stays focusable, and the handler refuses activation itself — a disabled button leaves the tab order, so a keyboard or TalkBack user loses the element they just activated, and a title tooltip explains nothing on a touch screen. The reporter is on Android. Two rules protect the credential, and both are enforced by the shape of the state rather than by convention: - An unconfirmed outcome REVEALS the password. If the request left the device and no answer came back, the server may well have stored it, making the revealed string the only copy in existence. Discarding it could leave an account holding a credential nobody knows. - `password` and `message` are independent fields, so an error has nowhere to write that would replace a reveal. Revealed rows are also marked unavailable, so a second reset cannot be issued until Dismiss. Offline is a definite state, not a lost response: the service worker caches only GET /api/*, so a PATCH never leaves the browser. It is checked before anything is generated, and reported as the certainty it is. The live region is rendered empty on first paint and only its text is replaced, because a region inserted together with its content is not reliably announced. It announces that a password appeared and where — never the secret itself, which a polite region would speak aloud once, unrepeatably, to whoever is in earshot. DoD items verified (22): - [x] 1-2. control on every row; enabled iff eligible and not self, else aria-disabled with a visible, aria-describedby reason - [x] 3-4. PATCHes newPassword; "Resetting…" and no double-submit - [x] 5-6. revealed as selectable text, announced; stays unavailable - [x] 7. Copy confirms success; failure offers a manual-copy hint - [x] 8-10. 4xx reveals nothing; offline definite; unknown REVEALS - [x] 11-12. errors never replace a reveal; both announced - [x] 13-14. 404 drops the row and its plaintext; rows isolated - [x] 15-17. Dismiss clears and restores focus; 44px targets; activating an aria-disabled control fires no request - [x] 18-19. victim login from a separate context, old password 401 last; plaintext in no storage, no IndexedDB, no console - [x] 20-22. CSS variables only, no hover-only; untruncated at 360px; declarative Gherkin with distinct victim emails Verified independently by the orchestrator: pnpm check 0 errors / 14 pre-existing warnings; pnpm test:unit 306 passed / 26 files; admin-users.spec.ts 28 passed; full e2e suite 126 passed. The reviewer additionally mutation-tested the security core, each mutation rebuilt and caught with clean attribution: revealing null on an unconfirmed outcome, forcing the offline pre-flight false (which falls into the unknown path and would reveal a password that never left the browser), dropping the state merge in patchReset, swapping aria-disabled for disabled (which breaks focus return), and deleting the activation guard — the last proving the force-click assertions test the component refusing rather than Playwright declining to click. Reviewed-by: adversarial-review (PASS) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbqJcFofZSbhbGwUwZP3wU
Crumbs by Bretzel
A self-hostable, offline-first note-taking app that respects your privacy. Part of the Bretzel app universe.
Install
Docker (Recommended)
docker compose up -d
Open http://localhost:3000 and create your admin account on first visit.
With SSO (optional)
# docker-compose.yml
services:
crumbs:
image: ghcr.io/bretzel-app/crumbs:latest
ports:
- "3000:3000"
volumes:
- crumbs-data:/data
environment:
- ORIGIN=https://notes.example.com
- AUTH_OIDC_ISSUER=https://authentik.example.com/application/o/crumbs/
- AUTH_OIDC_CLIENT_ID=your-client-id
- AUTH_OIDC_CLIENT_SECRET=your-client-secret
- AUTH_OIDC_DISPLAY_NAME=Authentik
restart: unless-stopped
volumes:
crumbs-data:
Google and GitHub OAuth are also supported. See docs/AUTH.md for all providers and setup guides.
With Email Notifications (optional)
Add SMTP settings to enable email notifications (share alerts, welcome emails, security alerts):
environment:
- SMTP_HOST=smtp.example.com
- SMTP_PORT=587
- SMTP_USER=your-username
- SMTP_PASS=your-password
- SMTP_FROM=Crumbs <noreply@example.com>
Auto-enabled when SMTP_HOST is set. Users can opt out in Settings > Preferences. See docs/DEPLOYMENT.md for details.
Manual (Node.js)
git clone <repo-url> crumbs && cd crumbs
pnpm install
pnpm build
DATABASE_URL=./data/crumbs.db ORIGIN=http://localhost:3000 node build
Development
pnpm install
pnpm dev
Run make help to see all available commands, or use pnpm directly:
pnpm test # Unit + E2E tests
pnpm check # Type checking
pnpm build # Production build
Features
- Rich notes with Markdown, checklists, image attachments, and 12 color themes
- Organize with #tags, pinning, archive, and trash
- Full-text search across titles, content, and tags
- Note version history — browse and restore previous versions
- Share notes with other users on the same instance
- Email notifications for shares, security alerts, and account events (optional SMTP)
- PWA — installable, works offline via IndexedDB + LWW CRDT sync
- MCP server — let AI assistants (Claude Code, etc.) manage your notes
- Multi-user auth (Argon2) with optional OAuth/SSO (Google, GitHub, OIDC)
- Docker deployment with a single command
See docs/FEATURES.md for detailed feature documentation.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | SvelteKit 2 (Svelte 5 runes) |
| Language | TypeScript (strict) |
| UI | Tailwind CSS 4 |
| Database | SQLite (better-sqlite3) + Drizzle ORM |
| Client DB | IndexedDB (idb) |
| Sync | LWW CRDTs |
| Auth | Argon2 + session cookies |
| Testing | Vitest + Playwright |
| Container | Docker (multi-stage) |
| CI/CD | GitHub Actions |
Documentation
| Document | Description |
|---|---|
| Features | Detailed feature list and behavior |
| Architecture | Local-first sync design, tech rationale, DB schema |
| Auth | Password auth, OAuth/SSO setup (Authentik, Keycloak, etc.) |
| Deployment | Docker, Node.js, reverse proxy, backups, env vars |
| API | REST API reference (auto-generated) |
| Contributing | How to contribute |
| Security | Vulnerability reporting policy |
| Changelog | Release history |
CI/CD
- CI — lint, type check, unit tests, E2E tests, Docker build on every push/PR
- Release — builds and pushes Docker image on
v*tags
Configure registry via GitHub Secrets: REGISTRY_URL, REGISTRY_USER, REGISTRY_TOKEN.
License
Languages
TypeScript
68.5%
Svelte
25.1%
HTML
2.7%
CSS
2.7%
Shell
0.5%
Other
0.5%
