diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f019c06d..a071dff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,8 @@ jobs: # front-end build would serialise it for nothing: it validates Rust, not the bundle. - name: Placeholder frontendDist run: | - mkdir -p dist/devbox/browser - printf 'ci' > dist/devbox/browser/index.html + mkdir -p dist/devnotes/browser + printf 'ci' > dist/devnotes/browser/index.html - run: cargo fmt --manifest-path src-tauri/Cargo.toml --check @@ -99,7 +99,7 @@ jobs: - uses: actions/upload-artifact@v7 with: name: front-dist - path: dist/devbox/browser + path: dist/devnotes/browser retention-days: 1 build-rust: @@ -117,8 +117,8 @@ jobs: - name: Placeholder frontendDist run: | - mkdir -p dist/devbox/browser - printf 'ci' > dist/devbox/browser/index.html + mkdir -p dist/devnotes/browser + printf 'ci' > dist/devnotes/browser/index.html - name: Build run: | @@ -164,7 +164,7 @@ jobs: path: coverage retention-days: 7 - # ⚠️ Windows is not redundant with Ubuntu here. DevBox ships primarily to Windows, and the + # ⚠️ Windows is not redundant with Ubuntu here. DevNotes ships primarily to Windows, and the # back-end reasons about `app_data_dir()`, path separators, file locking and SQLite — all of # which differ. Until this leg existed, nothing blocking ever ran the tests on the platform # most users are on: `test-e2e` covers Windows but is `continue-on-error`. @@ -207,8 +207,8 @@ jobs: - name: Placeholder frontendDist run: | - mkdir -p dist/devbox/browser - printf 'ci' > dist/devbox/browser/index.html + mkdir -p dist/devnotes/browser + printf 'ci' > dist/devnotes/browser/index.html - name: Test run: | @@ -267,7 +267,7 @@ jobs: - run: npm ci # `tauri build` chains `beforeBuildCommand` (the e2e front-end build) then cargo. The - # binary it produces carries the `com.devbox.app.e2e` identifier, so its profile is its + # binary it produces carries the `com.devnotes.app.e2e` identifier, so its profile is its # own: it cannot overwrite an existing library. - name: Build the e2e binary run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f1e50ef..39d84cca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: run: | pkg=$(node -p "require('./package.json').version") npm_lock=$(node -p "require('./package-lock.json').packages[''].version") - cargo_lock=$(awk '/^name = "devbox"$/{f=1;next} f&&/^version = /{print;exit}' \ + cargo_lock=$(awk '/^name = "devnotes"$/{f=1;next} f&&/^version = /{print;exit}' \ src-tauri/Cargo.lock | cut -d'"' -f2) echo "Cargo.toml=$VERSION package.json=$pkg package-lock=$npm_lock Cargo.lock=$cargo_lock" for found in "$pkg" "$npm_lock" "$cargo_lock"; do @@ -269,7 +269,7 @@ jobs: # # ⚠️ The version comes from `prepare` and not from `GITHUB_REF_NAME`: on a # `workflow_dispatch` the latter is `main`, and the binaries would be called - # `devbox-main-windows.exe`. + # `devnotes-main-windows.exe`. - name: Gather installers and the standalone executable env: VERSION: ${{ needs.prepare.outputs.version }} @@ -282,9 +282,9 @@ jobs: # The bundler does not collect the bare binary; renaming it keeps the two operating # systems from overlapping in the release. if [ '${{ matrix.target }}' = 'windows' ]; then - cp src-tauri/target/release/devbox.exe "release/devbox-${VERSION}-windows.exe" + cp src-tauri/target/release/devnotes.exe "release/devnotes-${VERSION}-windows.exe" else - cp src-tauri/target/release/devbox "release/devbox-${VERSION}-linux" + cp src-tauri/target/release/devnotes "release/devnotes-${VERSION}-linux" fi ls -l release diff --git a/CHANGELOG.md b/CHANGELOG.md index c998ae9a..c506b0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -Notable changes to DevBox, newest first. +Notable changes to DevNotes, newest first. **The newest section is generated.** `.github/workflows/release.yml` writes it from the pull requests merged since the last tag, then commits it **before** creating the tag — this file is @@ -74,7 +74,7 @@ the release notes the updater hands over. - Keep the comments that say what the code cannot (#128) - Refilm the README, and drop the screenshot the GIF repeats (#113) - Give a card title its width back, and plant the pin in the corner (#114) -- Say which platforms DevBox ships for, and stop implying macOS (#110) +- Say which platforms DevNotes ships for, and stop implying macOS (#110) - Make the e2e suite pass, and fix the data loss it was reporting - Film the README on a full board (#105) - Show a note created while its editor was closing (#104) diff --git a/CLAUDE.md b/CLAUDE.md index 8e7e4997..dd91bdbc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -DevBox — a desktop "Swiss Army knife" utility app for developers (notes-taking, hashing, encoding). Front-end is **Angular 22** (standalone components, signals, zoneless change detection), native engine is **Rust / Tauri v2**. +DevNotes — a desktop "Swiss Army knife" utility app for developers (notes-taking, hashing, encoding). Front-end is **Angular 22** (standalone components, signals, zoneless change detection), native engine is **Rust / Tauri v2**. The **notes feature is complete end to end**: front-end (spaces with creation, renaming, deletion and filtering, search, filters, tag rail, sections, full note editing — content, format, tags, pin, deadline, move to another space, deletion), IPC (no mock data left, the Rust backend is the only data source, every read/write goes through the tauri-specta bindings generated from the Rust signatures), and Rust (persistence to an embedded SQLite database). Built on top of that: a 30-day trash with undo, multiple selection with bulk actions, corpus-wide tag management, `{{fields}}` in snippets, a quick-paste palette on a global shortcut, attachments, import / export / share, and a second **note kind** — the todo list, which has an ordered list of items instead of a body. The titlebar's "À propos" menu also carries the three help panels — "Nouveautés" (the shipped `CHANGELOG.md`, parsed in Rust), "Prise en main" (a written guide) and "Raccourcis clavier" (a read-only sheet fed by a registry) — and a fresh install opens on sample notes rather than on an empty canvas. There is no crypto or formatters module: they were removed once it became clear they would ship dead code. @@ -51,7 +51,7 @@ Run all commands from the repo root (`package.json` there wraps both Angular and - `npm install` — install JS dependencies (also required before the first Rust build, since Tauri's build script reads front-end config). - `npm run tauri dev` — main dev loop: starts `ng serve` (hot-reload on `src/`) and launches the Tauri window, which auto-rebuilds/reloads on Rust changes (slower than the front-end hot-reload). - `npm start` / `ng serve` — Angular dev server only (port **1420**, fixed in `angular.json`; Tauri's `devUrl` in `tauri.conf.json` depends on this port). -- `npm run build` — production Angular build, output to `dist/devbox/browser` (path referenced by `frontendDist` in `tauri.conf.json`). +- `npm run build` — production Angular build, output to `dist/devnotes/browser` (path referenced by `frontendDist` in `tauri.conf.json`). - `npm run tauri build` — full production build; native executable/installer lands in `src-tauri/target/release`. - Rust-only iteration: `cargo build` / `cargo check` from `src-tauri/` (faster than a full `tauri build` when just checking Rust compile errors). - `npm test` — Angular unit tests via the `@angular/build:unit-test` builder with **Vitest** (jsdom, no browser required). `npm run test:watch` re-runs on change; `npm run test:coverage` adds a v8 coverage report with 80% thresholds. @@ -75,6 +75,7 @@ Run all commands from the repo root (`package.json` there wraps both Angular and These are the non-obvious constraints; the rest of the architecture is in `docs/architecture.md`. +- **⚠️ Four names are the address of a library, not decoration.** `app_data_dir()` is `data_dir()/identifier`, so the Tauri `identifier` (`com.devnotes.app`, and `com.devnotes.app.e2e` for the suite) is where every library lives, along with `db::DB_FILE_NAME` (`devnotes.sqlite3`) and its two sidecars, the `devnotes.` preference prefix (`PreferencesService` filters on it, `SETTINGS_KEYS` builds it, and `backup::wanted` reads `devnotes.automaticBackups` straight out of the file before the front end has booted) and the `.devnotes` export extension, which every bundle already written carries. Renaming one sends an installed copy to a virgin profile with the notes still on disk and no way in — the key file is what opens the database, and a new folder has neither. The rename to DevNotes moved all four at once and cost nothing because the application had no users yet; the next one needs a startup migration that adopts the old directory, not a `sed`. - **The end-to-end harness is a build flavour, not a flag.** `npm run e2e:build` merges `src-tauri/tauri.e2e.conf.json` and links the `e2e` Cargo feature: `withGlobalTauri`, the `wdio` capability declared **inline** (a file under `capabilities/` would ship), the `e2e` Angular configuration whose only difference is `"polyfills": ["@wdio/tauri-plugin"]`, and an identifier of its own — `app_data_dir()` being `data_dir()/identifier`, that is what keeps the suite out of the library you dogfood. ⚠️ The Rust plugins and the npm polyfill go **together**: with the crates alone the runner waits on `window.wdioTauri` and hangs before opening a session; with the polyfill alone the front end invokes `plugin:wdio|…` commands nothing answers. Details in `docs/architecture.md`. - **⚠️ The e2e runs are not isolated from each other, and that shapes every scenario.** `driverProvider` is `embedded`, so the WebDriver server lives inside the application and the service spawns it **once** for the whole run: the fifteen spec files share one process, one SQLite file and one `preferences.json`. Raising `maxInstances` changes nothing — the service skips its per-worker spawn for this provider. So the profile is wiped once, by `tsx e2e/reset-profile.ts` **before** wdio starts (a wdio hook has no ordering against the service's own `onPrepare`, and would meet a locked database); `before()` only reloads the page, which resets the front end and no data; and a spec file seeds its own preconditions instead of assuming a clean corpus. The numeric prefix on each file is the run order, and `01-first-launch` is the only one that meets a virgin profile — hence the only one that can resolve the seeded space, which it writes to a marker file for the rest. ⚠️ `reopenSession()` is **not** a restart and nothing can be: the process hosts the server, so it must stay up. It proves the interface was rebuilt from what the commands answer, never that anything reached the disk — `15-preferences-on-disk` reads the file from Node for that. ⚠️ **A scenario waits on a condition, never on a duration**: `eventually(read, matches, what)` in `support/app.ts`, which hands the value back so the assertion reads what it waited for. A `browser.pause` before an `expect` is a guess at a round trip on a runner sharing a CPU with a WebView, and it is how one scenario went red on Windows and green on a re-run of the very same commit. The **one** exception is an assertion that nothing happened — that is not a condition anything can wait on, and the three that remain say so in a comment. ⚠️ `scripts/e2e-waits.test.mjs` holds that: it fails on a `browser.pause` followed within four lines by a read or an assertion, unless a comment above says the wait is `deliberately` one. The sweep is only as good as its lookahead — #190 demanded a literal `expect(` on the very next line, missed a value read now and asserted two lines down, and covered a sixth of the job (#197). Address a control by its id rather than by position wherever a page can hold two of them. - **The IPC surface is generated.** `src/app/core/ipc/bindings.ts` comes from tauri-specta: one typed function per command plus a TS type per struct crossing the bridge. It is committed and regenerated by `npm run tauri dev` or `npm run bindings` (the `export-bindings` binary). Adding a command means annotating it `#[tauri::command]` **and** `#[specta::specta]`, adding it to `collect_commands![...]` in `src-tauri/src/lib.rs` — the single list, it both registers with Tauri and drives the generation — then regenerating. Every type crossing the bridge derives `specta::Type`. Specta refuses `usize`/`i64`/… (JSON precision), hence `NotesView.matched: u32`. The generator is _not_ wired as a `#[test]`: on Windows the test exe lives in `target/debug/deps/`, without the `WebView2Loader.dll` that linking `Builder::export` then needs, and the whole test binary fails to start. @@ -87,7 +88,7 @@ These are the non-obvious constraints; the rest of the architecture is in `docs/ - **A conversion exists only where the wire shape differs from the model shape.** `model/` is the vocabulary the app reasons in, `data/` is the boundary: the generated wire types (imported under a `Wire*` name, never re-declared), the repository, the conversion. Notes need theirs (`Date` ↔ ISO, patch copied field by field) and it lives in `core/data/note.mapper.ts`. Spaces don't: `Space` crosses the bridge as itself. Don't reintroduce an identity mapper for symmetry. The section key no longer needs a runtime guard either — `NoteSectionKey` is generated, so a variant added in Rust is a compile error. - **A folder cuts a space into regions, and `ON DELETE SET NULL` is the whole point.** `folders` (migration 10) holds name, colour and owning space; `notes.folder_id` is nullable and deleting a folder leaves every note standing, merely unfiled — hence no refuge argument where `delete_space(id, targetSpaceId)` needs one. ⚠️ A folder belongs to one space, so `file_many` narrows to it and `NotePatch::apply` clears `folder_id` whenever `space_id` moves: a chip naming a folder the space switcher cannot reach is worse than none. Filing is a batch command of its own (`file_notes` / `file_notes_back`), never a `NotePatch` field, and it answers the filings it **changed** — same rule as `move_notes`. The colour is **assigned** by rotation (`FolderColour::nth`, a `closed_enum!` of the five theme accents), not chosen: drawing a folder stays one gesture. The card's chip is a **square swatch on a neutral pill** — a tinted pill is what a tag is — and an unfiled note gets no chip at all. ⚠️ Board geometry must never enter `Note` or `Folder`: `transfer::Bundle` deserialises both, so a coordinate there would travel in every export. - **The library rail is the navigation, and the two switchers are its fallback.** `notes/sidebar/library-tree/` draws every space with its folders under it; while it shows, the space switcher and the folder switcher leave the topbar — two places to change space is how a tree and a dropdown drift apart. Shown or hidden is `AppSettings.showLibraryRail` (`Ctrl+B`), restored at launch and deliberately absent from the preferences panel. ⚠️ `FoldersStore` therefore loads **every** space's folders and narrows in a computed: the rail needs the whole library, and it is what lets a folder in another space be opened without a round trip. ⚠️ A folder row answers with the whole `Folder`, not an id — the page selects the space **first**, because `activeFolder` resolves against the active space's folders. The three space actions live once, in `space-editor/`, projected by the switcher and the rail, exactly as `folder-editor/` already was. -- **The board is a second view of a space, and it dims rather than narrows.** `board_view` (migration 11) answers zones and positions, never sections — `build_sections` must not learn about a folder. The search, the quick filters and both rails mark `matches` instead of removing: reflowing the survivors throws away the only thing the board has. ⚠️ Cards **flow** inside a zone and are **placed** outside one, so a `note_positions` row means "this note is loose" and `file_many` deletes the rows it files. The geometry lives in `folders.x/y/w/h` + `note_positions`, never on `Note` or `Folder` — `transfer::Bundle` deserialises both. ⚠️ `store::board::geometry` is a **read that writes**: it materialises the first layout so a drag lands among cards that all have a stored place, and it is idempotent. Pan only, no zoom. The view is remembered per space (`devbox.notes.view.`) and is unavailable on "all spaces". ⚠️ `apply_folders` does not run on the board: a chip naming the zone a card sits in is noise. +- **The board is a second view of a space, and it dims rather than narrows.** `board_view` (migration 11) answers zones and positions, never sections — `build_sections` must not learn about a folder. The search, the quick filters and both rails mark `matches` instead of removing: reflowing the survivors throws away the only thing the board has. ⚠️ Cards **flow** inside a zone and are **placed** outside one, so a `note_positions` row means "this note is loose" and `file_many` deletes the rows it files. The geometry lives in `folders.x/y/w/h` + `note_positions`, never on `Note` or `Folder` — `transfer::Bundle` deserialises both. ⚠️ `store::board::geometry` is a **read that writes**: it materialises the first layout so a drag lands among cards that all have a stored place, and it is idempotent. Pan only, no zoom. The view is remembered per space (`devnotes.notes.view.`) and is unavailable on "all spaces". ⚠️ `apply_folders` does not run on the board: a chip naming the zone a card sits in is noise. - **Every board gesture is pointer events, and it commits on `pointerup` alone.** ⚠️ HTML5 drag & drop is dead in this WebView (see the `dragDropEnabled` entry below), so moving a card, moving a zone, resizing one and drawing one are all `pointerdown` + `setPointerCapture` + `pointermove` + `pointerup`, with the arithmetic in `board-gesture.ts` (no signals, no DOM). A drag under `DRAG_THRESHOLD_PX` writes nothing — it was a click — and `pointercancel` discards. Everything a gesture writes **snaps to `GRID_PX`** (20px), which is the dotted background's own lattice: keep the two in step, and mind that a resize snaps _before_ it clamps, the minimum being `folders::board`'s and not a multiple of it. **The handle is the whole card**, and there is no grip: pointer events already tell a drag from a click by the distance travelled, so a press that goes nowhere opens the note and one that travels moves it. ⚠️ The drop leaves a click behind on the card it just moved — `onCardActivated` swallows exactly that one, and clears the flag on the next press so an abandoned drag cannot eat a later click. ⚠️ A press on the card's own controls (`isCardControl`) starts nothing: it is aimed at them. The grip that used to sit in the corner was drawn on top of the selection tick and took every pointer meant for it. ⚠️ A card **being** dragged is drawn on the surface at the pointer (`travelling`), whichever side of a zone wall it started on: a filed card flows and has no coordinates of its own, so without that ghost a drag out of a zone carries nothing at all. ⚠️ Moving a zone carries its notes and resizing one captures nothing, both for free — a filed card has no coordinates. Unreal's own overlap rule was refused. ⚠️ `BoardStore` stages what moved, writes it as **one** `save_board_layout` behind a 400 ms debounce, and clears the overlay only on success; `save_board_layout` skips a card filed since the drag, or it would put back the row `file_many` dropped. The keyboard twin is the selection bar's "Ranger dans" — the same batch command, so the two cannot drift. - **Choosing a folder is opening it**, from the switcher or from a zone title — one state, `FoldersStore.activeFolderId`. The breadcrumb then replaces the space switcher, the view switch and the folder switcher: from inside a folder there is one place to go. ⚠️ The inside is **not spatial** and it is a **flat grid** — `notes::view::build` counts an opened folder as filtering, and `build_sections` still knows nothing about a folder. A note created here arrives filed; the palette's never does. ⚠️ Escape falls through selection → search → folder, and the middle rung asks `hasUserFilters`, never `isFiltering` — the latter is the view's own answer and is true inside a folder, so Escape would never reach the last rung. The three folder actions live once, in `folder-editor/`, projected by the switcher, the breadcrumb and the zone menu. - **A folder travels with an export; its coordinates do not.** `transfer::Bundle` carries the folders **actually cited** (like the spaces) and each note its `folderId`, both `#[serde(default)]` — no `FORMAT_VERSION` bump, an older file still parses. ⚠️ Nothing is stripped to keep the geometry home: `Folder` simply has none, which is why it was kept off the model. On import a folder is matched **by name inside the destination space**, case-insensitively, and created when absent; spaces are merged first because a folder needs one. ⚠️ A note's `folderId` is the sending library's — remapped, and **dropped** when the file did not carry the folder, or the foreign key would lose the whole import over a note that is merely unfiled. `ImportReport.foldersCreated` sits beside `spacesCreated`. @@ -101,7 +102,7 @@ These are the non-obvious constraints; the rest of the architecture is in `docs/ - **The search is debounced (150 ms).** Filtering crosses the IPC bridge, so `setSearchQuery` updates the field immediately but defers the query. In specs, fake only `['setTimeout', 'clearTimeout']`. - **Read-only store signals.** Writable signals in a store are private (`_x`) and exposed via `.asReadonly()`; mutation goes through methods. Writes are **not** optimistic: persist, adopt the returned note, reload the view — nothing is applied locally, so there is nothing to roll back. `resource.value()` throws while in error — read it behind a `hasValue()` guard. - **`NotesQueryStore.view` is a `linkedSignal` that only retains what it is read through.** It keeps the previous view during a reload so the canvas doesn't blank on every keystroke. Everything the store exposes reads it, and `isLoading` reads it _first_ — a `&&` that short-circuits past it would drop the freshly loaded view on the floor. -- **⚠️ Transloco eats `{{…}}`.** An unknown `{{name}}` in a translation is replaced by the **empty string**, not left alone — so no translated string can carry a snippet's `{{fields}}`. The two sample snippet bodies are hard-coded in `core/state/sample-notes.service.ts` (they are code, so they would not be translated anyway), and a chapter of the guide that names a key receives it as an interpolation parameter instead of spelling it out. The application's name is the exception that costs nothing: a translation writes `{{app}}` and `AppTranslocoLoader` adds `app: APP_INFO.name` to every language, because the transpiler resolves an unknown interpolation against a **sibling key** before giving up on it. One line, no call site passes a parameter, and a string with parameters of its own still resolves both. No translated string spells `DevBox` out; a spec on the shipped files holds that. +- **⚠️ Transloco eats `{{…}}`.** An unknown `{{name}}` in a translation is replaced by the **empty string**, not left alone — so no translated string can carry a snippet's `{{fields}}`. The two sample snippet bodies are hard-coded in `core/state/sample-notes.service.ts` (they are code, so they would not be translated anyway), and a chapter of the guide that names a key receives it as an interpolation parameter instead of spelling it out. The application's name is the exception that costs nothing: a translation writes `{{app}}` and `AppTranslocoLoader` adds `app: APP_INFO.name` to every language, because the transpiler resolves an unknown interpolation against a **sibling key** before giving up on it. One line, no call site passes a parameter, and a string with parameters of its own still resolves both. No translated string spells `DevNotes` out; a spec on the shipped files holds that. - **A count is an ICU plural, and the transpiler that reads it is ours.** `{count, plural, =0 {…} one {# note} other {# notes}}` in `plural-transpiler.ts`, ~60 lines over `Intl.PluralRules`. ⚠️ **Not** `@jsverse/transloco-messageformat`, which was installed, tried and removed: it compiles each message with `new Function`, the CSP is `script-src 'self'`, and the application boots onto an error banner and seeds nothing. The unit suite cannot see that — vitest runs under jsdom, where there is no policy to violate — so only the e2e run caught it. ⚠️ The grammar is deliberately tiny: `plural`, exact `=N`, the categories `Intl.PluralRules` answers, and `#`. No `select`, no `selectordinal`, no nesting. Plain interpolation stays `{{name}}` and is still `DefaultTranspiler`'s, which is what keeps the `{{app}}` sibling-key trick above working. ⚠️ `TranslocoService` asks for the transpiler in its own constructor, so the service is resolved on the first render rather than injected. `=0` is written out wherever zero reads badly: French calls zero `one`, so "0 résultat" would look like a singular. Two specs on the shipped files hold the rest — no string may carry "(s)", and none may carry a brace that is neither an interpolation nor a plural. - **Translation keys, not strings.** Code that produces user-visible text returns a translation reference (`{ key, params }`) consumed by the `transloco` pipe in the template. Adding a string means adding it to **both** `src/app/core/i18n/translations/fr.json` and `en.json`. No user-visible string belongs in the Rust back-end — a new note gets an empty title, and the UI renders a translated placeholder. - **Accessibility is enforced by the linter, except the two things it cannot see.** Decorative emoji need `aria-hidden`, toggles need `aria-pressed`, and information shown only graphically needs a `.visually-hidden` text twin — `npm run lint` catches most of it. What it cannot catch is **contrast** (`scripts/palette.test.mjs`), **target size** — a control takes `@include hit-target`, which is 24×24 of box and never a bigger glyph, since growing the type would be a density change — and **whether a destructive control looks like one**: `@include destructive` is red text at rest, not on `:hover`, because a warning that arrives under the pointer plays no part in choosing. ⚠️ Text and border, never a solid fill (one red button beside the single amber accent reads as a second accent), and the pressed state fills with `--bg-3` rather than a red tint, since red text on a tint of itself falls under AA (#191). ⚠️ On a card that box is not free — the card is a fixed 150px and `.card-items` is `overflow: hidden`, so `MAX_VISIBLE_ITEMS` and the "+N autres" badge have to keep agreeing with what is actually drawn. `07-checklists` measures the rendered boxes and the badge together, because the stylesheet alone cannot say either. @@ -147,9 +148,9 @@ These are the non-obvious constraints; the rest of the architecture is in `docs/ - **Two native services are always faked in specs.** `AppWindowService` because a real `exit()` takes the test runner down with the app, and `FileDialogService` because outside Tauri the plugin throws. Both use the token-plus-adapter pattern of `ClipboardService`, and both treat an unavailable plugin as "nothing was chosen" rather than an exception. - **The changelog is baked into the binary, and its grammar is thin.** `src-tauri/src/changelog.rs` pulls the repository's `CHANGELOG.md` in with `include_str!` (a path resolved from `CARGO_MANIFEST_DIR`, like `BINDINGS_PATH`) and `changelog/model.rs` parses it: `## ` a release, `### ` a category, `- ` an entry, an indented line continues the one above. Reshaping the file past that empties "Nouveautés" — a test on the shipped file is what says so. That grammar now has **two** consumers: the Rust test that imposes it and `scripts/release-notes.mjs` that emits it. It is deliberately untranslated, like the release notes the updater hands over. ⚠️ The **newest section is generated** by `release.yml` from the merged pull requests, and committed before the tag because `include_str!` bakes the file in at compile time — editing that section by hand is fine until the next release overwrites it; older sections are never touched. - **A fresh install arrives arranged, not empty.** The seeding writes two folders beside the space and the notes, files three of the four and leaves one loose, and the page then **selects the space it made** — with exactly one, "all spaces" is a distinction without a difference and the only state where the board cannot be shown at all. ⚠️ The folders go in the **same transaction**, for the reason below: a space standing without them reads as "already seeded" to both guards, for ever. ⚠️ Each seeded row is stamped a millisecond apart, and in **opposite directions** — `folders::list` orders `created_at` ascending, the canvas orders `updated_at` descending — because sharing one instant left both orders falling back to a random UUID. A sample names its folder by **index**: the folders have no id until the transaction runs. -- **A fresh install seeds sample notes, and two guards decide it.** A virgin database has no space, so not even a note can be created: `SampleNotesService` files four. The preference marker `devbox.notes.samplesSeeded` alone would re-seed anyone whose preferences file went missing, and "no space at all" alone would re-seed the day the last space disappears — together they only ever match a database that has never been written to. The marker is written **after** the seeding, because there is no halfway: `seed_samples` writes the space and the four notes in one transaction. ⚠️ Six round trips used to seed them, and a process killed between any two left a space with nothing in it — which both guards read as "already seeded". -- **Every open checks the file is still sound, and a damaged one has a way out.** `db::quick_check` runs inside `db::open`, ⚠️ **before** the migrations — they write, and running them over a damaged file is how a salvageable database becomes an unsalvageable one. It becomes `StorageError::Damaged` → `ErrorCode::LibraryDamaged`, the one code the gate answers with an action instead of a message: `recovery::set_aside` rescues what `VACUUM INTO` will still hand over, then moves the database, its `-wal` / `-shm` sidecars and ⚠️ `attachments/` into `damaged//` — leave the attachments behind and the next launch's orphan sweep deletes the pictures of the notes just set aside. ⚠️ `vault.json` stays put: the passphrase does not change and the rescued copy needs that exact key. The command refuses while the library is open and answers where it wrote, so the interface can say it; the front end forgets `devbox.notes.samplesSeeded` along the way, or the fresh library opens on a canvas with no space — and a note cannot be created without one. -- **Migrations are append-only.** They are SQL files under `src-tauri/migrations/`, embedded by `embed_migrations!` and tracked in `__diesel_schema_migrations`. Changing the model means a new `YYYY-MM-DD-HHMMSS_name/` directory with an `up.sql` — never editing a shipped one, it has already run on existing installs. `db::migration::adopt_legacy_history` bridges databases still versioned by the old `PRAGMA user_version` (1..3): it marks the matching migrations as applied and zeroes the pragma, so nothing is replayed. Deleting the database file is a legitimate reset during development (`app_data_dir()/devbox.sqlite3`). +- **A fresh install seeds sample notes, and two guards decide it.** A virgin database has no space, so not even a note can be created: `SampleNotesService` files four. The preference marker `devnotes.notes.samplesSeeded` alone would re-seed anyone whose preferences file went missing, and "no space at all" alone would re-seed the day the last space disappears — together they only ever match a database that has never been written to. The marker is written **after** the seeding, because there is no halfway: `seed_samples` writes the space and the four notes in one transaction. ⚠️ Six round trips used to seed them, and a process killed between any two left a space with nothing in it — which both guards read as "already seeded". +- **Every open checks the file is still sound, and a damaged one has a way out.** `db::quick_check` runs inside `db::open`, ⚠️ **before** the migrations — they write, and running them over a damaged file is how a salvageable database becomes an unsalvageable one. It becomes `StorageError::Damaged` → `ErrorCode::LibraryDamaged`, the one code the gate answers with an action instead of a message: `recovery::set_aside` rescues what `VACUUM INTO` will still hand over, then moves the database, its `-wal` / `-shm` sidecars and ⚠️ `attachments/` into `damaged//` — leave the attachments behind and the next launch's orphan sweep deletes the pictures of the notes just set aside. ⚠️ `vault.json` stays put: the passphrase does not change and the rescued copy needs that exact key. The command refuses while the library is open and answers where it wrote, so the interface can say it; the front end forgets `devnotes.notes.samplesSeeded` along the way, or the fresh library opens on a canvas with no space — and a note cannot be created without one. +- **Migrations are append-only.** They are SQL files under `src-tauri/migrations/`, embedded by `embed_migrations!` and tracked in `__diesel_schema_migrations`. Changing the model means a new `YYYY-MM-DD-HHMMSS_name/` directory with an `up.sql` — never editing a shipped one, it has already run on existing installs. `db::migration::adopt_legacy_history` bridges databases still versioned by the old `PRAGMA user_version` (1..3): it marks the matching migrations as applied and zeroes the pragma, so nothing is replayed. Deleting the database file is a legitimate reset during development (`app_data_dir()/devnotes.sqlite3`). - **`db/schema.rs` is hand-written, not `diesel print-schema`.** Generating it would make `cargo check` depend on an up-to-date database outside the repo. Adding a column means editing **both** the migration SQL and this file; `check_for_backend` on `NoteRow` turns a divergence into a compile error. Diesel does not model the `CHECK`s, the `ON DELETE CASCADE`s or the `NOCASE` collation — those live in the migration SQL and are simply obeyed. Where a collation must be applied to an expression rather than a column (`spaces.name`), the query drops to a `diesel::dsl::sql` fragment; that is deliberate, not a gap to tidy up. - **`PRAGMA foreign_keys` is per connection, not per database.** It's set in `db::configure`; without it the `ON DELETE CASCADE` clauses in the schema are inert and deleting a note leaves its tags behind. - **A modal is a shell, not a copy.** `DialogComponent` (`shared/dialog/`) owns the scrim, the panel, `role="dialog"`, `aria-modal`, the focus trap, Escape and the backdrop click; a dialog projects its content and declares a `layer`. The rung table in `dialog.model.ts` is the **only** place a modal `z-index` exists — its index in `LAYERS` is both the `z-index` and the Escape priority. `DialogStack` gives the keystroke to whichever modal is in front, ordering by rung and not by arrival (the fields form is created after the palette but drawn over it), and its `hasOpenDialog()` is what tells the canvas its keyboard is taken. What a shell cannot guess — width, height, gap, padding, where it sits on the scrim — comes from `--dialog-*` custom properties the consumer sets on the `app-dialog` element; no measurement goes through an `input()` or gets spelled in a template. @@ -162,9 +163,9 @@ These are the non-obvious constraints; the rest of the architecture is in `docs/ - **One method writes a note's fields.** `NotesStore.applyPatch(id, patch)`, with a `UNCHANGED` table of per-field comparators deciding what actually moved — so closing the editor on an untouched note makes no round trip. The table is exhaustive by construction: a field added to `NoteDraft` stops it compiling until it says how to compare itself. The editor matches it with **one** output, `patchRequested = output()`; there were nine, and adding a field meant editing four files. - **The canvas keyboard is one table.** `CANVAS_KEYS` in `CanvasKeyboardDirective` (applied as a `hostDirective` of the page) carries both the caps the shortcuts sheet draws and the behaviour bound to them; `CANVAS_SHORTCUT_GROUP` is derived from it. An entry with no `run` is documented here and handled elsewhere (`Ctrl+K` is the search field's). A `run` answers whether it acted, and only then is `preventDefault` called. - **Nothing reloads a view by hand, `NotesStore` included.** Every writer bumps `NotesRevision` (`core/state/`); `NotesQueryStore.queryParams` and `BoardStore.queryParams` both read it, so both `resource`s re-run on their own. ⚠️ `NotesStore` used to call `this.notes.reload()` instead — that reloads the canvas and **only** the canvas, which is why a todo list ticked on the board stayed unticked until the view was switched. The page used to chain seven `store.reload()` calls and nothing caught the eighth writer who forgot. -- **The application describes itself from `Cargo.toml`, at compile time.** `app_info::METADATA` crosses as `.constant("APP_METADATA", …)` next to `GLOBAL_ACTION_EVENT`: display name, repository URL, author, handle, and the Rust toolchain the project pins (`build.rs` reads `channel` from `rust-toolchain.toml`). The card completes the line with Angular's `VERSION.full` and with `getTauriVersion()` — asked of the running framework rather than baked in, like the application's own version: a committed `bindings.ts` can lag behind a dependency bump. The standard fields come from `CARGO_PKG_*`; what Cargo has no field for lives in `[package.metadata.devbox]`, which `build.rs` reads and hands over as `cargo:rustc-env` variables — Cargo does not pass `[package.metadata]` to the crate. A constant and not a command, because the titlebar reads the name synchronously. ⚠️ The **version** is deliberately not in it: `getVersion()` reads it from the running binary, and `tauri.conf.json` declares none — without the key Tauri takes `Cargo.toml`'s. The release workflow reads that version and refuses to go on unless `package.json`, `package-lock.json` and `Cargo.lock` agree — before the tag exists, rather than after. +- **The application describes itself from `Cargo.toml`, at compile time.** `app_info::METADATA` crosses as `.constant("APP_METADATA", …)` next to `GLOBAL_ACTION_EVENT`: display name, repository URL, author, handle, and the Rust toolchain the project pins (`build.rs` reads `channel` from `rust-toolchain.toml`). The card completes the line with Angular's `VERSION.full` and with `getTauriVersion()` — asked of the running framework rather than baked in, like the application's own version: a committed `bindings.ts` can lag behind a dependency bump. The standard fields come from `CARGO_PKG_*`; what Cargo has no field for lives in `[package.metadata.devnotes]`, which `build.rs` reads and hands over as `cargo:rustc-env` variables — Cargo does not pass `[package.metadata]` to the crate. A constant and not a command, because the titlebar reads the name synchronously. ⚠️ The **version** is deliberately not in it: `getVersion()` reads it from the running binary, and `tauri.conf.json` declares none — without the key Tauri takes `Cargo.toml`'s. The release workflow reads that version and refuses to go on unless `package.json`, `package-lock.json` and `Cargo.lock` agree — before the tag exists, rather than after. - **A closed enum is declared once.** `Language` and `NoteKind` come from `closed_enum!` (`src-tauri/src/closed_enum.rs`), which generates the enum, `ALL`, `as_str`, `Display` and `FromStr` from one list. The literal given per variant is the **single** spelling: serde, the column, `Display` and `FromStr` all read it, so they cannot drift. -- **A setting is one line.** `SettingsStore.setting(key, codec)` builds the signal, its restore step and its write-through together; `SETTINGS_KEYS` is derived from `keyof AppSettings` (`devbox.${key}`). Adding a setting is a field on `AppSettings` plus that line — the named setters are a thin compatibility shim over `signal.write()`. +- **A setting is one line.** `SettingsStore.setting(key, codec)` builds the signal, its restore step and its write-through together; `SETTINGS_KEYS` is derived from `keyof AppSettings` (`devnotes.${key}`). Adding a setting is a field on `AppSettings` plus that line — the named setters are a thin compatibility shim over `signal.write()`. - **Side tables are narrowed by subquery, never by bound ids.** `notes::store::all_tags` and its two siblings filter on `note_id IN (SELECT id FROM notes WHERE space_id = ?)`. Binding one parameter per note measured _slower_ than reading the table whole past a few thousand notes; the subquery binds one value and rides the index. Reading a superset is harmless — `attach_related` only looks up the notes it holds. ## Design reference diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1c60fc0..3bb64740 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -# Contributing to DevBox +# Contributing to DevNotes Thanks for taking the time. This file covers what the compiler and CI cannot tell you: the handful of conventions that are load-bearing, and where the traps are. -DevBox is licensed under the [GNU GPL v3.0](LICENSE). Opening a pull request means you agree +DevNotes is licensed under the [GNU GPL v3.0](LICENSE). Opening a pull request means you agree to have your contribution distributed under those terms. ## Getting set up @@ -51,7 +51,7 @@ you use day to day. | ----------------------- | ------------------------------------------------------- | | `npm start` | Angular dev server only, port 1420 | | `npm run tauri dev` | The main loop: Angular dev server plus the Tauri window | -| `npm run build` | Production Angular build, into `dist/devbox/browser` | +| `npm run build` | Production Angular build, into `dist/devnotes/browser` | | `npm run tauri build` | Full production build, into `src-tauri/target/release` | | `npm test` | Unit tests (Vitest on jsdom — no browser needed) | | `npm run test:watch` | The same, re-running on change | @@ -95,7 +95,7 @@ read it before a structural change, and keep it in step when you make one. **Migrations are append-only.** A model change means a new `src-tauri/migrations/` directory, never an edit to one that already shipped — it has already run on installed databases. `src-tauri/src/db/schema.rs` is **hand-written**, not generated, so a new column -means editing both it and the SQL. Deleting `app_data_dir()/devbox.sqlite3` is a legitimate +means editing both it and the SQL. Deleting `app_data_dir()/devnotes.sqlite3` is a legitimate reset while developing. **The IPC surface is generated.** `src/app/core/ipc/bindings.ts` comes from the Rust diff --git a/README.md b/README.md index a52709a1..50ad1644 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# DevBox +# DevNotes -[![CI](https://github.com/vmillet-dev/devbox-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/vmillet-dev/devbox-rs/actions/workflows/ci.yml) -[![Latest release](https://img.shields.io/github/v/release/vmillet-dev/devbox-rs)](https://github.com/vmillet-dev/devbox-rs/releases/latest) +[![CI](https://github.com/vmillet-dev/devnotes-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/vmillet-dev/devnotes-rs/actions/workflows/ci.yml) +[![Latest release](https://img.shields.io/github/v/release/vmillet-dev/devnotes-rs)](https://github.com/vmillet-dev/devnotes-rs/releases/latest) [![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE) A notes and snippets manager for developers, on the desktop. Write a snippet once, find it @@ -10,7 +10,7 @@ by tag or by full text, and paste it into any application from a global shortcut Search it, narrow it by tag, open it — and from any application, `Ctrl+Alt+P` brings up the palette, where `Enter` copies and the window steps aside: -![A tour of DevBox: the board of notes, a search narrowing it and quoting the line that matched, a tag filter, a note open in the editor, then the quick-paste palette asking a snippet for its fields](docs/quick-paste.gif) +![A tour of DevNotes: the board of notes, a search narrowing it and quoting the line that matched, a tag filter, a note open in the editor, then the quick-paste palette asking a snippet for its fields](docs/quick-paste.gif) Everything stays on your machine, encrypted with a passphrase you choose and type once at launch. Nothing is uploaded, there is no account, and the application works with the network @@ -28,7 +28,7 @@ off. | **Bulk actions** | select several notes, then move, tag, export or trash them in one go | | **Tag management** | rename, merge or drop a tag across the whole library | | **Attachments** | drop a file on the editor or paste an image; open it, save it elsewhere, preview it inline | -| **Import / export** | a `.devbox` archive both ways, attachments included — everything, one space, or the selection | +| **Import / export** | a `.devnotes` archive both ways, attachments included — everything, one space, or the selection | | **Copy as Markdown** | the selection rendered for a pull request, a ticket or a chat message | | **Encrypted at rest** | one passphrase at launch; notes and attachments sealed on disk, exports optionally too | @@ -37,7 +37,7 @@ English, and it ships with a light and a dark theme. ## Your library is encrypted -DevBox asks for a passphrase the first time it runs, and once at every launch after that. +DevNotes asks for a passphrase the first time it runs, and once at every launch after that. It is what opens the library, and it is never stored anywhere — not in a keychain, not behind a "remember me". While the application runs the key lives in memory and nowhere else. @@ -55,13 +55,13 @@ What is sealed on disk: note titles, bodies and sources, checklist items, space library. An export written in the clear is the only copy that does not depend on it. ⚠️ **Opening an attachment** writes a decrypted copy — inside your own profile, never the -shared temporary folder — because the program that opens it reads from disk. DevBox deletes +shared temporary folder — because the program that opens it reads from disk. DevNotes deletes those copies when it quits, and sweeps whatever survived — a file another application still held, a crash — at the next launch. An export is the one file meant to leave the machine, so it is offered a key of its own: give it a passphrase and it travels sealed, attachments included, or write it in the clear -for a file any DevBox can read. The application asks which, every time, and says which one +for a file any DevNotes can read. The application asks which, every time, and says which one it wrote. ## Where your library lives, and how it is backed up @@ -69,21 +69,21 @@ it wrote. Two files, and they only mean anything together — the database is sealed, and the key file is what opens it: -| | | -| ----------- | -------------------------------- | -| **Windows** | `%APPDATA%\com.devbox.app\` | -| **Linux** | `~/.local/share/com.devbox.app/` | +| | | +| ----------- | ---------------------------------- | +| **Windows** | `%APPDATA%\com.devnotes.app\` | +| **Linux** | `~/.local/share/com.devnotes.app/` | -In it: `devbox.sqlite3`, `vault.json`, `attachments/`, and `preferences.json`. ⚠️ Copy the +In it: `devnotes.sqlite3`, `vault.json`, `attachments/`, and `preferences.json`. ⚠️ Copy the database without the key file and you have copied something nobody can open again. -DevBox takes a **rolling copy at launch**, at most one a day, and keeps the last three in +DevNotes takes a **rolling copy at launch**, at most one a day, and keeps the last three in `backups/`. Each one is a full library — database and key file together — so restoring is copying a folder back. It is written with `VACUUM INTO` rather than by copying the file, because under WAL the database on its own is not a consistent snapshot. Turn it off in Preferences → Security if you would rather it did not. -DevBox also checks the database is still sound every time it opens one. If it is not, it +DevNotes also checks the database is still sound every time it opens one. If it is not, it says so rather than starting on it, and offers to set it aside: the database, its attachments and whatever could still be rescued from it move into `damaged/`, and the next unlock starts on a fresh library. Your passphrase does not change — so what was set aside, @@ -95,34 +95,34 @@ For that, export somewhere else — or copy that folder to another machine. ## Install -DevBox runs on **Windows and Linux**. There is no macOS build: it cannot be tested here, +DevNotes runs on **Windows and Linux**. There is no macOS build: it cannot be tested here, and Gatekeeper wants a paid Apple Developer account with no free way around it on recent versions — shipping for a platform that can be neither tested nor distributed would be a promise nobody can keep. -Download from the [latest release](https://github.com/vmillet-dev/devbox-rs/releases/latest). +Download from the [latest release](https://github.com/vmillet-dev/devnotes-rs/releases/latest). **Windows** -| File | Pick it if | -| -------------------------------- | -------------------------------------------------------------- | -| `devbox__x64-setup.exe` | You just want DevBox installed. This is the one to take. | -| `devbox__x64_en-US.msi` | You deploy software through group policy or a management tool. | -| `devbox--windows.exe` | You want no installer at all — run it from where it lands. | +| File | Pick it if | +| ---------------------------------- | -------------------------------------------------------------- | +| `devnotes__x64-setup.exe` | You just want DevNotes installed. This is the one to take. | +| `devnotes__x64_en-US.msi` | You deploy software through group policy or a management tool. | +| `devnotes--windows.exe` | You want no installer at all — run it from where it lands. | **Linux** -| File | Pick it if | -| --------------------------------- | ------------------------------------- | -| `devbox__amd64.AppImage` | Any distribution, nothing to install. | -| `devbox__amd64.deb` | Debian, Ubuntu and derivatives. | -| `devbox--1.x86_64.rpm` | Fedora, RHEL and derivatives. | -| `devbox--linux` | The bare executable, no packaging. | +| File | Pick it if | +| ----------------------------------- | ------------------------------------- | +| `devnotes__amd64.AppImage` | Any distribution, nothing to install. | +| `devnotes__amd64.deb` | Debian, Ubuntu and derivatives. | +| `devnotes--1.x86_64.rpm` | Fedora, RHEL and derivatives. | +| `devnotes--linux` | The bare executable, no packaging. | ```bash -chmod +x devbox_*_amd64.AppImage # make the AppImage runnable, then launch it -sudo apt install ./devbox_*_amd64.deb # Debian and Ubuntu — pulls in what it needs -sudo dnf install ./devbox-*.x86_64.rpm # Fedora and RHEL +chmod +x devnotes_*_amd64.AppImage # make the AppImage runnable, then launch it +sudo apt install ./devnotes_*_amd64.deb # Debian and Ubuntu — pulls in what it needs +sudo dnf install ./devnotes-*.x86_64.rpm # Fedora and RHEL ``` On Windows, the installer and the MSI are opened by double-clicking them; the standalone @@ -131,7 +131,7 @@ On Windows, the installer and the MSI are opened by double-clicking them; the st Updates are offered inside the application, so you only download by hand once. Windows shows a SmartScreen warning the first time — choose **More info**, then **Run -anyway**. It is about the identity of the publisher, not about the file: DevBox has no +anyway**. It is about the identity of the publisher, not about the file: DevNotes has no code-signing certificate, while its updates are signed with minisign and verified before they install. Every release also publishes `SHA256SUMS.txt`. @@ -169,7 +169,7 @@ IPC surface between them is generated from the Rust signatures. ## License -DevBox is free software under the [GNU General Public License v3.0](LICENSE). You may use, +DevNotes is free software under the [GNU General Public License v3.0](LICENSE). You may use, study, share and modify it; a distributed fork has to stay under the same terms and ship its source. diff --git a/angular.json b/angular.json index 22df5b45..6693fb19 100644 --- a/angular.json +++ b/angular.json @@ -6,7 +6,7 @@ "analytics": false }, "projects": { - "devbox": { + "devnotes": { "projectType": "application", "schematics": { "@schematics/angular:component": { @@ -92,10 +92,10 @@ }, "configurations": { "production": { - "buildTarget": "devbox:build:production" + "buildTarget": "devnotes:build:production" }, "development": { - "buildTarget": "devbox:build:development" + "buildTarget": "devnotes:build:development" } }, "defaultConfiguration": "development" diff --git a/docs/architecture.md b/docs/architecture.md index df56b8fc..07667bfc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,11 +1,11 @@ # Architecture -How DevBox is put together, and the conventions to follow when extending it. +How DevNotes is put together, and the conventions to follow when extending it. For build/run instructions see the [README](../README.md). ## Overview -DevBox is a Tauri v2 desktop app: an Angular single-page front-end rendered in a WebView, +DevNotes is a Tauri v2 desktop app: an Angular single-page front-end rendered in a WebView, and a Rust process that owns everything native (storage, and later hashing and filesystem). The two halves talk only through Tauri's `invoke()` bridge. @@ -810,7 +810,7 @@ every export and land on top of the receiving machine's own arrangement. A switch in the header chooses between **Date** and **Tableau**. The date view is not replaced and stays the default; the board is another way to look at the same notes, and it -is remembered **per space** — one key each, `devbox.notes.view.`, so arranging one +is remembered **per space** — one key each, `devnotes.notes.view.`, so arranging one space does not switch the others. ⚠️ It is unavailable on "all spaces", where a folder belongs to no board: the switch disables rather than disappears, and falls back to the date view. @@ -1252,7 +1252,7 @@ not infallible. ### The quick-paste palette -`Ctrl+Alt+P` reveals the window and emits `devbox:palette`; `PaletteStore` opens, +`Ctrl+Alt+P` reveals the window and emits `devnotes:palette`; `PaletteStore` opens, searches, and on `Enter` copies the highlighted snippet and **hides the window** so the user lands back where they were pasting. @@ -1383,7 +1383,7 @@ order on screen is the order in the array. There used to be a contribution registry here — three of them, in fact, one per extension point — so the chrome could stay ignorant of a feature it might not have. That indirection had -exactly one purpose, a second tool, and [#23](https://github.com/vmillet-dev/devbox-rs/issues/23) +exactly one purpose, a second tool, and [#23](https://github.com/vmillet-dev/devnotes-rs/issues/23) decided there would not be one. With a single feature it protected nothing and cost a real detour: reading what a menu entry did meant opening the notes page. They are gone. @@ -1443,14 +1443,14 @@ matching a regex, so the pattern and `is_field_name` are held together by one te `the_pattern_and_the_rule_agree` — and by nothing else. `AutostartService` reads the system **first** and aligns the preference on what it finds: -turning the entry off from the task manager has to uncheck the box, not see DevBox put it back. +turning the entry off from the task manager has to uncheck the box, not see DevNotes put it back. **What the settings actually change** - **Theme** — `system` / `dark` / `light`, resolved into a `data-theme` attribute on `` (see _Theming_). `system` follows `prefers-color-scheme` live. - **Density** — `data-density`, which swaps four spacing variables. -- **Start with the system, minimise to tray, close to tray** — the last one was DevBox's fixed +- **Start with the system, minimise to tray, close to tray** — the last one was the application's fixed behaviour and stays the default; both tray settings are still refused when there is no tray (`desktop::hides_on_close` / `hides_on_minimize`), since hiding a window nothing can call back is worse than closing it. @@ -1536,7 +1536,7 @@ are not translated, and they could not be: Transloco reads `{{name}}` as an inte would replace a snippet's fields with empty strings on the way out. ⚠️ **Two guards decide a first launch, not one.** A preference marker -(`devbox.notes.samplesSeeded`) alone would re-seed anyone whose preferences file went missing; +(`devnotes.notes.samplesSeeded`) alone would re-seed anyone whose preferences file went missing; "no space at all" alone would re-seed the day the last space disappears. Together they only ever match a database that has never been written to. The marker is written **after** the seeding, because the seeding is one write: `seed_samples` creates the space and the four notes in a @@ -1548,7 +1548,7 @@ about samples nobody asked for would only add noise. ### Import, export and copying out -- **Export writes an archive**, `.devbox`, which is a zip: `bundle.json` at the root +- **Export writes an archive**, `.devnotes`, which is a zip: `bundle.json` at the root (`transfer::model::Bundle` — a version, an instant, the spaces cited, the notes and the attachment records) and one entry per attachment under `attachments/`, named by its `stored_name`. The bundle is deflated, being repetitive text; the attachments are stored as @@ -1577,10 +1577,10 @@ about samples nobody asked for would only add noise. more bytes, and the import path holds the file as a `String`, then a `serde_json::Value`, then a `Bundle` — three copies of every screenshot in memory, which a library of a hundred captures turns into a gigabyte. `file::Payload` hands entries over one at a time instead. -- **⚠️ A new DevBox reads an old file; an old DevBox does not read a new one.** `file::read` +- **⚠️ A new DevNotes reads an old file; an old DevNotes does not read a new one.** `file::read` sniffs the zip magic and falls back to parsing the whole file as JSON, so every `.json` export written before the archive still imports. The picker keeps `json` among its - extensions on the way in for exactly that reason, and offers only `devbox` on the way out. + extensions on the way in for exactly that reason, and offers only `devnotes` on the way out. `FORMAT_VERSION` is untouched: the container changed, the data shape did not. - **Import merges, it never replaces.** Spaces are matched by name, case-insensitively, and a note whose id is already taken is counted as skipped rather than overwritten — so the same @@ -1837,8 +1837,8 @@ who remember to touch the select. Three things keep it honest: front something happened — goes through **`AppEventsService`** (`core/ipc/app-events.service.ts`), which wraps `listen` from `@tauri-apps/api/event`. -**One topic, carrying a closed action.** There used to be three (`devbox:capture`, -`devbox:new-note`, `devbox:palette`), spelled out on both sides, where a typo produced a +**One topic, carrying a closed action.** There used to be three (`devnotes:capture`, +`devnotes:new-note`, `devnotes:palette`), spelled out on both sides, where a typo produced a subscription that was silently inert and that nothing reported. Now `desktop::GlobalAction` is a `closed_enum!` and the topic is a single constant, and **both are generated**: `lib.rs` exports them with `.typ::()` and `.constant("GLOBAL_ACTION_EVENT", …)`, neither of which @@ -1854,7 +1854,7 @@ every build. The about card completes the line with Angular's own `VERSION.full` same reason as the application's own version — a committed `bindings.ts` can lag behind a dependency bump, an answer from the bridge cannot. Every value comes from `Cargo.toml` — the standard fields through `CARGO_PKG_*`, and what Cargo has no field for through -`[package.metadata.devbox]`, which `build.rs` hands to the crate as environment variables +`[package.metadata.devnotes]`, which `build.rs` hands to the crate as environment variables read with `env!`. It is a constant and not a command on purpose: the titlebar reads the name **synchronously**, where a round trip would leave it empty for a frame. `core/services/app-info/` re-exports it once as `APP_INFO`, so nothing else imports `bindings.ts` for it. @@ -1898,7 +1898,7 @@ would keep answering. - Neither the topic nor the action set is spelled twice any more: both are generated, so a typo cannot produce the silently inert subscription this used to risk. - A shortcut already taken by another application is **logged and ignored**, never fatal: - DevBox has to start without it. + DevNotes has to start without it. - `AppEventsService.on()` returns an unsubscribe immediately although the subscription only lands a tick later; a component destroyed in between would otherwise stay subscribed for the whole session. @@ -1936,7 +1936,7 @@ read before Angular boots. ### System tray -DevBox stays resident in the notification area, and **the window's close button only hides it** +DevNotes stays resident in the notification area, and **the window's close button only hides it** — quitting goes through the tray menu. An app made to be one shortcut away would be pointless if closing it killed the shortcut. It is a preference now (see _Preferences_), still on by default; minimising to the tray is the same idea, off by default. Tauri emits nothing for @@ -2064,7 +2064,7 @@ still verify. Binding it would mean threading the row identity through every sea call in the stores; it buys nothing against the threat above, where the attacker reads the file rather than edits it and hands it back. -⚠️ Nor is it a defence against a machine already compromised while DevBox runs: the key is +⚠️ Nor is it a defence against a machine already compromised while DevNotes runs: the key is in this process’s memory for the length of the session, and there is no idle re-lock. ### The pieces @@ -2159,7 +2159,7 @@ on the way out: `read_attachment` decrypts into the `data:` URI the preview alre and `save_attachment` writes plaintext where the user chose to put it. ⚠️ `open_attachment` is the exception, and a deliberate one: the program that opens a -document reads it from disk, so DevBox writes a decrypted copy under `app_data_dir()/open/` +document reads it from disk, so DevNotes writes a decrypted copy under `app_data_dir()/open/` and opens that — one click, as before. Those copies are swept on the way out (`RunEvent::Exit`) and again at every launch, which is what covers one another application still held, and a crash. ⚠️ The profile and **not** the OS temporary directory: that one is @@ -2362,7 +2362,7 @@ colour, created_at)` and `notes.folder_id` points into it. ⚠️ The column was on Windows, where both are `%APPDATA%\`), readable from Rust and immune to a WebView cache wipe — unlike the `localStorage` it replaced. Two consumers: `LocaleService`, and the editor overlay's two display toggles — fullscreen - (`devbox.editorFullscreen`) and the fields drawer (`devbox.editorFieldsPanel`, open by + (`devnotes.editorFullscreen`) and the fields drawer (`devnotes.editorFieldsPanel`, open by default: a drawer folded on first sight hides the feature from whoever does not know it yet). - **The API stays synchronous** although the plugin's is not: both consumers read at construction time, and an async read would show the interface in one state then the @@ -2373,7 +2373,7 @@ colour, created_at)` and `notes.folder_id` points into it. ⚠️ The column was builder bundles modules before Vitest sees them, so `vi.mock` on an external package intercepts only intermittently. Outside Tauri the loader rejects and the service degrades to a memory-only cache, which is how every other spec runs. - - `hydrate()` adopts any `devbox.*` key left in `localStorage` by an earlier version, then + - `hydrate()` adopts any `devnotes.*` key left in `localStorage` by an earlier version, then clears it. Without that, updating the app would silently reset the interface language. - Adding a plugin also means declaring its permission (`store:default`) in `src-tauri/capabilities/default.json`, or the call is refused at runtime. @@ -2421,7 +2421,7 @@ Transloco's `transloco` pipe. French is the fallback locale. - `system` resolves through `resolveSystemLocale()` (`core/services/i18n/locale.model.ts`), which reads `navigator.languages` — the WebView takes it from the OS — and falls back to `SYSTEM_FALLBACK_LOCALE` (English) when the machine speaks neither language. Nothing is - persisted while the preference stays on `system`, so DevBox keeps following the OS. + persisted while the preference stays on `system`, so DevNotes keeps following the OS. - ⚠️ `DEFAULT_LOCALE` is a different thing: Transloco's fallback _bundle_, the file that answers when a key is missing from the other one. - `LocaleService.restore()` runs from the app initializer, **after** `SettingsStore.restore()` @@ -2949,7 +2949,7 @@ Three things the embedded WebDriver server will not do, each with a helper in `s case the control is asserted on — it exists, it is labelled — and never clicked: - The OS-level global accelerator. WebDriver types into the WebView, not into the machine, so - the palette is opened by emitting the same `devbox:action` event the accelerator sends. + the palette is opened by emitting the same `devnotes:action` event the accelerator sends. - The native file picker. `window.__TAURI_INTERNALS__.invoke` — the funnel every `invoke` goes through — is `writable: false, configurable: false`, so nothing can stand in front of it and a picker opened by a click would block the application until a human clicked it. Import, @@ -2962,7 +2962,7 @@ case the control is asserted on — it exists, it is labelled — and never clic - The system clipboard where the machine will not release it: on Windows a clipboard manager can hold the lock indefinitely, and a headless Linux runner may have no selection owner at all. `clipboardText()` answers `null` and the scenario calls `this.skip()` — skipped rather - than passed, because a bare `return` is a green test that asserted nothing. What DevBox owns + than passed, because a bare `return` is a green test that asserted nothing. What DevNotes owns is asserted anyway, through `DisplayNote.copyText`. #### In CI diff --git a/docs/releasing.md b/docs/releasing.md index 8814fc2d..c4c41204 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,7 +7,7 @@ state to clean up if it stops. ## The three steps 1. **Bump the version.** `npm version --no-git-tag-version` covers `package.json` - and its lockfile; `src-tauri/Cargo.toml` is edited by hand, then `cargo update -p devbox` + and its lockfile; `src-tauri/Cargo.toml` is edited by hand, then `cargo update -p devnotes` writes the Cargo lockfile. Merge to `main` and wait for CI to go green — the release refuses to start on a commit CI has not passed. @@ -57,8 +57,8 @@ hand is fine until the next release regenerates it. Older sections are never tou Per platform, from one matrix build: - **Windows** — the NSIS installer (`*_x64-setup.exe`), the MSI (`*_x64_en-US.msi`) and the - standalone executable (`devbox--windows.exe`). -- **Linux** — AppImage, `.deb`, `.rpm`, and the standalone binary (`devbox--linux`). + standalone executable (`devnotes--windows.exe`). +- **Linux** — AppImage, `.deb`, `.rpm`, and the standalone binary (`devnotes--linux`). Plus `SHA256SUMS.txt` over all of them, and `latest.json` — the manifest the in-app updater reads. diff --git a/docs/scratch-folders.html b/docs/scratch-folders.html index 06546d84..3cc7c55d 100644 --- a/docs/scratch-folders.html +++ b/docs/scratch-folders.html @@ -249,7 +249,7 @@
- DevBox — dossiers + DevNotes — dossiers @@ -262,7 +262,7 @@
Fichier À propos -
DevBox
+
DevNotes
FR
@@ -367,13 +367,13 @@
SH
Connexion prod
psql -h {{host}} -U {{user}} - -d devbox
+ -d devnotes
SH
Dump nocturne
-
pg_dump -Fc devbox +
pg_dump -Fc devnotes > /backups/$(date +%F).dump
@@ -417,7 +417,7 @@
SH
Vacuum hebdomadaire
VACUUM ANALYZE; - REINDEX DATABASE devbox;
+ REINDEX DATABASE devnotes;
@@ -487,7 +487,7 @@
SH
Connexion prod
psql -h {{host}} -U {{user}} - -d devbox
+ -d devnotes