# 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. ## 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 ServerName hyprcrd.local DocumentRoot /path/to/hyprcrd/public AllowOverride All Require all granted ``` 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 │ │ └── tests/ # UI + state test runner │ └── uploads/ # User media (writable) ├── relay/ # Relay service (mobile inbox + card sharing) │ ├── index.php # Single-file API + Mobile UI │ ├── .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`) - **Image gallery** 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 - **Keyboard shortcuts**: `Ctrl+K` search, `F1` help, `Shift+scroll` zoom, arrow keys in autocomplete ## 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. - **`tests/`** — UI + state test runner (40+ test files). Loaded only when `?test` is in the URL or `Ctrl+Shift+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=""` button dispatches to the registered handler. `api` is set after the editor initializes. | | `hyprcrd.markdown` | `registerFencedBlock(lang, fn)`, `registerPreProcessor(fn)`, `registerPostProcessor(fn)` | Add ` ``` ` 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', '')` 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 minimal web service for mobile note capture and card sharing between hyprcrd instances. ### Architecture ``` [Phone/Browser] → https://relay.example.com → [Relay Service] ↓ poll [hyprcrd (local)] [hyprcrd A] → push card → [Relay] ← poll ← [hyprcrd B] ``` ### Deploy to Webspace ```bash cd relay ./dist.sh ``` Upload the resulting `hyprcrd-relay.zip` to your webspace and extract it. The SQLite database is created automatically on first request. Open the URL and set a password + shared secret. Requirements: PHP 8.3+ with `pdo_sqlite`, Apache with `mod_rewrite`. ### Deploy with Docker ```bash cd relay docker build -t hyprcrd-relay . docker run -d -p 8081:80 -v relay-data:/var/www/html/data hyprcrd-relay ``` ### 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`. ### Setup 1. Open the relay URL, set a **password** (for UI login) and a **shared secret** (for encryption) 2. In hyprcrd: **Settings > Relay** — enter the relay URL and the same shared secret 3. Click **save** ### How it works - **Mobile inbox**: type a note on the relay UI, it arrives as a card with `#inbox` tag - **Push to relay**: right-click any card → "push to relay" — other instances pick it up on next poll - Messages are encrypted client-side (AES-256-CBC) before sending to the relay - The relay only stores ciphertext — it cannot read your messages - hyprcrd polls the relay every 45 seconds in the background - Messages are kept on the relay for 24 hours, then auto-deleted - Cards pushed from the same instance are not re-imported - Enter to send, Shift+Enter for newline ### Relay Security - Separate password (UI login) and shared secret (API + encryption) - Bcrypt hashing for both credentials - Rate-limited login (5 attempts/minute) - AES-256-CBC client-side encryption — relay never sees plaintext - Security headers (X-Frame-Options, HSTS, X-Content-Type-Options) - SQLite database blocked from direct download via `.htaccess` - Request size limited to 256KB ## 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