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
4 changes: 4 additions & 0 deletions .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jobs:
with:
path: ~/.npm
key: pr-gate-npm-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
# The web app builds first so the Rust build embeds real interface
# assets and the asset-serving tests exercise the strict branch.
- name: Web install
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ jobs:
rustup target add ${{ matrix.target }}
sudo apt-get update -q
sudo apt-get install -y -q musl-tools
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
- name: Web build
working-directory: web
run: npm ci && npm run build
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ not a production-readiness statement.

### Changed

- The interface now needs Chrome or Edge 111+, Firefox 114+, or Safari
16.4+ (iOS 16.4+). Firefox ESR 102 and earlier and Chrome or Edge before
111 no longer load it; see the [install guide](docs/install.md).
- The web interface is built with SvelteKit 3, Vite 8, Svelte 5.57, and
TypeScript 6 (from SvelteKit 2 and Vite 7), which also drops a
vulnerable `cookie` dependency from the build
([#54](https://github.com/FieldmouseWorks/consolebook/issues/54)). Building
it needs Node.js `^22.17 || >=24`; the installed server still needs no
Node.js.
- Building from source needs Rust 1.99 (`rust-toolchain.toml`), and the
bundled SQLite is now 3.51.3 (was 3.46.0) with sqlx 0.9. Existing data
directories open unchanged: their recorded migrations, records, exports,
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ together, with versioning governed by that format's contract.
## Build and verification

Use the Rust toolchain pinned in `rust-toolchain.toml` and npm's committed
lockfile. The Vite 7 dependency declares Node.js `^20.19.0 || >=22.12.0`;
use a compatible supported Node.js release. Node.js is build-time only.
lockfile. `web/package.json` declares Node.js `^22.17 || >=24` (SvelteKit 3
requires 22.17; vite-plugin-svelte 7 excludes 23.x); CI builds with Node.js 24.
Node.js is build-time only.

From the repository root, run in this order:

Expand Down
4 changes: 4 additions & 0 deletions docs/decisions/0005-embedded-web-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

- **Status:** Accepted
- **Date:** 2026-08-28
- **Amended:** 2026-10-15 by [#54](https://github.com/FieldmouseWorks/consolebook/issues/54):
the toolchain is now SvelteKit 3 on Vite 8, with `#lib` subpath imports
and the adapter configured in `web/vite.config.ts`; the decision is
otherwise unchanged.

## Context

Expand Down
4 changes: 4 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ first program, people, and training session. You need a Linux machine with
systemd (Ubuntu, Debian, RHEL, Rocky, Alma, Fedora, and similar), x86_64 or
arm64, and an administrator account that can use `sudo`.

**Browsers:** people use Consolebook in Chrome or Edge 111 or later, Firefox
114 or later, or Safari 16.4 or later (iOS 16.4 or later). Older browsers,
including Firefox ESR 102 and earlier, do not load the interface.

Consolebook is pre-alpha. Use invented data until your agency has decided it is
ready for real records. [ADR 0023](decisions/0023-linux-installation-lifecycle.md)
records the locations, service account, and upgrade behavior this page relies on.
Expand Down
42 changes: 26 additions & 16 deletions web/e2e/draft-recovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3251,15 +3251,14 @@ test('overlapping SPA navigation keeps the draft locked until the current route
await expect(author.getByLabel(MOST)).toBeEnabled();
await expect(author).toHaveURL(new RegExp(`${seeded.draftUrl}$`));
expect(await author.evaluate(() => history.length)).toBe(entryCount);
await expect(author.getByRole('button', { name: 'Resume editing this draft' })).toHaveCount(0);
await expectSpaAlive(author);
} finally {
firstLayout.release();
await authorContext.close();
}
});

test('a stranded router state requires an explicit same-entry resume', async ({
test('a shallow traversal that aborts a held departure leaves the draft editable in place', async ({
page,
setupCode,
browser
Expand All @@ -3274,16 +3273,25 @@ test('a stranded router state requires an explicit same-entry resume', async ({
await markSpa(author);
await homeSessions(author);
await openDraftFromHome(author, '2026-06-02', seeded.draftId);
const originalState = await author.evaluate(() => history.state);
// Recreate a shallow popstate that resets the router token while
// the destination GET is held. Native hash entries need the active
// SvelteKit state copied onto them for this synthetic boundary.
await author.evaluate(() => { location.hash = 'earlier'; });
// Two in-app hash links give this draft shallow history entries that
// SvelteKit itself records. A shallow popstate while the destination
// GET is held resets the router token and aborts that departure.
// SvelteKit 2 left `navigating` published after that abort, stranding
// the editor frozen; SvelteKit 3 clears it (sveltejs/kit#17118), so the
// editor must come back on its own, on the same entry.
await author.evaluate(() => {
for (const name of ['earlier', 'current']) {
const link = document.createElement('a');
link.href = `#${name}`;
link.id = `hash-${name}`;
link.textContent = name;
document.body.append(link);
}
});
await author.locator('#hash-earlier').click();
await expect(author).toHaveURL(/#earlier$/);
await author.evaluate((state) => history.replaceState(state, '', location.href), originalState);
await author.evaluate(() => { location.hash = 'current'; });
await author.locator('#hash-current').click();
await expect(author).toHaveURL(/#current$/);
await author.evaluate((state) => history.replaceState(state, '', location.href), originalState);
const entryCount = await author.evaluate(() => history.length);
await author.route('**/api/instance', async (route) => {
const response = await route.fetch();
Expand All @@ -3298,16 +3306,18 @@ test('a stranded router state requires an explicit same-entry resume', async ({
await expect(author).toHaveURL(/#earlier$/);
await author.evaluate(() => history.forward());
await expect(author).toHaveURL(/#current$/);
await expect(author.getByLabel(MOST)).toBeEnabled();
// Edit and save while the abandoned departure's layout load is still
// held: its late answer must not carry the page away from the draft.
const typed = 'Invented text typed during the held window.';
await author.getByLabel(MOST).fill(typed);
await expectSaved(author);
layout.release();
await author.waitForLoadState('networkidle');
await expect(author.getByLabel(MOST)).toBeDisabled();
await expect(author.getByRole('button', { name: 'Resume editing this draft' })).toBeVisible();
await author.getByRole('button', { name: 'Resume editing this draft' }).click();
await expect(author.getByLabel(MOST)).toBeEnabled();
await expect(author).toHaveURL(new RegExp(`${seeded.draftUrl}#current$`));
await expect(author.getByLabel(MOST)).toBeEnabled();
await expect(author.getByLabel(MOST)).toHaveValue(typed);
expect(await author.evaluate(() => history.length)).toBe(entryCount);
expect(await author.evaluate(() => history.state['sveltekit:states']))
.toEqual(originalState['sveltekit:states']);
const unsaved = 'Invented post-abort note.';
await author.route(`**/api/drafts/${seeded.draftId}/content`, async (route) => {
await route.abort('failed');
Expand Down
Loading
Loading