Commit Graph
5 Commits
Author SHA1 Message Date
Tom FluxandClaude Opus 5 8338374a8d Follow the OS dark mode on the login page
The board's dark theme is a saved preference, which there is no way to
know before you're signed in, so the login page follows
prefers-color-scheme instead. Palette taken from nullboard's .theme-dark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
2026-08-14 22:44:41 +01:00
Tom FluxandClaude Opus 5 00d3cb0e61 Add systemd unit, nginx config and a README
The nginx block is plain http; certbot adds the TLS server block and the
redirect itself. It sets X-Forwarded-Proto, which is load-bearing: the
app compares the browser's Origin against the URL it believes it is
serving, and without that header it thinks it is on http while the
browser says https, decides every save is cross-site and drops the
session.

Upstream's README is kept as README.nullboard.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
2026-08-14 22:41:11 +01:00
Tom FluxandClaude Opus 5 d94984cd97 Add tests for the store and the API
Rack::MockRequest with a cookie jar rather than rack-test, so the suite
needs nothing beyond the Debian packages the app already uses.

Covers key validation and batch atomicity, password hashing, the schema
version guard, auth and lockout, the storage API, and that a note
containing a closing script tag can't break out of the bootstrap block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
2026-08-14 22:41:11 +01:00
Tom FluxandClaude Opus 5 7e2f280acc Store boards on the server rather than in localStorage
Adds Storage_Server next to upstream's Storage_Local and swaps which one
is instantiated; the rest of nullboard.html is untouched so upstream
changes still merge.

The Storage contract is synchronous — setItem has to return true or false
there and then — which no round trip can satisfy. So the whole keyspace
is held in memory and the server is somewhere to push it to:

  - The initial copy is embedded in the page by the server, already
    parsed and in hand before the app boots. In the page rather than a
    .js file so another site can't <script src> it and read the boards.
  - Writes are collected by key, so a note edited repeatedly is one
    write, and flushed shortly after as a single transaction. A failed
    batch goes back on the queue and retries with a backoff rather than
    being dropped.
  - The logo shows saving/not saved, and closing the tab with writes
    still queued asks first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
2026-08-14 22:41:01 +01:00
Tom FluxandClaude Opus 5 2ae11e8d1e Add a Sinatra server that keeps boards in SQLite
Nullboard's storage layer bottoms out in getItem/setItem/delItem over
string keys, with boards, revisions and undo history all modelled on top
of them client-side. So the server needs to be nothing more than a
key/value store, and it doesn't parse a board anywhere.

One password, bcrypt-hashed, in the same database as the boards. The
session secret lives there too, so restarting the service doesn't sign
you out and the secret never has to exist in the unit file or in git.

Until a password is set every route returns 503 pointing at
bin/validboard-passwd. A first-run setup page would be friendlier, but it
would also hand the board to whoever found the URL first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
2026-08-14 22:40:51 +01:00