Files
thandClaude Opus 4.6 d31e186bd9 docs: document app-only compose + fix dev.sh for relay profile
dev.sh still has to opt into the relay profile now that it's no
longer in the default set; otherwise `./dev.sh` would print "relay:
http://localhost:8081" but skip starting the container. README gets
a short compose section in Quick Start explaining the app-only
default and how to add --profile relay for a local relay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:12:06 +02:00

544 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# hyprcrd
A self-hosted card/note manager with markdown, tags, image galleries, and Obsidian-compatible export.
## Requirements
- PHP 8.3+
- Apache with `mod_rewrite`
- PHP extensions: `pdo_sqlite`, `zip`
No external dependencies. All assets are bundled locally.
## Quick Start (Docker)
```bash
git clone https://git.uesome.de/tanztee/hyprcrd.git
cd hyprcrd
docker build -t hyprcrd .
docker run -d -p 8080:80 -v hyprcrd-data:/var/www/html/data hyprcrd
```
Open `http://localhost:8080` and set your password on first visit.
The `-v hyprcrd-data:/var/www/html/data` flag persists the database across container restarts.
### Via docker compose
If you prefer compose, the bundled `docker-compose.yml` starts the app alone by default:
```bash
docker compose up -d # just the app on :8088
```
To also bring up a local relay (for multi-tenant messaging / collab / chat), opt in via the `relay` profile:
```bash
docker compose --profile relay up -d # app on :8088 + relay on :8081
```
Most installs talk to a remote relay, so the app-only default keeps the local setup minimal.
## Manual Installation
### 1. Clone
```bash
git clone https://git.uesome.de/tanztee/hyprcrd.git
cd hyprcrd
```
### 2. Set document root
Point your web server's document root to the `public/` directory.
**Apache vhost example:**
```apache
<VirtualHost *:80>
ServerName hyprcrd.local
DocumentRoot /path/to/hyprcrd/public
<Directory /path/to/hyprcrd/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
Make sure `mod_rewrite` is enabled:
```bash
a2enmod rewrite
systemctl restart apache2
```
### 3. Set permissions
```bash
mkdir -p data public/uploads
chmod 755 data
chmod 777 public/uploads
```
The web server user (e.g. `www-data`) needs write access to both directories.
### 4. PHP settings (optional)
For larger image uploads, adjust in `php.ini`:
```ini
upload_max_filesize = 20M
post_max_size = 22M
```
### 5. Open and set password
Navigate to your URL. On first visit, you'll be prompted to set a password (min. 4 characters). That's it.
## Directory Structure
```
hyprcrd/
├── config.php # App constants (auto-configured)
├── Dockerfile
├── docker-entrypoint.sh
├── dist.sh # Build distribution ZIP
├── src/
│ ├── bootstrap.php # Initialization + plugin discovery
│ ├── Database.php # SQLite schema & migrations
│ ├── PluginManager.php # Plugin scanner & asset/slot renderer
│ ├── CalDavClient.php # CalDAV HTTP client + iCal parser
│ └── CalDavSync.php # Bidirectional CalDAV sync engine
├── public/ # Document root
│ ├── index.php # Entry point & API
│ ├── .htaccess # URL rewriting
│ ├── css/
│ │ ├── style.css # App styles
│ │ ├── tailwind.out.css # Generated Tailwind utilities
│ │ ├── fonts.css # Local @font-face rules
│ │ └── vendor/ # Phosphor Icons CSS + fonts
│ ├── fonts/ # JetBrains Mono & Silkscreen
│ ├── js/
│ │ ├── hyprcrd.js # Plugin API (window.hyprcrd, sync-loaded)
│ │ ├── app.js # Orchestrator
│ │ ├── modules/ # Core feature modules
│ │ └── vendor/ # marked.js, DOMPurify
│ ├── plugins/ # Optional, drop-in plugin folders
│ │ ├── canvas/ # ASCII art canvas editor
│ │ ├── chat/ # Encrypted relay chat
│ │ ├── collab-buckets/ # Collaborative bucket sync
│ │ ├── email/ # Email overlay
│ │ ├── gitea/ # Gitea issue integration
│ │ ├── logseq-import/ # Logseq markdown importer
│ │ ├── slideshow/ # Presentation mode
│ │ └── tests/ # UI + state test runner
│ └── uploads/ # User media (writable)
├── electron/ # Electron desktop app wrapper
│ ├── main.js # Spawns PHP, creates BrowserWindow
│ ├── router.php # PHP built-in server router
│ ├── fetch-php.sh # Downloads static PHP binaries
│ └── php-bin/ # Platform-specific PHP binaries
├── relay/ # Relay service (multi-tenant messaging + collab)
│ ├── index.php # Entry point + API dispatch
│ ├── inc/ # Backend modules (bootstrap, crypto, auth, routes)
│ ├── ui/ # Frontend (layout.php, style.css, app.js)
│ ├── .htaccess # URL rewriting + data protection
│ ├── Dockerfile
│ └── dist.sh # Build relay distribution ZIP
└── data/
└── hyprcrd.db # SQLite database (auto-created)
```
## Features
- **Cards** with markdown body, color, tags, and position
- **Buckets** for organizing cards into groups
- **Tags** written as `#tagname` or hierarchical `#parent/child` in card body, extracted automatically
- **Markdown** with `[[Card Title]]` wiki-links, ```` ```gallery ```` image galleries, ```` ```tree ```` list views, ```` ```ascii ```` art canvas (via canvas plugin)
- **Search** combining text and `#tag` filters (e.g. `#project meeting notes`)
- **Assets gallery** (images, audio, PDFs) with orphan detection and fullscreen viewer
- **Split view** showing tag-related and content-related cards
- **Import/Export** as JSON+media ZIP or Obsidian/Logseq-compatible markdown vault
- **Drag & drop** card reordering and grouping
- **Calendar** with month view, journal entries, appointments, and day popup
- **CalDAV sync** with ownCloud/Nextcloud (bidirectional, `#termin` tagged cards)
- **Inline calculator** (`?3+5` → `8`, `34+56?` → `34+56=90`)
- **Snippets** (`?dat`, `?time`, `?now`, `?todo`, `?table`, `?code`, ...)
- **Due dates** with overdue notifications and pulsing cards
- **Relay** for mobile note capture and card sharing between instances
- **Notification banner** for upcoming events, birthdays, overdue tasks
- **Board split view** with side-by-side preview
- **Superview** compact multi-column list with hover cards
- **Super-buckets** — nest buckets one level deep with a dropdown for children
- **Card groups** — drag-to-group, join multiple cards, dissolve
- **Context menu** — color, tags, copy text, mark done, join cards, make ticket (gitea)
- **Find & replace** — in-card (Ctrl+H) and across all cards (Ctrl+Shift+H via search)
- **Arrow key navigation** on the board with Enter to open
- **PDF upload** in the editor toolbar
- **Keyboard shortcuts**: `Ctrl+K` search, `F1` help, `?` shortcut reference, arrow keys, 1–9 bucket switch
- **Electron desktop app** — standalone AppImage/DMG/NSIS with bundled PHP, no server needed
## Plugins
hyprcrd has a drop-in plugin system. A plugin is just a folder under `public/plugins/` with a `plugin.json` manifest. The core scans the directory at startup, loads each plugin's CSS/JS/HTML, and the plugin self-registers against `window.hyprcrd`. Plugins are entirely optional — removing a plugin folder removes the feature, with no code changes anywhere else.
### Bundled plugins
- **`canvas/`** — ASCII art canvas editor. Adds a toolbar button + ` ```ascii ` fenced block. Draw with brush, line, rectangle, ellipse, fill, and text tools using a monospace character grid.
- **`chat/`** — Encrypted chat between relay tenants. Footer flyout with delivery + read receipts.
- **`collab-buckets/`** — Collaborative buckets shared between relay tenants with exclusive card locking and E2E encrypted sync.
- **`email/`** — Email overlay (toggled with `M` key).
- **`gitea/`** — Gitea integration: create issues from cards, link to repos.
- **`slideshow/`** — Slideshow presentation mode for cards with `slide-image` blocks.
- **`logseq-import/`** — Import Logseq markdown journals and pages.
- **`tests/`** — UI + state test runner (40+ test files). Loaded only when `Ctrl+Shift+Alt+T` is pressed, so production users pay no cost for it.
### Plugin layout
```
public/plugins/myplugin/
├── plugin.json # Manifest (required)
├── myplugin.js # Entry point (loaded as ES module)
├── myplugin.css # Optional styles
├── myplugin.html # Optional HTML fragment (overlays, modals, etc.)
└── backend.php # Optional PHP hooks
```
**`plugin.json`:**
```json
{
"name": "myplugin",
"version": "1.0.0",
"description": "What this plugin does",
"frontend": {
"js": ["myplugin.js"],
"css": ["myplugin.css"],
"html": ["myplugin.html"]
},
"backend": "backend.php"
}
```
### Plugin API (`window.hyprcrd`)
Loaded synchronously before any module, so plugins can register handlers immediately:
| Namespace | Methods | Purpose |
|---|---|---|
| `hyprcrd.editor` | `registerCommand(name, fn)`, `getCommand(name)`, `api.{body, insertMd, updatePreview}` | Add toolbar commands. The `data-md="<name>"` button dispatches to the registered handler. `api` is set after the editor initializes. |
| `hyprcrd.markdown` | `registerFencedBlock(lang, fn)`, `registerPreProcessor(fn)`, `registerPostProcessor(fn)` | Add ` ```<lang> ` block renderers (returns HTML, passes through DOMPurify). Pre/post processors run on the source/HTML around `marked.parse()`. |
| `hyprcrd.hooks` | `on(name, fn)`, `off(name, fn)`, `emit(name, data)` | Generic event bus. Use for cross-plugin or core→plugin signaling. |
| `hyprcrd.util` | `$(idOrSel)`, `$$(sel)`, `esc(s)` | Lightweight DOM/escape helpers so plugins don't need to import core modules. |
| `hyprcrd.registerPlugin(meta)` | — | Register the plugin's metadata for introspection. |
### Minimal plugin example
`public/plugins/hello/plugin.json`:
```json
{
"name": "hello",
"version": "1.0.0",
"description": "Inserts a greeting",
"frontend": { "js": ["hello.js"] }
}
```
`public/plugins/hello/hello.js`:
```js
const h = window.hyprcrd;
h.editor.registerCommand('hello', () => {
h.editor.api.insertMd('Hello, world! ', '', '');
});
// Inject a toolbar button
const btn = document.createElement('button');
btn.dataset.md = 'hello';
btn.className = 'toolbar-btn';
btn.title = 'Hello';
btn.textContent = '👋';
document.getElementById('md-toolbar').appendChild(btn);
h.registerPlugin({ name: 'hello', version: '1.0.0' });
```
That's the entire plugin. Drop the folder in, reload, the button appears.
### Backend hooks (PHP)
If a plugin needs server-side code, set `"backend": "backend.php"` in the manifest. The file is `require_once`'d during bootstrap. Backend plugins can use `PluginManager::instance()->addToSlot('slot-name', '<html>')` to inject HTML into named slots in `index.php` (e.g. `md-toolbar`).
### Removing a plugin
```bash
rm -rf public/plugins/canvas
```
That's it. The toolbar button, overlay, CSS, and markdown handler all disappear together. The core has no awareness of any specific plugin.
## Data & Backup
All data lives in two places:
- `data/hyprcrd.db` — SQLite database (cards, buckets, settings)
- `public/uploads/` — uploaded images and audio
Back up both to preserve everything. The database auto-migrates on startup.
## Export Formats
- **JSON export** (`export` button) — full backup with media, re-importable
- **Markdown vault** (`vault` button) — Obsidian/Logseq-compatible structure:
```
Bucket Name/
Card Title.md # YAML frontmatter + markdown body
attachments/
image.jpg
```
## CalDAV Sync (ownCloud / Nextcloud)
hyprcrd can sync appointment cards with any CalDAV server (tested with ownCloud 10.15.3.0).
### Setup
1. Open the **calendar** (button in header)
2. Click the **gear icon** in the calendar header
3. Enter your CalDAV URL, username, and app password:
```
URL: https://cloud.example.com/remote.php/dav/calendars/your-user/personal/
Username: your-user
Password: your-app-password
```
4. Click **test** to verify the connection
5. Click **save**
### How it works
- Cards tagged with `#termin` are synced as calendar events
- The appointment card template maps to iCalendar fields:
```markdown
Title: 2026-04-15 — Team Meeting
**Zeit:** 14:30-15:30
**Ort:** Room 42
**Teilnehmer:** Alice, Bob
---
**Notizen:**
Agenda items here
#termin
```
- **Sync is bidirectional**: local changes push to the server, remote changes pull to hyprcrd
- **Conflict resolution**: last-modified timestamp wins
- **Deleted cards**: tracked via tombstones, deleted on the server on next sync
- **Auto-sync**: triggers when opening the calendar (max once per minute)
- **Manual sync**: click the cloud icon in the calendar header
### Security
- Credentials are encrypted at rest (AES-256-CBC) in the SQLite database
- All sync operations run server-side (PHP/cURL), no client-side credential exposure
- Supports HTTPS with certificate verification
## Calendar
The built-in calendar shows cards on their dates:
- **Journal cards** (title = `2026-04-08`) appear on that date
- **Appointment cards** (title = `2026-04-08 — Subject`) appear on their date
- **Other cards** fall back to their creation date
- **Click a day** to see all cards in a popup
- **Double-click** or use **+ journal** / **+ termin** to create new entries
- Appointment template includes Zeit, Ort, Teilnehmer, Notizen fields
- Navigate months with arrow buttons, **today** jumps to current date
## Vault Key (Multi-Instance Encryption)
When running multiple hyprcrd instances that share CalDAV credentials, you need a shared encryption key.
### Setup
1. Go to **Settings > Account > Vault Key**
2. Enter the same key (min 8 characters) on all instances
3. Click **save**
### How it works
- The vault key encrypts/decrypts CalDAV credentials stored in the database
- Key derivation: `SHA-256(vault_key)` → deterministic, same on all instances
- Without a vault key, encryption falls back to the login password hash (single-instance only)
- Changing the vault key automatically re-encrypts CalDAV credentials
- Changing the login password does NOT affect encryption when a vault key is set
### Important
- The vault key is stored in plaintext in each instance's database — it is NOT synced
- You must enter it manually on each connected instance
## Relay
A self-hosted **multi-tenant** message relay for hyprcrd. Multiple users
share one relay instance — each one has their own login, their own
client encryption secret, and their own inbox. Cards (and in the future:
encrypted chat messages) can be sent **between tenants** on the same
relay.
**Full documentation:** [`relay/README.md`](relay/README.md)
### Architecture
```
[hyprcrd alice] ──push──┐ ┌──poll── [hyprcrd alice]
├──► [Relay] ◄────────────┤
[hyprcrd bob] ──push──┘ tenants: alice, bob └──poll── [hyprcrd bob]
sender encrypts → relay decrypts with sender's secret →
re-encrypts with shared inter-tenant key → stores →
on recipient poll: decrypts → re-encrypts with recipient's secret
```
### Deploy
**Webspace** (any PHP host with `mod_rewrite` and `pdo_sqlite`):
```bash
cd relay
./dist.sh
# upload hyprcrd-relay.zip and extract into your webspace
```
**Docker**:
```bash
cd relay
docker build -t hyprcrd-relay .
docker run -d -p 8081:80 -v relay-data:/var/www/html/data hyprcrd-relay
```
The `data/` directory is created on first request and contains the
SQLite DB plus the master encryption key (chmod 600, blocked from web
access via `.htaccess`).
### Development (both together)
```bash
./dev.sh
```
Starts hyprcrd on `http://localhost:8088` and the relay on `http://localhost:8081` in the same Docker network. Source files are bind-mounted — changes are live without rebuilding. From inside the hyprcrd container, the relay is reachable at `http://relay:80`.
Under the hood this runs `docker compose --profile relay up --build`. The relay service lives behind the `relay` compose profile, so a plain `docker compose up` brings up only the app — handy when you're developing against a remote relay.
### Setup workflow
1. Open the relay URL **from a desktop browser** — the setup wizard
is desktop-friendly
2. Create the first **admin** account: username + password (UI login)
+ shared secret (client encryption)
3. As admin, click the **users** tab and create accounts for any
additional users you want to give relay access to
4. Each user enters their credentials in their own hyprcrd:
**Settings → Relay** → relay URL + username + shared secret
5. To send a card from one tenant to another, use the relay UI's
compose form (recipient picker) — or push cards from hyprcrd
### How it works (high level)
- **Per-tenant inboxes**: each user only sees their own messages
- **Cross-tenant routing**: send a card to another tenant on the same
relay; the relay decrypts with the sender's secret, re-encrypts with
a shared inter-tenant key for storage, then re-encrypts with the
recipient's secret on delivery
- **At-rest encryption**: every sensitive value in the DB (tenant
secrets, the inter-tenant key, message bodies) is encrypted. The
master key lives outside the DB in a chmod-600 file
- **24-hour ephemeral**: messages and attachments auto-delete after a
day. Cards extracted in hyprcrd are the long-term store
- **Encrypted chat**: tenant-to-tenant chat with delivery + read receipts
(via chat plugin). Per-pair E2E key exchange is on the roadmap
### Relay Security
- Per-tenant bcrypt-hashed passwords + secrets
- Master key encryption for tenant secrets and the inter-tenant key
at rest
- HttpOnly + SameSite=Strict session cookies
- Rate-limited login (5 attempts/minute per IP)
- AES-256-CBC for all message encryption
- Security headers (X-Frame-Options, HSTS, X-Content-Type-Options)
- `data/` directory blocked from direct download via `.htaccess`
- Request size limited to 256KB (15MB for attachments)
- Last-admin protection prevents accidental lockout
For the full security model (including what's NOT protected and the
roadmap to strict E2E for chat), see [`relay/README.md`](relay/README.md).
### Migrating from the old single-tenant relay
When you upgrade an existing relay installation, the new code automatically
detects the legacy schema and drops the old `messages` and `attachments`
tables (the data is 24h-ephemeral anyway). The setup wizard appears on
your next visit. You'll need to:
1. Run through the desktop setup wizard to create the first admin
2. Recreate any other tenant accounts that need access
3. On every connected hyprcrd, update **Settings → Relay** to add the
new **username** field and the new shared secret
The old `relay_key` was bcrypt-hashed and cannot be recovered, so all
client secrets need to be reissued.
## Electron Desktop App
hyprcrd can run as a standalone desktop app via Electron — no web server needed.
```bash
cd electron
./fetch-php.sh # download static PHP binary (~20MB)
npm install
npm start # dev mode
npm run dist # build AppImage / DMG / NSIS
```
The app bundles a static PHP binary per platform and stores user data (database + uploads) outside the app bundle:
- **Linux**: `~/.config/hyprcrd/data/`
- **macOS**: `~/Library/Application Support/hyprcrd/data/`
- **Windows**: `%APPDATA%/hyprcrd/data/`
See [`electron/README.md`](electron/README.md) for details.
## Rebuilding Tailwind CSS
After changing Tailwind utility classes in HTML or JS files:
```bash
# Download standalone CLI (once)
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && chmod +x tailwindcss
# Build (includes plugins so plugin HTML/JS contributes utility classes)
./tailwindcss --input public/css/tailwind.css --output public/css/tailwind.out.css --content "public/index.php,public/js/**/*.js,public/plugins/**/*.{js,html}" --minify
```
## Security
- Bcrypt password hashing
- CSRF protection on all mutations
- Rate-limited login (5 attempts/60s)
- Upload directory blocks PHP execution
- DOMPurify sanitizes all rendered markdown
- Security headers (X-Frame-Options, HSTS, etc.)
- AES-256-CBC encryption for CalDAV credentials and Relay messages
- Vault key system for multi-instance CalDAV encryption (independent of login password)
- Relay: separate password + shared secret, rate-limited, client-side encryption
## License
MIT