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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ jobs:
- name: Multi-window history smoke
run: npm run test:multi-window-history-smoke

- name: Split view animation smoke
run: npm run test:split-smoke

- name: Keyboard guards smoke
run: npm run test:keyboard-guards-smoke

Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ Prefer to browse one board at a time? Switch to **Focus Flow** for large preview

### More than a collection of images

Keep the thinking alongside the references. Add text notes, lists, drawings, and arrows; tag images to find them again; and pull a color palette from a reference.
Keep the thinking alongside the references. Add text notes, lists, drawings, and arrows; label images to find them again; and pull a color palette from a reference.

| Tool | What you can do |
|---|---|
| **Arrange & group** | Resize with proportions intact, rotate, snap to nearby items, and organize references in named groups. |
| **Notes & drawing** | Add formatted notes, checklists, pen marks, and arrows to explain an idea. |
| **Image tools** | Crop, flip, view in grayscale, and sample colors with the eyedropper. |
| **Tags & search** | Label references, filter by tag, and search within a board. |
| **Labels & search** | Label references, filter by label, and search within a board. |
| **Navigation** | Zoom at the cursor, pan across the canvas, fit the selection or whole board, and jump around with the minimap. |
| **Appearance** | Choose from six dark themes, toggle the dot grid, and keep tools compact or always visible. |

Expand All @@ -104,9 +104,10 @@ like any other document.

Saving adds only new or changed images to the file, so a save on a large
board takes a moment instead of rewriting everything. Boards from older
versions open as before and become the current format on their next save,
with the previous file kept once as `.refboard.bak`. Boards saved by 2.1.2 or
later need **RefBoard 2.1.2 or later**. See [board files](docs/BOARD_FILES.md).
versions open as before and become the current format on their next save.
A temporary `.refboard.bak` protects file replacement and is removed after a
successful save; an interrupted replacement keeps it available for recovery.
Boards saved by 2.1.2 or later need **RefBoard 2.1.2 or later**. See [board files](docs/BOARD_FILES.md).

## Keyboard shortcuts

Expand All @@ -120,7 +121,7 @@ later need **RefBoard 2.1.2 or later**. See [board files](docs/BOARD_FILES.md).
| Pan / zoom | **Space+drag** / **Mouse wheel** |
| Add a text note | **Shift+T** |
| Group / ungroup | **Ctrl+G** / **Ctrl+Shift+G** |
| Search / tags | **Ctrl+F** / **Ctrl+Shift+T** |
| Search / labels | **Ctrl+F** / **Ctrl+Shift+T** |
| Toggle the minimap | **M** |
| Undo / redo | **Ctrl+Z** / **Ctrl+Shift+Z** |
| Export board / images | **Ctrl+E** / **Ctrl+Shift+I** |
Expand Down
43 changes: 32 additions & 11 deletions bootstrapper/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const { app, BrowserWindow, ipcMain, shell } = require('electron');
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
const fs = require('fs');
Expand Down Expand Up @@ -113,8 +113,11 @@ ipcMain.handle('installer:start', async () => {
}
});

// Launch the freshly installed RefBoard, then quit the bootstrapper.
ipcMain.handle('installer:launch', async () => {
// Only leave the installer once Windows has accepted the launch. A missing or
// blocked executable must leave the Launch button available for another try.
let launchInFlight = null;

function launchInstalledApp() {
// Per-user install path used by electron-builder NSIS (perMachine:false).
const localAppData = process.env.LOCALAPPDATA || '';
const guesses = [
Expand All @@ -123,15 +126,33 @@ ipcMain.handle('installer:launch', async () => {
];
const exe = guesses.find((p) => p && fs.existsSync(p));

if (exe) {
spawn(exe, [], { detached: true, stdio: 'ignore' }).unref();
} else if (localAppData) {
// Fallback: open the Programs folder so the user can find it.
shell.openPath(path.join(localAppData, 'Programs', 'RefBoard'));
}
if (!exe) return Promise.resolve({ launched: false, reason: 'app-not-found' });

return new Promise(resolve => {
let child;
try {
child = spawn(exe, [], { detached: true, stdio: 'ignore', windowsHide: true });
} catch (err) {
resolve({ launched: false, reason: 'spawn-failed', error: String(err) });
return;
}
child.once('error', err => {
resolve({ launched: false, reason: 'process-error', error: String(err) });
});
child.once('spawn', () => {
child.unref();
resolve({ launched: true });
});
});
}

setTimeout(() => app.quit(), 400);
return { launched: Boolean(exe) };
ipcMain.handle('installer:launch', async () => {
if (launchInFlight) return launchInFlight;
launchInFlight = launchInstalledApp();
const result = await launchInFlight;
if (result.launched) setTimeout(() => app.quit(), 400);
else launchInFlight = null;
return result;
});

// Window chrome controls (your app.js calls minimize / close).
Expand Down
24 changes: 22 additions & 2 deletions build/installer-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ let autoTimer = null;
let paused = false;
let installing = false;
let installComplete = false;
let launching = false;

const sceneElements = features.map((feature, index) => {
const scene = document.createElement('div');
Expand Down Expand Up @@ -169,10 +170,29 @@ function animateInstallPhase(from, to, duration, label, meta) {

async function simulateInstall() {
if (installComplete) {
if (window.RefBoardInstaller?.launch) window.RefBoardInstaller.launch();
else {
if (launching) return;
if (!window.RefBoardInstaller?.launch) {
installState.textContent = 'Launch is ready';
installMeta.textContent = 'The packaged installer will now open RefBoard.';
return;
}
launching = true;
installButton.disabled = true;
installButton.querySelector('.button-label').textContent = 'Launching\u2026';
let result;
try {
result = await window.RefBoardInstaller.launch();
} catch {
result = { launched: false };
}
if (result?.launched !== true) {
launching = false;
installButton.disabled = false;
installButton.querySelector('.button-label').textContent = 'Retry launch';
installState.textContent = 'Could not launch RefBoard';
installMeta.textContent = result?.reason === 'app-not-found'
? 'The installed app could not be found. Run setup again to repair the installation.'
: 'Windows could not start the app. Please try again.';
}
return;
}
Expand Down
14 changes: 10 additions & 4 deletions docs/BOARD_FILES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ indexes leave dead bytes in place until enough have piled up (4 MB and a
quarter of the file), when the next save writes a fresh compact file. A save
interrupted by a crash leaves the previous state intact.

When replacing a board file, RefBoard temporarily keeps the previous file as
`Name.refboard.bak`. A successful replacement removes this temporary backup.
If replacement is interrupted and the main file is missing, RefBoard restores
the backup when opening the board or loading recent boards. It never promotes
unfinished `.saving-*` files. Backups left by older versions are removed after
the board's next successful save or preview update.

## Older boards

- Boards saved by RefBoard 2.0.x kept every image inside one JSON file. They
open as before and become the current format on their next save, with the
previous file kept once as `Name.refboard.bak`.
open as before and become the current format on their next successful save.
- Boards saved by RefBoard 2.1.0 and 2.1.1 came as a pair: `Name.refboard`
plus `Name.refboard.images`. Keep the pair together until the board has
been opened and saved once in RefBoard 2.1.2 or later; that save folds the
images into the single file, keeps the previous index as
`Name.refboard.bak`, and removes the `.images` file.
images into the single file and removes the `.images` file and temporary
`.bak` once conversion succeeds.

Boards saved by RefBoard 2.1.2 or later open only in 2.1.2 or later.
89 changes: 89 additions & 0 deletions docs/RELEASE_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Release audit — 2026-09-27

Scope: RefBoard 2.1.3 development changes, including the label workflow,
temporary backups and split animation. Two agents independently covered runtime smoke tests
and security/dead code. This records tested behavior; it is not a guarantee that
every bug or vulnerability has been found. No release was published.

## Issues fixed during this audit

- Restore label colors when opening another board, clear inherited colors when
the opened file has none, and include colors in crash-recovery session data.
The label smoke now leaves the current board before reopening its saved file;
opening the current path had previously bypassed the actual load.
- Close container and legacy sidecar handles when new-file initialization fails.
Forty-two fault-injection cases exercise truncate, write, sync, cleanup failure,
and caller ownership after success across both formats.
- Handle synchronous and asynchronous installer launch failures. Wait for the
process to spawn before quitting; failed launches keep the installer open with
Retry launch. Tests cover failed/rejected calls, retries and duplicate requests.
- Remove six unused main-process imports and the unused bootstrapper shell import.
Dynamically used renderer functions and compatibility modules were retained.

## Verification passed

- `npm test`: all 68 commands passed after the fixes, including file replacement,
crash recovery, concurrent saves, session abort, clipboard, container/sidecar
integrity, installer retry, and the new handle lifecycle checks.
- Twenty isolated Electron smoke invocations covered keyboard guards, autosave
failure, undo memory, multiple windows, split view, save/open, view/theme
persistence, image edit isolation, lossless JPEG/WebP crop exports, content-aware
worker cancellation, clipboard, landing layout, search, groups, grid rendering,
drag-out staging and zoom. History checks used 2,000 items; the 500-image zoom
check reported zero renderer errors or quality downgrades.
- Six additional final smoke runs passed: labels from source and the packaged
executable, label context menus, legacy/sidecar conversion, incremental saves,
and aborted saves. Label checks include real file reload, session color data,
mixed selections, explicit removal, Undo/Redo, filtering and keyboard controls.
- Root and bootstrapper `npm audit --json`: zero known dependency vulnerabilities
reported. Reviewed navigation restrictions, external URL handling, IPC session
ownership, file boundaries, Electron isolation and dynamic text rendering;
no confirmed exploitable issue was identified in the inspected paths.
- An isolated Windows unpacked build completed with publishing disabled. Its
label smoke passed, final runtime files matched source, and inline script
syntax checks passed. The build stayed outside the release output directory.
- `git diff --check` passed.

## Split animation follow-up

Two agents separately investigated frame pacing and renderer layout. On the
tested Windows display, a nominal 16 ms main-process timer produced roughly
31 ms layout updates while display frames arrived every 6.3 ms. Changing canvas
width also reset its unchanged height, clearing the backing surface twice per
step. After closing, a separate CSS height transition briefly squeezed the board.

The slide now uses primary-renderer animation frames, a monotonic clock, a
400 ms easing curve with smooth acceleration, and a single completion deadline
for hidden or stalled renderers. Sender and animation identity checks reject
unrelated or stale callbacks. Native bounds are applied only when changed.
The primary canvas keeps its full backing surface while clipping follows the
divider; its top moves with the slide without stretching its height. The
minimap updates independently of full-board redraws.

The final 121-item development smoke measured 6.2/6.3 ms median opening/closing
layout intervals, with 43/43 and 52/52 observed display frames advancing the
slide. Canvas dimension writes dropped from 26/22 to 2/2. These measurements
describe this machine, not a guaranteed refresh rate on every device.
Motion and 450 ms of post-close samples showed no canvas stretching beyond
one CSS pixel of DPI rounding. Reopening, divider movement, item preservation,
and renderer error checks passed. The rebuilt Windows app also passed the
enhanced split smoke. CI now runs this regression check on pushes.

Additional unit cases cover stale frame IDs, wrong senders, reversal, cancellation,
failed loading, fallback completion, frame-loop cleanup, stable canvas sizing,
and both titlebar policies.

## Remaining release checks

- Run the actual installer and upgrade an existing installation on Windows.
Installer error handling has automated coverage; end-to-end installation and
auto-update execution were not exercised by this audit.
- Manually complete native drag-and-drop into another application. Automated
coverage verifies the staged files, not another application's drop handling.
- Follow `MAINTAINING.md` for versioning, release notes, installer payload refresh
and packaging before publishing. This audit did not update release artifacts.

Local final test/build logs are in the Windows temporary directory as
`refboard-release-final-tests.log` and `refboard-release-final-build.log`.
Split follow-up logs use `refboard-smooth-final-tests.log` and
`refboard-smooth-final-build.log`.
Loading
Loading