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
17 changes: 17 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,24 @@ on:
workflow_dispatch:

jobs:
preflight:
runs-on: ubuntu-latest
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: Check Vercel secrets
id: check
run: |
if [ -n "${{ secrets.VERCEL_TOKEN }}" ] && [ -n "${{ secrets.VERCEL_ORG_ID }}" ] && [ -n "${{ secrets.VERCEL_PROJECT_ID }}" ]; then
echo "has-secrets=true" >> "$GITHUB_OUTPUT"
else
echo "has-secrets=false" >> "$GITHUB_OUTPUT"
echo "Vercel secrets not configured; skipping deploy."
fi

deploy:
needs: preflight
if: needs.preflight.outputs.has-secrets == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ Before submitting a PR:
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — Vercel deployment
- [docs/TESTING.md](docs/TESTING.md) — Testing checklist

When you change behavior (storage, sharing, limits, encryption), update the relevant docs and README sections so the system remains self-explanatory.

## Deployment Notes

- This is a **static-only** app. Do not add Edge Functions or serverless code.
- GitHub Actions deploys to Vercel only when repository secrets are configured.

## Questions?

Open an issue for bugs, feature requests, or questions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ python -m http.server 8000

- **Block editor** — Notion-style blocks: `/` for slash commands, Tab/Shift+Tab to indent, drag handles to reorder. Supports headings, lists, todos, code, quotes, callouts, dividers, toggles.
- **URL as storage** — Content is compressed (binary + deflate/brotli + base85) and stored in the URL hash. ~25–40% more capacity than before. No backend, no accounts.
- **Share by link** — Copy the URL to share; recipients see the same content when they open it.
- **Share by link** — Copy the URL to share; recipients see the same content when they open it. Share/QR links are computed from the current saved hash.
- **QR code** — Use `/qr` for a QR code of the current page; a small QR is also shown in the bottom-right panel.
- **Local version history** — Git-like branching stored in `localStorage`: each version has a hash, timestamp, and parent. You can undo/redo across versions and create branches by editing an older version and saving.
- **History graph** — In the **History** menu: SVG graph (time left→right, branches stacked) and a vertical list. Click a version to **preview** it (read-only with a banner); from there you can **Restore this version** or **Back to current**.
- **Version undo/redo** — Buttons in the History menu step to parent (undo) or forward along the redo stack (redo). Editor undo/redo remains `Ctrl+Z` / `Ctrl+Y`.
- **Reset** — Clears version history and keeps only the current document as a single version.
- **Export** — Share menu: **Copy link**, **Export TXT**, **Export HTML**, **Export MD**.
- **Lock** — Share menu: **Lock with password** encrypts content (AES-GCM). Share the password separately with recipients.
- **URL size limit** — Status bar shows URL size; at 8 KB input is blocked (typing disabled, paste truncated). Delete content to continue.
- **Lock** — Share menu: **Lock with password** encrypts content (AES-GCM). Requires HTTPS or localhost. Share the password separately with recipients.
- **URL size limit** — Status bar shows the computed share URL size with thresholds (safe/warning/danger/critical). At 8 KB input is blocked (typing disabled, paste truncated). Delete content to continue.
- **Theme** — Light/dark via `?theme=light` or `?theme=dark`, or system preference.
- **PWA** — Installable; manifest and service worker for offline support.

Expand Down Expand Up @@ -72,17 +72,49 @@ kheMessage/

---

## Deployment (static-only)

This project is a **static PWA**: there are no serverless or Edge Functions. Deploy the files as-is on any static host (Vercel, Netlify, GitHub Pages). Vercel configuration lives in `vercel.json` and only defines rewrites/headers/caching.

## How it works (overview)

1. You type in a **block editor** (Notion-style): each line is a block. Use `/` for slash commands, Tab/Shift+Tab to indent, drag handles to reorder. Content is Markdown-friendly with inline formatting.
2. Changes are **debounced** (e.g. 1200 ms); then **save** runs: content + optional inline styles are compressed, turned into a hash, and written to the URL and `localStorage`. A **version entry** `{ hash, t, parents }` is appended to local history.
3. **Version undo** moves to the parent of the current head and pushes the current head onto a redo stack; **version redo** pops from that stack. Any new save clears the redo stack and can create a new branch.
4. **Preview** loads a chosen version into the editor (read-only), with a banner to restore that version or return to the current head.
2. Changes are **debounced** (e.g. 1200 ms); then **save** runs: blocks → serialize → compress → hash. The hash is written to the URL and `localStorage`. A **version entry** `{ hash, t, parents }` is appended to local history.
3. **Share/QR** uses the computed share URL (origin + path + optional theme + hash) so the status bar and QR match what you can copy.
4. **Version undo** moves to the parent of the current head and pushes the current head onto a redo stack; **version redo** pops from that stack. Any new save clears the redo stack and can create a new branch.
5. **Preview** loads a chosen version into the editor (read-only), with a banner to restore that version or return to the current head.

For a deeper dive, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

---

## Storage, limits, and sharing (details)

### URL storage pipeline

Blocks are serialized into a compact binary format, compressed (brotli when available, otherwise deflate-raw), then encoded into a URL-safe base85 or base64url hash. The hash is stored in the URL fragment (`#...`) so there is **no backend** and nothing is sent to a server when you type.

### URL size thresholds

The status bar shows the byte size of the computed share URL. Thresholds are conservative:

- **safe**: 2 KB
- **warning**: 4 KB
- **danger**: 8 KB (input blocked)
- **critical**: 16 KB (most platforms reject these URLs)

These limits are heuristics; browsers and platforms vary in their actual max URL length. The app errs on the safe side.

### Share link and QR

The **Share → Copy link** action saves first (when possible) and copies the computed share URL. QR codes use the same computed URL so QR density matches the actual link length.

### Encryption (password lock)

When you lock a note, the serialized blocks are encrypted with **AES‑GCM** using a key derived from the password (PBKDF2, SHA‑256). Encryption requires **HTTPS or localhost** because WebCrypto is only available in secure contexts.

---

## Architecture

```mermaid
Expand Down
25 changes: 25 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ flowchart TB
- `hash` — Current head hash (compressed content)
- `kheMessage_localVersions` — Array of `{ hash, t, parents }` entries (max 50)

## Share URL + Size Metrics

The UI (status bar, QR, and copy link) is driven by a computed **share URL** rather than raw `location.href`:

1. **Compute hash** from current blocks (`getShortestHash`).
2. **Build share URL**: `origin + path + ?theme + #hash`.
3. **Cache size** in bytes to avoid expensive recomputation on every keystroke.

This ensures the size indicator and QR code reflect the **actual shareable link** (not a stale hash).

### Size Thresholds

The status bar uses conservative thresholds to warn early:

- **safe**: 2 KB
- **warning**: 4 KB
- **danger**: 8 KB (input blocked)
- **critical**: 16 KB (link likely fails in many contexts)

These are **heuristics**. Real platform limits vary, so the app prevents writes past the danger threshold to avoid broken links.

## Version History

kheMessage implements a Git-like version history with branching and merge support.
Expand Down Expand Up @@ -198,3 +219,7 @@ flowchart TB
```

Cache version is defined in `sw.js` as `CACHE_VERSION`; updating it invalidates old caches.

## Encryption Context

Password lock uses WebCrypto (AES‑GCM + PBKDF2). WebCrypto only works in secure contexts, so encryption is available on **HTTPS or localhost**.
31 changes: 19 additions & 12 deletions docs/CODE_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ A line-by-line map of the main application file and key functions.
| ~2310–2459 | Binary serialization | `serializeBlocks`, `deserializeBlocks` |
| ~2459–2525 | Base85 encoding | `encodeBase85`, `decodeBase85`, `looksLikeBase85` |
| ~2525–2585 | Compression | `compressBlocks`, `decompressToBlocks` |
| ~2585–2781 | Encryption | `encryptBlocks`, `decryptBlocks`, AES-GCM, PBKDF2 |
| ~2585–2781 | Encryption | `encryptBlocks`, `decryptToBlocks`, AES-GCM, PBKDF2 |
| ~2781–3030 | Block rendering | `renderBlocks`, `blocksToDOM`, `domToBlocks` |
| ~3030–3382 | Block operations | `insertBlock`, `deleteBlock`, `moveBlock`, `setBlockType` |
| ~3382–3471 | Slash menu | Slash command handling, type selection |
| ~3471–3501 | Multi-select | Shift+click range selection |
| ~3501–3597 | Drag and drop | Block reordering, indent level on drop |
| ~3597–3624 | URL limit | 8KB enforcement, typing disabled at limit |
| ~3624–3731 | Status bar | URL size display, size warnings |
| ~3731–3908 | Save/load | `save`, `set`, `setFromHash`, `get`, `blocksToHash`, `hashToBlocks` |
| ~3908–3962 | Theme | Light/dark toggle, URL param, system preference |
| ~3962–4520 | Version history | `pushLocalVersion`, `versionUndo`, `versionRedo`, `enterPreview`, `exitPreview` |
| ~4520–4588 | UI helpers | `setSaveStatus`, `notify`, `debounce`, `updateQR` |
| ~3597–3720 | URL metrics + limit | `buildShareUrl`, URL size caching, 8KB enforcement |
| ~3720–3840 | Status bar | URL size display, size warnings |
| ~3840–4030 | Save/load | `save`, `load`, `setFromHash`, URL updates |
| ~4030–4125 | Theme | Light/dark toggle, URL param, system preference |
| ~4125–4685 | Version history | `pushLocalVersion`, `versionUndo`, `versionRedo`, `enterPreview`, `exitPreview` |
| ~4685–4760 | UI helpers | `setSaveStatus`, `notify`, `updateQR` |
| ~4588–4727 | Export | `downloadTXT`, `downloadHTML`, `downloadMD` |
| ~4727–5100 | Initialization | DOM ready, hash load, event listeners, Editor setup |

Expand All @@ -42,11 +42,10 @@ A line-by-line map of the main application file and key functions.
| Function | Description |
|----------|-------------|
| `save()` | Gets blocks → compresses → updates URL hash + localStorage, pushes version |
| `set(hash)` | Decompresses hash → sets blocks → renders DOM |
| `setFromHash(hash)` | Async version of `set`; handles encrypted content |
| `get()` | Returns string representation of content + style |
| `blocksToHash(blocks)` | Serializes and compresses blocks to hash string |
| `hashToBlocks(hashStr)` | Decompresses and deserializes hash to blocks |
| `load()` | Resolves hash from URL/localStorage → renders blocks |
| `setFromHash(hash)` | Async load; handles encrypted content |
| `getShortestHash(blocks)` | Computes best hash encoding (binary vs compact) |
| `buildShareUrl(hash)` | Builds share URL for size/QR/copy |

### Version History

Expand All @@ -59,6 +58,14 @@ A line-by-line map of the main application file and key functions.
| `exitPreview()` | Returns to current head, editable |
| `clearVersionHistory()` | Clears all history (Reset) |

### URL Size + Share Metrics

| Function | Description |
|----------|-------------|
| `refreshUrlMetrics()` | Computes cached share URL + byte size |
| `syncUrlMetricsToLocation()` | Syncs metrics to `location.href` (preview/lock) |
| `isAtInputLimit()` | Enforces danger threshold (8 KB) based on share URL size |

### Block Types (BLOCK_TYPES)

| Value | Type | Description |
Expand Down
5 changes: 5 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The repository includes `.github/workflows/deploy.yaml` that:
- **Production**: Automatic deploy on push to main branches
- **Preview**: Automatic deploy on pull requests (URL commented on PR)
- **Action**: Uses `amondnet/vercel-action@v25`
- **Secrets guard**: Deploy steps are skipped when required secrets are missing (common for forked PRs).

### Required GitHub Secrets

Expand Down Expand Up @@ -75,6 +76,10 @@ The project includes `vercel.json` with:
- `qrcode.js`: `max-age=31536000, immutable` (1 year)
- Cursor files: `max-age=31536000, immutable` (1 year)

## No Edge Functions

This project is **static-only** and does not use Vercel Edge Functions or serverless functions. All functionality is client-side in `index.html`.

## Custom Domain

1. Add your domain in Vercel project settings
Expand Down
8 changes: 8 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ php -S localhost:8000

Then visit `http://localhost:8000`.

> **Tip:** Encryption (password lock) requires HTTPS or localhost because WebCrypto is only available in secure contexts.

### Option 3: VS Code Live Server

Use the Live Server extension to serve the project.
Expand Down Expand Up @@ -69,6 +71,12 @@ See [docs/TESTING.md](TESTING.md) for the full testing checklist. Before submitt
4. Test keyboard navigation
5. Check console for errors

## URL Storage + Limits (Developer Notes)

- **Share URL computation**: The app computes the share URL from the current saved hash (origin + path + optional theme + hash). Status bar and QR use this computed URL so the displayed size matches the actual share link.
- **Limits are conservative**: The 8 KB “danger” threshold is a safety limit; real platform maximums vary. The app blocks input at the danger threshold to prevent broken links.
- **Local files**: When running from `file://`, share URLs reuse the current file path + hash because `location.origin` is `null`.

## Cursor IDE

The project includes `.cursor/` with:
Expand Down
7 changes: 5 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ Before deployment, ensure these secrets are configured in GitHub repository sett
- [ ] URL updates after typing (debounced)
- [ ] Refreshing page preserves content
- [ ] Sharing URL works - recipient sees same content
- [ ] URL size indicator shows in status bar
- [ ] 8KB limit enforced (typing disabled at limit)
- [ ] URL size indicator shows in status bar (matches share URL)
- [ ] 8KB danger limit enforced (typing disabled at limit; paste truncated)
- [ ] Size warnings appear at 4KB/8KB/16KB thresholds

#### Version History

Expand All @@ -82,6 +83,7 @@ Before deployment, ensure these secrets are configured in GitHub repository sett
- [ ] Export TXT downloads correctly
- [ ] Export HTML downloads correctly
- [ ] Export MD downloads correctly
- [ ] Lock/unlock works (AES-GCM) on HTTPS/localhost

#### QR Code

Expand Down Expand Up @@ -188,6 +190,7 @@ Verify cache headers:
- [ ] URL with invalid base64
- [ ] localStorage full/disabled
- [ ] Service worker registration fails
- [ ] Password lock unavailable on insecure origins (shows message)

## Performance Metrics

Expand Down
Loading