mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat/halloween-theme
121 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b31c2ff1d2 |
ci(client): hold the en keys nothing reaches to a baseline that only shrinks
lint:i18n-keys only listed unused en keys as information, and it looked for references in client/src alone, so every key the server sends (system notice and notification channel keys in its registries) read as dead. The reference search now also reads server/src and plugin-sdk/src; those references only mark keys as reached and are not held to the client's key rules. The unused keys per en domain file are counted against scripts/i18n-unused-baseline.json, which only shrinks and fails on an entry above its count until --update lowers it. Today's 841 unused keys in 44 files are frozen, none deleted. |
||
|
|
c83fa66c67 |
ci: hold unused files, exports and dependencies to a knip baseline that only shrinks
Nothing looked for dead code: no knip, depcheck or ts-prune, noUnusedLocals off. knip.jsonc configures knip over the client, server and shared workspaces with their real entry points (main.tsx, index.ts and bootstrap, the MikroORM config, the forked plugin host and its SDK surface, migrations and seeders found by glob, the shared build entries with one per locale, the scripts, the tests and the Playwright launcher). lint:knip (scripts/ci/knip-ratchet.mjs) counts its findings per category and file against scripts/ci/knip-baseline.json, which only shrinks and is built on the client's ratchet helpers. Today's 427 findings are frozen, among them unused server dependencies (nodemon, @types/uuid, eslint-plugin-prettier, @mikro-orm/cli), the unused root unrun, and imports of packages no package.json lists (leaflet.markercluster, geojson). False positives are fixed in knip.jsonc with the reason. knip is a root devDependency; the check runs in server-quality, the job that installs every workspace, and knip.jsonc joins the code area of the change classifier. |
||
|
|
83d807781e |
ci(client): hold strict null check errors per file to a baseline that only shrinks
The client compiles with strict: false, so nullable contract fields arrive as if always present and implicit anys stay invisible. tsconfig.strict.json is the same program with strictNullChecks and noImplicitAny, and lint:strict (scripts/strict-lint.mjs) counts its errors per file against scripts/strict-baseline.json: a file may not gain one, a new file has to be clean, and an entry above its count fails until --update lowers it. Nothing is converted here; today's 534 errors in 165 files are frozen. allowJs stays: the tests import the .mjs scripts and read their JSDoc types through it. The todo MSW handler maps a cleared priority to its default so the test helper written in this branch starts at zero. The check runs after the typecheck in client-quality. |
||
|
|
c266a5e1a7 |
chore(client): freeze typed promise, hook, size, store and global rules in the ESLint baseline
The client lint ran untyped with two react-hooks rules, so nothing saw a floating promise, an async handler where a void one is expected, a whole-store Zustand subscription or a window event bus, all of which client/CLAUDE.md forbids. eslint.config.mjs now lints with the project service and adds, as warnings: no-floating-promises, no-misused-promises (void returns in JSX attributes included), the rest of react-hooks v7's recommended set, complexity 20, max-depth 4, max-params 5 and max-lines-per-function (120 in a .ts function, 300 in a .tsx one, whose body also holds its markup), no-console and no-non-null-assertion. Three local rules in scripts/lib/eslint-rules.mjs cover what no stock rule sees: an empty .catch() callback, use*Store() without a selector (the message points to useShallow, since an object selector without it loops on Zustand 5), and window.__* globals and window.dispatchEvent with an event of our own. scripts/eslint-baseline.json takes today's count of each new rule, so the existing code is frozen, not changed, and each new instance fails lint:warnings. The CI step gets the 8 GB heap typed linting needs. |
||
|
|
242756708e |
ci(client): run theme:lint as a per-file ratchet with the missing token rules
theme:lint only reported a total and exited 0, skipped whole files by name and never ran in CI. It now counts per file under src/ against scripts/theme-baseline.json, which only shrinks, like lint:rtl: the colour literals, arbitrary colour classes and numeric font sizes it already saw, plus palette classes (bg-gray-100, text-indigo-600, bg-white), raw text sizes (text-sm, text-[13px]) instead of the type tiers, z-index literals above z-50 instead of a step of the layering scale, and reads of the dark_mode setting. The file exemptions are gone; the map, PDF and brand files carry their count in the baseline instead. theme-lint-disable markers stay valid in a comment and are counted against scripts/theme-disable-baseline.json, like rtl-lint-disable in lint:rtl; a new one raises its file's entry by hand, where review sees it. The comment handling lint:rtl already had moves into scripts/lib/source.mjs so both read source the same way. The check runs in the client-quality job. |
||
|
|
4427c435a8 |
ci(server): module mocks, hand-built services and test file length in tests/ may only shrink
lint:test-mocks counts vi.mock/vi.doMock of src/config, src/nest/realtime and src/db per file under tests/, lint:test-new-service counts new XService(/new XRepository( there with every class whose constructor takes nothing allowed (derived from src/ and tests/helpers/), and lint:size holds files under tests/ to 2000 lines against their own baseline. Both counters share scripts/lib/count-ratchet.mjs, which gains a prepare hook for a check that reads the tree first. The three run as steps of the existing server lint job; the CLAUDE.md files and the module guide describe the test helpers they point to. |
||
|
|
87071bf738 |
feat(server): @ResponseContract checks a handler's responses against its shared schema in tests
The decorator records the @trek/shared response schema a route answers with. Under NODE_ENV=test it adds an interceptor that parses every response and fails the request with a logged 500 naming the handler and the issues, so the e2e and integration suites catch a response that drifted from its contract; in production nothing is added to the request path and the body is never rewritten. lint:response-contracts holds the handlers still without one to a per-file baseline that only shrinks, and runs in server-quality. |
||
|
|
e7c1acfb47 | chore(server): lint:mcp-zod and lint:service-http hold inline tool zod and error envelopes to a shrinking baseline | ||
|
|
4a3c97e500 |
docs(server): name the query API per statement shape and hold QueryBuilder calls to a baseline
server/CLAUDE.md now says which API new repository code uses: Kysely for join, aggregate and set-based statements, the entity methods for single-table CRUD, no new QueryBuilder calls. The TrekRepository docstring no longer calls Kysely an escape hatch. lint:query-api counts this.qb()/createQueryBuilder() per repository file against a baseline that may only shrink and runs in the server CI job. The same file documents the JSON column and timestamp rules from the two changes before. |
||
|
|
b1da50466d |
ci(server): strict type errors per file may only shrink
tsconfig.strict.json is the build config with strictNullChecks and noImplicitAny on. lint:strict runs tsc over it and holds the errors per file to scripts/strict-baseline.json (204 in 109 files today): a file may only lose errors, a new file starts at none, and an entry above its count fails until --update lowers it. An error outside any file or an unreadable tsc run fails closed. It runs in the server job; the build itself stays on strict: false. |
||
|
|
1ea435d730 |
ci: server and shared lint stop rewriting files, and Prettier is checked per file
shared's lint ran eslint --fix with prettier/prettier as an error, so in lint-prettier.yml the format:check after it only ever saw files it had just rewritten, and the server's format scripts pointed at a test/ directory that does not exist. Now lint is check-only in both (lint:fix applies fixes), the server scripts cover src/ and tests/, and a lint:format ratchet on the shared library holds every file outside scripts/format-baseline.json to Prettier in both workspaces: in the server job of test.yml and in lint-prettier.yml, which keeps providing the lint check. Formatting leaves ESLint in shared, and the unused eslint-plugin-prettier goes. Once the tree is reformatted in one commit, --update empties the baselines and the check is a full prettier --check. |
||
|
|
c9f66156d3 |
ci(server): lint:warnings holds the ESLint warnings per rule to a baseline
Server lint ran without --max-warnings or a baseline, so the rules left on warn could grow without limit. lint:warnings runs ESLint with the server's own config through the shared tally, fails on any error or inline rule config, and holds the warnings per rule for src, tests and eslint-disable'd messages to scripts/eslint-baseline.json, which only falls. It replaces lint:check in CI. no-await-in-loop, no-non-null-assertion and no-console come in as warnings held at today's counts. npm run lint no longer rewrites files; lint:fix does. |
||
|
|
cfef8742f4 |
docs(server): the Nest README is a blueprint again, and guides may only name paths that exist
server/src/nest/README.md drops from 988 lines to under 200: the module shape with weather as the reference, the cross-cutting pieces, the guards, parity and the test layout, without the bridge playbook and the services recipe that contradicted the code. The migration log moves verbatim to server/docs/nest-migration-history.md, marked historical. scripts/ci/doc-paths.mjs resolves every backtick path in the CLAUDE.md files, the root README, the blueprint and the wiki against the tracked files. The paths already missing are listed in a baseline that only shrinks, and the changes job runs it so docs-only changes are checked too. The wiki no longer says the Playwright flows stay out of CI. |
||
|
|
889afeae92 |
build(scripts): server ratchets share one baseline library at the repository root
scripts/lib/ratchet.mjs carries the tree walk, the CRLF-safe reader and the baseline handling the client ratchets already use, next to a warning tally and a format comparison that take ESLint and Prettier from the calling workspace. server lint:size now runs on it with the same messages and exit codes, and the libraries have node:test suites that CI runs in the server job. |
||
|
|
feef872776 |
ci(client): lint:pairs keeps a desktop view and its phone twin on one hook
Most features have a desktop view and a phone view, and until now each carried its own copy of the feature's logic, so a fix on one side drifted away from the other. With the logic moved into one shared hook per feature, scripts/feature-pairs.json names every feature with its hooks and the desktop and phone files that run on them. lint:pairs fails when a listed view stops importing one of its pair's hooks (a type only import does not count), when a listed file is gone, when a hook sits in a view layer rather than under components, hooks, utils or the folder of a page under pages/, and when a file under src/mobile/screens is in no pair and not under mobileOnly with the reason it has no twin. A new phone screen is so either wired to the desktop feature's hook or marked as phone only on purpose. lint:dup catches the copies that look alike; this one catches the copies written anew. The manifest starts with 120 features and 62 phone only screen files, each with the reason it stays on the phone alone. client/CLAUDE.md says how to change a feature, add one with both views and add a phone screen. |
||
|
|
88d196bae8 |
ci(client): lint:dup holds copied code to a baseline that only shrinks
Most features have a desktop view and a phone view, and each carried its own copy of the feature's logic, so SonarCloud's duplication condition was the gate a pull request touching both failed most often, and only after a scan of twenty minutes. lint:dup finds the same copies before the push. It runs jscpd's detector (@jscpd/core with @jscpd/tokenizer) over the source files under src/ at the thresholds the SonarCloud gate applies to TypeScript, 100 tokens over at least 10 lines, and counts per file the lines inside a block that repeats elsewhere or in the same file. Tests and declaration files are left out as Sonar leaves them out, and .ts and .tsx are read as one language, so a hook that repeats a view's code is a copy too. The files are read sorted and with readText, so every machine counts the same. scripts/dup-baseline.json holds the files that still hold copies with their count; a file may hold no more than its entry, a file without an entry none, and an entry above its file or for a file that is gone fails until --update lowers it. --list prints every block with its other side. The engine packages are used instead of the jscpd command, which walks the tree in file system order, skips files past its size limits without a word and drops every result once it meets a file it has no format for. |
||
|
|
ccf8c4309a |
ci(server): check the generated Kysely types and the SQL dialect baseline
check:db-types and lint:dialect were documented as gates but not run. |
||
|
|
1bba65634c |
test(server): seed and read test data through ORM factories
The server suite seeds and reads its database through some 5,500 raw better-sqlite3 statements on the sync handle. They restate the schema by hand, skip the repositories the app goes through and run on SQLite only, so nothing in the suite could ever exercise the dialect layer on another engine. tests/helpers/factories/ adds typed factories per domain (users, trips and days, places, assignments, bookings, budget, packing, to-dos, files, journeys, tours, collections, notifications, settings, plugins, tokens, atlas, vacay, photos, collab) and generic readers (findRow, findRows, countRows, updateRows, deleteRows) that go through MikroORM in a request context of their own, against the app's ORM or a createTestOrm handle. They refuse a write to a read-only foreign-key twin, which MikroORM would otherwise drop without a word. The README there is the conversion guide. The categories integration suite and the tags and calendar-feed e2e suites are converted in full as worked examples; the tags suite also moves from hand-rolled tables to the migrated snapshot. lint:test-sql (scripts/test-sql-ratchet.mjs) holds the raw statements left to a per-file baseline: growth and stale entries fail, files whose subject is raw SQL are exempt with a reason, and a single statement can be allowed with a test-sql-allow comment. CI runs it next to lint:boundaries. |
||
|
|
8018f10273 |
feat(server): gate writes outside a transaction with lint:tx
The rule that a multi-statement write runs in uow.transactional was written down but nothing held it. scripts/tx-writes.mjs reads every class under src/ with the TypeScript parser and counts, per method, the writes it issues outside a transactional callback: repository calls that build an INSERT, UPDATE or DELETE, calls to methods that write (on the class itself or on a constructor-injected one), and a transaction holding writes as one. A write in a loop counts twice, of two branches only the larger counts, local helpers count where they are called, and a helper whose every call sits inside a transaction is not reported. --explain lists the line of every call that counted. Today's 102 reported methods sit in scripts/tx-writes-baseline.json and may only shrink; most are syncs with network I/O between their writes, plugin lifecycle steps and adapters calling two service writes. A new method fails CI, and an entry above what its method holds fails until --update lowers it, the same ratchet as lint:size and lint:boundaries. Two JSDoc tags mark the deliberate cases with their reason in the tag: @txStandalone for a write that stands on its own whatever the caller does (the audit row, notification sends, the booking price mirror, the assignment journey refresh, the quota retention purge), @txIndependent for writes complete one by one on purpose (the OAuth token retention sweep, the notification claim and its release). Runs in CI after lint:boundaries; documented in server/CLAUDE.md, the root CLAUDE.md and the wiki's dev setup page. |
||
|
|
c84d0efb1a |
ci(server): hold unmeasured methods and only SQL errors to the probe baseline
The Postgres probe only ratcheted the methods it managed to call. A method it could not build arguments for, one that threw before its first statement and one that timed out were counted and logged but held to nothing, so a new repository method with a callback parameter could land SQLite-only SQL unseen, and the part of the code the probe reaches could shrink without anyone noticing. pg-probe-baseline.json gains an `uncovered` list of those methods. A method outside it that goes unmeasured fails the run, and so does an entry whose method is measured now or gone, so the list only shrinks. It is seeded from the same first measurement as `failing`; a baseline without the key reads as unmeasured. The failing count also counted every refusal, including data errors the sample values cause (a "probe" string in a date column, SQLSTATE 22007), so renaming a parameter could fail the gate on a change that never touched the SQL. Only classes 42 and 0A count now; the rest is reported in the log and the step summary without being held. The docs said the probe measures every repository statement. They now say it measures what each method reaches with sample arguments against empty tables, and that the methods it cannot reach are held to the list. |
||
|
|
12bf6c0da3 |
ci(server): fail the Postgres probe on an unmeasured baseline
The probe shipped with scripts/pg-probe-baseline.json at `failing: null`, and an unmeasured baseline only reported. Every repository statement Postgres refused, new SQLite-only SQL included, still passed the job; only the dialect helper cases could fail it, so the ratchet was off until some later commit seeded the file. An unmeasured baseline now fails the run outside --update, and the log, step summary and annotation say which artifact file to commit. The job stays red until the first measurement lands, so the gate cannot reach dev holding nothing. A run that called no repository method (an unreadable tsconfig, a moved repositories directory) fails as well, also under --update, so a broken plan can neither pass nor seed an empty baseline. |
||
|
|
b8144bb92a |
ci: probe the server's SQL against a Postgres service
Nothing in CI ran a statement against anything but SQLite, so the claim that the dialect layer keeps the repositories portable was never measured and new SQLite-only SQL could land unseen. The new postgres-probe job (server/scripts/pg-probe.ts, npm run probe:pg) does that against a postgres:17 service container: - it derives a Postgres schema from the entities with MikroORM's schema generator, keeping TEXT timestamps, mapping the SQLite-only defaults and the NOCASE collation, and leaving foreign keys out; - it runs every dialect helper on an in-memory SQLite and on Postgres against the same expectation, and every case has to pass; - it calls every public repository method once with arguments the TypeScript checker builds from the parameter types, through a Postgres driver that runs each statement alone in a rolled-back transaction with a statement timeout and records whether Postgres accepted it. The statements Postgres refuses are held per method to scripts/pg-probe-baseline.json, which may only shrink. The baseline starts unmeasured (failing: null): the job reports, passes and uploads the first measurement as the pg-probe-baseline artifact, together with a JSON report, so the commit that seeds it and every later commit that lowers it needs no local Postgres. The probe refuses a database holding tables it did not create. @mikro-orm/postgresql joins the server's devDependencies at the 7.2.1 the other MikroORM packages resolve to; the image's production install leaves it out. The job needs the preflight gates like every test job, runs only for the code area, stops after ten minutes and is part of ci-ok. |
||
|
|
c8b31b3f57 |
ci: skip the secret-dependent jobs on Dependabot runs
A run Dependabot triggers only sees Dependabot secrets, and its PRs come from a branch in this repo, so the fork guard let s3-contract start with an empty MINIO_LICENSE_KEY. AIStor then never became healthy and ci-ok went red on every grouped bump PR. s3-contract and scan now also skip when Dependabot is the actor; ci-ok already counts a skip as passed. |
||
|
|
5f7d76fd5a |
ci: run the new size, boundary, layering, offline and i18n gates in CI
Each gate came with its npm script but ran nowhere. The server job now runs lint:size, lint:boundaries and shared's contracts:open after the shared build; the client job runs lint:warnings in place of lint:check (it fails on errors the same way and also holds the warning count per rule), then lint:format, lint:layers, lint:offline, lint:skips and lint:i18n-keys. All of them are steps of jobs ci-ok already needs. Both size baselines are taken on the merged branch. Four server files grew while the other changes landed (the claimed reminder, the email uniqueness error and the generator rule for an expression index) and four client files by two or three lines with the plural forms; one client file shrank and is lowered. |
||
|
|
c5ba70f67e |
ci(release): let a later green ci-ok supersede a red run on the same commit
Both release workflows refused a commit if any ci-ok check run on it was not green. Every workflow run is its own check suite, so a red run stays on the commit after a later one passed: a flaky job on the dev push, or a push run cancelled by a manual run on the same ref (ci-ok runs with always() and fails on the cancelled jobs). Running Tests & SonarQube again, as the error told the maintainer to do, added a green suite but never cleared the red one, and the message did not say which run was red. The gate now lives in scripts/ci/ci-ok-verdict.mjs. It fetches every attempt (filter=all), keeps the newest attempt per check suite, refuses while one is still going and requires the run that finished last to be green. It prints the link of every run it counted, and an older red run it overrode shows up as a notice. A response it cannot read, one cut off by pagination or a run without a suite, conclusion or completion time fails the release instead of passing it. Tests run with test:ci-scripts. |
||
|
|
3f2b60b00b |
ci: list moved files under their old path, run the tests for chart, compose and wiki changes
The changes job listed the diff with rename detection on, so a file moved out of an area showed up only under its new path. Moving server code or a chart template under docs/ turned every area off and ci-ok passed on a change that removed source from the build. The diff now runs with --no-renames, and a test reads the exact command out of test.yml and runs it against a scratch repository with a move in it. The code area also missed files the test suites read from disk outside the packages. config-templates.test.ts pins docker-compose.yml and the Helm chart to the code defaults, help.test.ts serves wiki pages and images, and the client's help registry test checks its links against the wiki headings. A chart, compose or wiki change now runs the tests, and a wiki change also builds the image, which ships wiki/ as the in-app help. Markdown is still ignored everywhere except under wiki/. |
||
|
|
d88dc17e8e |
ci(release): build the bump commit, add a minor tag, make the bump an explicit choice
docker.yml built and published whatever `main` pointed to when the build and merge jobs started, not the commit it had just bumped and tagged, so anything that landed in between shipped under a version that did not contain it. build, merge and release-helm now check out the bump commit, the way docker-dev.yml already pinned its prerelease builds. The push of that commit fast-forwards main from the commit the run started on, so a race fails the release instead of skipping over it. Nothing tied a release to a green CI run. Both release workflows now refuse a commit unless every ci-ok check run on it has finished green, and docker.yml refuses to run on anything but main. bump=auto fell back to a patch whenever no prerelease was in flight, which would have published the ORM release, the largest change the project has made, as 4.3.4. auto now only finalizes a prerelease above the last stable tag and otherwise stops, asking for patch, minor or major. The inputs are unchanged. A patch that adds migrations gets a warning, not a refusal, since patch releases have carried them before. Stable releases also publish :MAJOR.MINOR, so an install can take the fixes of one minor line without being moved onto the next on day one. The tag tables in the wiki list it and the prerelease tag, and the root CLAUDE.md describes the release and prerelease flow. |
||
|
|
4cfe6b59ba |
ci: move every workflow off Node 20 actions, pin the third-party ones, add Dependabot
security.yml, lint-prettier.yml and the release workflows still ran actions built for Node 20 (checkout and setup-node v4, github-script v7, the docker actions v3/v5, create-github-app-token v1), which GitHub now forces onto Node 24 with a deprecation warning on every run. They move to the Node 24 majors test.yml already uses (download-artifact to v7, since v6 is still Node 20). Third-party actions, several of which hold the Docker Hub and GitHub App secrets, are pinned by commit with the tag in a comment. ubuntu-latest moves to a new Ubuntu release on 10-19, so every job names ubuntu-24.04 and the switch happens in a PR. Every setup-node reads .nvmrc; publish-plugin-sdk leaves 22, which it ran under a comment claiming it matched the server. security.yml gains an npm audit job over the lockfile, on PRs and pushes into dev as well as main. It covers what the image installs and the plugin SDK, takes seconds, and would have flagged the brace-expansion regression the ORM squash brought in the day it landed. Scout keeps its image scan on main. Neither becomes a required check: a CVE published overnight would turn every open PR red, including the ones that cannot fix it. The scout job now also skips PRs into dev, which the trigger change would otherwise hand it. .github/dependabot.yml adds weekly version updates into dev for the root workspace, plugin-sdk, the actions and the Docker base images, minor and patch grouped per ecosystem, majors one PR each. The Node major in the Dockerfile is left to a hand-made change together with .nvmrc. |
||
|
|
8e695cc267 |
ci: gate merges on one ci-ok check that covers the image and the chart
The ruleset required only `lint` (shared's eslint and prettier) and `check-target`. Types, lint, coverage, end-to-end and the S3 contract were advisory, and the three health PRs merged with red checks. Nothing on a PR into dev built the Dockerfile or rendered the Helm chart either, which is how #2610 shipped a Dockerfile no build could parse. ci-ok needs every gate, test, image and chart job and is meant to be the one check the ruleset requires. It runs with always(), fails when a job it needs failed or was cancelled, and passes when they passed or were skipped. The Sonar scan stays outside: its quality gate reports on the PR by itself, and an outage there should not block merges. A required check cannot sit behind a workflow-level path filter: on a docs-only PR the workflow never starts and the check never reports. The filter moves into a `changes` job. It diffs the PR's merge commit against its base (or a push against the previous head) and scripts/ci/ changed-areas.mjs maps the files to code, image and deploy areas. It fails closed: no usable diff, an empty one or a path it cannot read cleanly runs everything. Its tests run in the same job. docker-image builds the Dockerfile with buildx (layer cache written by pushes only, read by PRs), starts it with the same hardening as docker-compose.yml, waits up to three minutes for /api/health/ready, checks the VERSION file and that / serves the client, and prints the container log. deploy-manifests lints the chart, renders it with the default values and with each secret, persistence and ingress mode, validates the output with kubeconform, and runs `docker compose config` on every compose file. helm and kubeconform are pinned by version and sha256. While the file is open: every job runs on ubuntu-24.04 instead of the moving ubuntu-latest, reads its Node version from .nvmrc (the plugin-sdk jobs ran 22 under a comment saying it matched the server, which runs 24), download-artifact moves from v6, still on Node 20, to v7, and the Sonar action is pinned by commit. |
||
|
|
5189d7fcca |
ci(client): source files may not grow past their size baseline
lint:size counts the lines of every source file under client/src, tests aside. A file may hold 1000 lines; the 24 files that are longer today are listed in scripts/size-baseline.json with their current length and fail the check when they grow. --update lowers an entry once a file shrinks and never raises one, so a long file is split by concern instead of growing. A baseline that cannot be read or parsed stops the check with its reason, and --update leaves it as it is. It runs in the client job next to the reading direction check. |
||
|
|
8dca372c47 |
ci: run the browser flows and the map bundle check on every change
Both only ran on a developer's machine, and both had stopped telling the truth. The end-to-end flows now run in CI in Chromium and in WebKit (the iPad and iPhone cases need the engine every iOS browser uses), with the browsers cached on the lockfile and the traces uploaded when a flow fails. A second job builds the client and fails when one chunk carries both map engines. Making them pass meant fixing what had rotted. The setup closed every first-run notice but the release notes, whose X is called Close, so the notes covered the dashboard and every flow after login timed out on a click. The iPad case still expected undraggable place rows; tablets drag on purpose since #1616, behind the long-press bridge, so it now checks what #1432 is about, a swipe that scrolls and raises no drop overlay, with locators for the scroller and the Plan tab that match today's markup. The iPhone case read the body scroll lock through window.scrollBy, which moves an overflow: hidden viewport in every engine; it now reads the lock where it takes effect and checks the page keeps its place. |
||
|
|
ba70ad28b8 |
fix(client): spacing and alignment follow the reading direction
Arabic runs the app right to left, but margins, paddings, insets, text alignment, borders and corner radii were written for the left side, so icons sat against the wrong edge of their labels, indents and badges pointed the wrong way and amount columns aligned left. They now use the logical forms (ms-, ps-, start-, text-start, marginInlineStart, insetInlineEnd and their CSS equivalents), which are the very same sides in a left to right language. Geometry stays physical on purpose: maps, positions measured from the pointer or a bounding box, time axes, sliders, transforms and print output. Amounts drawn as several spans keep their order in a right to left row, the time picker keeps hours before minutes, and two switches whose thumb slid out of its track in Arabic now move the right way. lint:rtl counts what is still physical per file against a baseline that may only shrink (1512 uses in 370 files before, 319 in 113 now), and CI runs it, so new code reaches for the logical forms. |
||
|
|
1a4df59bf3 |
ci: give every job a timeout
No job had timeout-minutes, so a hung step held a runner for GitHub's six-hour default. Test and housekeeping jobs stop after 45 minutes, the image builds after two hours. |
||
|
|
de8caa81a2 |
Feat/orm : mikroorm (#2416)
* base entities
* update deps
* fix date as string
* rename entities
* migrations + seeders
* feat(db): DbTimestampType keeps DATETIME columns as the CURRENT_TIMESTAMP text
* feat(db): discover entities from an explicit list instead of a glob
* test(db): createTestOrm binds an ORM to a suite's own SQLite handle
* feat(db): dialect helper is the one place SQL functions are spelled
* test(db): dialect helper refuses malformed column references and day counts
* feat(db): snake_case entity shape and the first three repositories (Days, DayNotes, Trips)
* fix(db): repositories mirror the statement's column set and guard their row types
* feat(db): request-context and unit-of-work helpers for the ORM, with concurrency tests
* test(server): integration suites boot on the migrated schema snapshot
* test(server): snapshot harness keeps the legacy id semantics
* fix(test): route MikroORM's dynamic imports through vitest from the setup file, not the ORM config
* test(db): coverage ratchet entries for the ORM foundation
* test(server): websocket suites boot on the migrated schema snapshot
* test(server): coverage's uncovered-file pass goes through the SWC transform
* fix(db): dialect helpers dispatch on the platform; lint gate for the repository layer
* test(server): ORM suites open the schema snapshot; RESET_TABLES derived from the schema
* chore(server): promise safety net and sweep tooling for the async migration
* refactor(realtime, systemNotices, addons, airports, audit, categories, permissions, query-helpers, tags, transit, trip-membership): async DB-touching methods; transactions through UnitOfWork
* fix(server): observe every promise the async sweep left unobserved (timer, handshake, plugin-runtime test)
* test(doc-sync): unit suites await the methods the async sweep made async
* refactor(atlas, school-holidays, settings): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule refusing a Promise used as a plain value, and the misses it found
* test(server): suites await the methods Task 1's forced edits made async
* refactor(budget, common, day-notes, days): async DB-touching methods; transactions through UnitOfWork
* test(oauth): controller suite awaits the async service
* chore(server): db-call-graph --unawaited lists calls to async methods whose result is used without await
* refactor(llm-parse, mcp-shared, oauth, place-shadow, route-usage, storage, todo, trip-invite): async DB-touching methods; transactions through UnitOfWork
* refactor(plugins): async DB-touching methods; transactions through UnitOfWork
* chore(server): db-call-graph --sync stops its transitive walk at an async callee
* chore(plugins): drop gate-only asyncs; observe the scheduler's rejection; async event-sink stub
* refactor(notifications, place-photos, backup): async DB-touching methods; transactions through UnitOfWork
* refactor(auth, files): async DB-touching methods; transactions through UnitOfWork
* test(server): suites await the methods earlier sweep groups made async
* refactor(tokens): async DB-touching methods; the auth token handlers await them
* test(files): upload filters fail closed on an excluded or unresolvable type
* refactor(collab, collections, maps): async DB-touching methods; transactions through UnitOfWork
* refactor(memories, photos, packing, share): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule also refuses a Promise handed to expect() without resolves/rejects
* refactor(vacay, admin, doc-sync): async DB-touching methods; transactions through UnitOfWork
* fix(vacay): type the loop accumulators the async sweep introduced
* refactor(journey, oidc, place-enrichment, tokens, trip-members): async DB-touching methods; transactions through UnitOfWork
* fix(server): the journey hooks are awaited by their callers, not detached
* refactor(assignments, public-api, accommodations, places): async DB-touching methods; transactions through UnitOfWork
* refactor(trips): rewrite AccommodationsService.createAccommodation .bind alias
Recipe R4 trap named in the Task 12 brief: a .bind alias of an
AssignmentsService/AccommodationsService method is typed any and hides a
missing await from tsc and the promise lint rules. createAccommodation
belongs to this task's group, so rewritten as an arrow forwarding to the
method instead of a bound reference.
* refactor(assignments): drop the unused Trip type alias
* refactor(reservations, booking-import, calendar, integrations, roadtrip): async DB-touching methods; transactions through UnitOfWork
* refactor(roadtrip, reservations): return await on transactional; document the single-flight promises
* refactor(trip-read-model, trips, feeds): async DB-touching methods; transactions through UnitOfWork
* refactor(server): remove DatabaseService.transaction — every transaction runs through UnitOfWork
* refactor(trips, feeds): drop added return annotations; no double await on the feed guard
* fix(server): phase 1 review follow-ups — awaited returns, logged upload-filter rejections, covered handshake/notice branches, call-graph gate in CI, dead test imports
* fix(server): upload filters answer multer once on a throwing callback; positive call-graph gate test
* test(db): entity metadata must match the migrated schema; fix the four drifts it found
* feat(db): primary-relation entities proven (BudgetCategoryOrder, VacayUserSettings)
* test(db): parity gate also holds declared pivot entities; per-field column types; missing-set ratchets both ways
* test(db): numeric ids for the primary-relation spike
* feat(db): re-runnable entity generator encoding the snake_case/DbTimestampType rules
* fix(db): generator guard rails — validation cannot mask hidden()/index loss; typed JSON; loud failures
* fix(db): generator keeps NOT NULL boolean defaults; fails if a JSON interface import survives
* refactor(db): every entity in the snake_case shape; document-sync entities added; one repository per entity
* fix(db): no NaN class-field initialisers; entity generator --check gate in CI; parity normaliser keeps literal case
* chore(db): entity --check reports stray files; class-field census is a ratchet; migrate-encryption typechecked
* feat(db): AppSettings and Users repositories; forFeature wiring pattern
* refactor(permissions): permission flags through AppSettingsRepository
* fix(server): boot-time settings read runs inside an explicit ORM request context
* refactor(audit): audit log writes through AuditLogRepository
* fix(db): PK reads refresh past the identity map; typed owned-lookup generics; e2e ORM module mirrors production config
* docs(db): owned-lookup call-site convention; refresh side effect; e2e ORM module spread notes
* fix(audit): audit rows are native inserts, not a unit-of-work flush; closed test ORMs
* fix(server): non-HTTP entrypoints run inside an ORM request context; permission loads fail closed on context errors
* refactor(categories, tags): repositories replace raw SQL
* refactor(addons): addon catalog and photo providers through repositories
* fix(db): generator captures inline UNIQUE constraints
SqliteSchemaHelper.getIndexes() drops every unnamed sqlite_autoindex_
entry before entity-generator ever sees it, so a table-level
UNIQUE(...) constraint with no separate named CREATE UNIQUE INDEX
(settings' UNIQUE(user_id, key) among ~40 others) never reached the
generated entity's uniques: metadata. Adds a general
RULE9_addImplicitUniqueConstraints (skips a column set identical to
the table's own primary key — AppSettings.key/Addons.id noise) fed by
a PRAGMA index_list/index_info scan, and regenerates every entity.
* chore(addons): well-known gate wraps only the addon read in a request context; drop unused test factories
* fix(platform): well-known gate tolerates a synchronous addon read from the context helper
* fix(db): PK lookups inside patch refresh past the identity map; corrected identity-map notes
* refactor(settings): user and instance settings through repositories
SettingsRepository (getForUser/upsertForUser/getOne/deleteForUser) replaces
the per-user settings table's raw SQL; SettingsService drops DatabaseService
entirely. instance-api-keys.ts's readInstanceApiKey/writeInstanceApiKey/
resolveApiKey take an explicit AppSettingsRepository/UsersRepository instead
of a raw connection.
An earlier version resolved those two repositories itself from the active
MikroORM request context, so none of its six callers needed to change. That
worked in production and e2e (always inside a real request) but broke
service construction in plain unit tests across every calling domain
(addons, auth x2, maps, transit, unsplash - 185 failures on a full run),
since none of them wrap a request context. Each of those six services now
injects its own AppSettingsRepository/UsersRepository and passes them
through explicitly, matching how every other converted repository in this
migration is consumed; their own SQL is untouched (outside Plan 3a).
* test(server): harness constructors follow the Task 5 repository signatures; one test ORM per handle in the plugin host
* fix(db): generator names relation properties in captured UNIQUE constraints
* fix(settings): drop dead wiring and the speculative deleteForUser; strict row assertions
* fix(server): plan 3a review follow-ups — fail closed without an ORM at every choke point; seam and D5 ratchet tests; dead method and DDL copies removed
* fix(server): plan 3a re-review follow-ups — plugin boot activation and the airports backfill inside a request context; the supervisor answers the child before failing closed; BOOT-SWEEP-001 drives the real boot hooks; ws adapter seam pinned
* feat(db): generated entities declare their repository type; test casts dropped
* fix(server): plan 3a re-review 2 follow-ups — absent-ORM boot branch pinned; airports boot hook never rejects; BOOT-SWEEP-001 asserts the seven routes; duplicate comment removed
* feat(server): pre-init photo route and WS handshake run in a request context; InviteTokensRepository
* refactor(auth): users table through UsersRepository; JWT and MFA lookups repository-backed
* refactor(tokens): MCP and API tokens through McpTokensRepository
* refactor(auth): passkeys and registration invites through repositories
* fix(db): row-out repository reads bypass the identity map — stale write-back over nativeUpdate at flush; insertUser via em.insert; settings round-trip pinned
* fix(server): non-numeric route ids answer the legacy not-found, never NaN (tokens, categories, tags); mint path back to two statements; identity-map regression pinned
* refactor(oauth): OAuth server tables through repositories
OauthClientsRepository/OauthTokensRepository/OauthConsentsRepository
replace OauthService's raw SQL (OA1-OA33). revokeChain's recursive CTE
and the oauth_clients join (its FK actually targets client_id, not the
row id the generated relation assumes) go through em.getKysely();
OA19's chain revoke is one nativeUpdate($in); OA12's consent grant is
em.upsert on the (client, user) unique key. revokeAllForUser built now
for Task 5. Every row-out read is disableIdentityMap: true; inserts go
through em.insert.
* fix(auth): non-numeric passkey and invite ids answer the legacy 404, never NaN; invite join type pinned
* fix(db): generator emits the referenced column for non-PK foreign keys; OAuth client joins through the relation; FK parity un-skipped
* refactor(db): TrekRepository base — every repository path validates the request context and reads bypass the identity map by default
* refactor(auth): AuthService and account cleanup through repositories
* refactor(oidc): OIDC identities and settings through repositories
* fix(auth): invite validation keeps expires_at null on the wire; password transaction scopes and reset-token burn pinned
* fix(server): plan 3b close-out — case folding on both sides for identifier lookups (non-ASCII login regression); coverage ratchet restored; repository scaffold and instanceof ratchet on TrekRepository; D-shape tests load-bearing
* refactor(server): trip-access primitives async at every caller
* refactor(trips): trip access through TripsRepository; demo seed in a request context; dialect helpers for the trips cluster
* refactor(days): days and day notes through repositories
* refactor(assignments): day assignments through repositories
* refactor(server): trip-membership, place-shadow, place-enrichment, place-photos and query helpers through repositories
* refactor(trip-members): membership, guests and the trip read model through repositories
* refactor(places): places, tags, ratings and place images through repositories
* fix(server): place-shadow source counts ordered in SQL; identity-map ratchets load-bearing; photo-cache race pinned
* refactor(places): import paths through repositories
* refactor(trips): trips through repositories
* fix(server): trip ids parsed once at the gate for places and assignments; NUL-safe value quoting on the SQLite platform; places parity tests completed
* refactor(trips): trip copy through repositories for the owned tables
* fix(server): plan 3c close-out — non-finite ids bound as NULL, trip id parsed once at every places/days/ws gate
Platform: NulSafeSqlitePlatform now renders a bound NaN as NULL and
+/-Infinity as 9e999/-9e999, matching better-sqlite3's own legacy binding
(rule 22 extension) — the program-wide fail-safe for any caller that still
passes an unguarded Number(untrusted) into a converted statement.
PlacesService.verifyTripAccess and RealtimeGateway.handleJoin stop doing
that: verifyTripAccess parses once with toRowId and threads the parsed id
through create/import/bulk/rating/export.gpx (deleting the seven
toRowId(tripId) ?? -1 sites), and handleJoin gets a Number.isFinite guard.
DaysService gets the same treatment across list/create/getDay/reorder/
insert/resyncAccommodationDays, closing the gap where a hex-spelled trip id
could renumber a real trip's days while the raw-bind inverted-accommodation
guard silently matched nothing.
DatabaseService.getTripTitle is deleted; PlacesService injects
TripsRepository directly and calls getTitle itself.
Template/cheap-item cleanup: DayAssignmentsRepository.listForTimeSort uses
$castTo instead of an unknown bridge; the duplicated TagRow interface in
Places.repository.ts is now imported from Tags.repository.ts;
AssignmentWithPlaceRow extends the guarded DayAssignmentRow instead of
restating its columns; PlaceShadowPicksRepository.totals drops its
unreachable ??-fallback branch; Places.repository.ts's updatePlace types
the NOT NULL name column as string; trip-members.service.ts's
transferOwnership threads one parsed trip id through all three writes and
corrects two stale "raw-bind seam" docstrings that were actually Number()
seams; a dead canAccessTrip mock is removed from reservations.e2e.test.ts.
Tests: the 23 new no-explicit-any warnings in TRIP-SVC-069 are typed
(lint back to 2416); HTTP and WS regression coverage for the non-finite
and non-canonical id shapes across places/days routes, mutation-proved;
TRIP-JOIN-005 asserts the exact concurrent-join settlement instead of
>= 1; a new tests/unit/db/transaction-yield.test.ts pins rule 24's
measured transaction-yield/dirty-read/nested-BEGIN behaviour.
* test(server): plan 3c close-out — copy-source full-row parity, Kysely rollback proofs, identity-map relabels, trips e2e on the snapshot DB
* fix(server): plan 3c close-out — days create/insert answer the guard 404 for non-canonical trip ids instead of 500; exact settlement and cross-request yield ratchets
* feat(server): plan 3d groundwork — dialect helpers, visibility predicate, feed and cron context ratchets
* refactor(roadtrip): roadtrip tables through repositories
* refactor(reservations): reservations, endpoints and travelers through repositories
* refactor(feeds): ICS feed tokens and anonymous feeds through repositories
* refactor(accommodations): stays through repositories inside the existing transactions
* test(server): plan 3d — MCP test helper constructors follow the accommodations and days services
* refactor(trips): trip copy through repositories for the roadtrip, accommodation and reservation tables
* refactor(calendar): calendar reads and reservation visibility through one predicate
* feat(server): plan 3e groundwork — packing visibility predicate, memories access-gate audit
* fix(server): plan 3d close-out — legacy text shape for stored accommodation ids restores the date-change restamp; reservation ids parsed once at the gate; parity tests for the reservation repositories
* refactor(photos): trek photos, memories access and cache sweeps through repositories
* refactor(files): trip files and file links through repositories, with transactions added
* test(server): plan 3e — hand-built FilesService, FilesRpc and AllowedFileTypesService calls follow the repository constructors
* refactor(memories): unified, immich and synology photo sync through repositories
* refactor(collab): notes, polls, messages and links through repositories
* refactor(budget): budget items, members, payers and settlements through repositories
* refactor(todo): todo items through repositories
* refactor(packing): packing items, bags and templates through repositories
* feat(server): plan 3f groundwork — systemNotices DI shape, TRAVELER_OWNS predicate, ntfy/storage-health specs, composite-PK pins
* refactor(school-holidays): country and region catalog through repositories
* refactor(notifications): in-app notifications and channel preferences through repositories
* refactor(notifications): reminder crons, mailer, ntfy and webhook transports through repositories
* fix(server): plan 3e close-out — repository registrations restore the boot; packing assignee and bag updates parse and no-op as legacy; coverage and scoping ratchets for the budget, packing and collab repositories
* refactor(vacay): vacation plans, entries, shares and holiday calendars through repositories
* refactor(system-notices): notice dismissals through a Nest provider and repository
* refactor(atlas): visited countries, regions and bucket list through repositories
* feat(server): plan 3g groundwork — GALLERY_CHRONOLOGICAL_ORDER Kysely ordering, unixEpochToIsoKysely helper, composite-PK DDL pins, JB5/R7 confirmations
* refactor(journey): journey and trip access control, CRUD and the trip-sync engine through repositories
* feat(server): plan 3h groundwork — date-offset dialect helpers, entity/repository fitness confirmations, pre-init and encryption pattern confirmations, survivor re-grep
* fix(server): plan 3f close-out — ntfy operator-scope test on the live send path; vacay date-offset and shift-order parity; todo reminder window; coverage and parity ratchets for the atlas and vacay repositories
* refactor(journey): journey stats, entries, photos, contributors and suggestions through repositories
* refactor(journey): public share links, TREK Studio books and the journal facades through repositories
* refactor(collections): access model, hydration, export/import and list CRUD through repositories
* refactor(server): journey-table survivors in trips, photos, memories, auth-cleanup and plugin-contributions through repositories
* refactor(integrations): dawarich connection, sync, tracks and review-flow through repositories
* refactor(collections): saved places, copy-to-trip, labels and invitations through repositories
* refactor(integrations,booking-import,airports,maps,route-usage): airtrail, booking import, airport backfill, maps cache and route-usage counters through repositories
* refactor(doc-sync): connections, reconciler and controller-SQL relocation through repositories
* test(integrations): add AirTrail saveSettings tx-asymmetry ratchet (Plan 3h Task 4 addendum)
* fix(server): plan 3g close-out — transactional book first-save, skeleton reconcile and share-link create; unknown-key entry patches are no-ops again; parity and authorization pins for the journey repositories
* feat(server): plan 3i groundwork — AuditLogRepository's reserved methods, the demo connection-swap spike, repository-fitness confirmations
* feat(server): plan 3j groundwork — scheduler onto CronRegistrarService, SDK-contract confirmation, repository fitness
* refactor(storage): backend/category config and usage stats through AppSettingsRepository; fix the seed-file test race and the shared-replica migration-flip gap
* refactor(share): public share links, the pre-init photo fallback, and collection-places survivors through repositories
* feat(server): plan 3j — the withCamelCase utility, unwired, pending a contract decision
* refactor(admin): user CRUD, stats, audit log, version check and addon config through repositories
* test(admin): pin createUser's pre-existing non-transactional uniqueness-check race (AD2-4)
* refactor(plugins): the trip-write role gate through a repository, in isolation
* refactor(demo,backup): seed and reset through DemoRepository, live-connection backup statements through MaintenanceRepository, isDemoUserId through an injected DemoService
* fix(server): plan 3h close-out — new-value-wins COALESCE restores stay/reservation propagation; doc-sync state filter truthiness; request-context and parity ratchets for share, doc-sync and the integrations
* refactor(plugins): runtime lifecycle, config CRUD and the uninstall cascade through repositories
* fix(server): plan 3i close-out — DemoModule imported where its service is consumed (e2e restored); admin addons list selects the provider relation; saveDemoBaseline awaited; backup tests on the maintenance repository
* refactor(plugins): outbound OAuth tokens and per-user plugin settings through repositories
* refactor(plugins): install-time gates, the capability-audit hash chain and the registry through repositories
* refactor(plugins): contribution-hook controllers, the small plugin controllers and the plugins-table survivors through repositories
* test(server): plan 4 — collapse mcp tool test harnesses onto the ORM snapshot
* test(server): plan 4 — collapse nest service test harnesses onto the ORM snapshot
* fix(server): plan 3j close-out — serialized audit-chain appends, atomic metadata and scheduler limits, rollback and race proofs for the plugin repositories
* refactor(server): plan 4 — convert the program's orphaned raw-SQL callers
* fix(server): plan 4 — un-skip PARITY-009 (FK deleteRule and index-name parity)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (trip-domain services)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (plugins + MCP surfaces)
* fix(server): plan 4 — RULE12 paren-count scanner ignores string literals
The entity generator's RULE12_fixGarbledCheckExpressions CHECK-expression
paren-balance scanner counted every `(`/`)` character regardless of
whether it fell inside a single-quoted SQL string literal. A CHECK
expression like `status IN ('confirmed)', 'tentative')` has a literal `)`
inside its first string value; the scanner would hit depth 0 at that
in-string character and truncate the expression, losing its own real
closing paren. The scanner now tracks single-quote string state (handling
the doubled `''` SQLite escape) and skips parens while inside one.
Latent-bug fix — no shipped entity's CHECK expression has hit this shape
yet (3d's ledger: "nothing affected today").
* refactor(server): plan 4 — unify the trip-access predicate's Kysely copies
TripsRepository.findForViewer/listForUser/activeTrip each hand-wrote the
identical trip-access boolean (t.user_id = ? OR m.user_id IS NOT NULL)
through the Kysely expression builder. Task 7's security review (M1)
evaluated a shared helper and set it aside for time, falling back to the
TRIPREPO-041 cross-method parity test as a regression guard instead.
Extracted the boolean into one generic helper, tripAccessExpr
(_shared/trip-access.ts), following the reservation-visibility.ts
publicReservationExpr precedent: fully generic over <DB, TB> since the
predicate has no correlated subquery, so it composes across
TripSelectKyselyDB and ActiveTripKyselyDB without needing a narrower
fixed-alias shape. The LEFT JOIN itself stays hand-written at each call
site — tripSelectQuery's own docstring already explains why a shared join
helper can't be typed across these differently-joined outer queries.
feeds.service.ts and memories-access.service.ts no longer duplicate this
predicate at all (Task 2's facade-inline sweep already converted both onto
TripsRepository.findAccessible/getFeedTokenIfReachable/
listReachableActiveTrips, which read through the canonical MikroORM
QueryBuilder form, accessibleTripsQuery) — the remaining duplication this
task closes is the three Kysely copies inside this file.
TRIPREPO-041 (64/64 in the file) stays green as the unification's own
regression guard.
* fix(server): plan 4 — trek_photo_cache_meta.cache_key NOT NULL
The column was a bare TEXT PRIMARY KEY, which SQLite does not implicitly
make NOT NULL (unlike an INTEGER PRIMARY KEY rowid alias). Every writer
(TrekPhotoCacheMetaRepository.upsertMeta, the table's only INSERT path)
has always bound a real string — 3e's R5 confirmed this. Appends a new
migration rebuilding the table with cache_key NOT NULL (SQLite has no
ALTER COLUMN, so this follows the same rebuild recipe
Migration20200101020000 uses in the opposite direction) and updates the
entity/generated metadata to match. Schema tightening, not a behaviour
change.
* refactor(server): plan 4 — retire the safely-convertible string-form where conditions (rule 23)
Places.repository.ts's existsInTrip/findInTrip/findRaw/reclaimInputs/
listByTripAndIds and Trips.repository.ts's findPublicForShare all took a
raw `.where('col = ?', [v])` string condition even though their id/trip_id
parameters were already plain `number` (or, for findPublicForShare,
narrowed to `number` here after confirming its one caller,
share.service.ts#getSharedTripData, only ever passes a number sourced
from a Kysely-typed share_tokens row, never an unconverted URL param) —
converted all six to MikroORM's typed `.where({...})` filter object.
The remaining raw string-form conditions in src/db/repositories/** are
NOT converted here: they are the `number | string` raw-bind seam
findAccessible/isOwner's own docstrings describe (trip_id/id threaded
through unconverted from req.params), which structurally requires the
deferred guard-level single toRowId parse to land first — narrowing any
of those methods' signatures now would either break the '0x10'/'007' id
parity those docstrings warn about, or duplicate the deferred item's own
work ahead of it landing.
* refactor(server): plan 4 — trek-photos.repository.ts finishes its rename
3e Task 6 renamed the CLASS (TrekPhotosRepository -> TrekPhotoRegistrationService,
since the name collided with the generated ORM TrekPhotos.repository.ts) but
left the FILE and its test file's names unchanged. Renames both to
trek-photo-registration.service.ts / .test.ts.
The old path is kept as a thin re-export stub (the src/websocket.ts shape)
rather than repointed at every one of its ~20 importers in this pass: several
of those files sit in two sibling tasks' exclusively-owned windows on this
shared branch (Task 3's budget/packing/todo/accommodations/places ripple,
reached through tests/helpers/plugin-host.ts and mcp-test-controllers.ts;
Task 5c's held-back tests/unit/services/{trekPhotoMedia,conflictUpdate}.test.ts)
and are off limits here. Flagged for a follow-up once those tasks land: delete
the stub and repoint every remaining importer at ./trek-photo-registration.service.
* fix(server): plan 4 — plugin discovery's delete+reinsert pairs are atomic
install/discovery.ts#upsert refreshed a plugin's actions and settings-field
descriptors as two independent delete-then-reinsert pairs (DI5-DI8) with no
transaction spanning either pair, let alone both: a crash between a delete
and its own reinsert left that plugin with an EMPTY actions or
settings-field set until the next discovery run, instead of its previous
still-valid rows. Wraps both pairs in one uow.transactional, so a discovery
refresh for a plugin's descriptors is now all-or-nothing.
Deliberate behaviour change, named for the user per the task brief.
DiscoveryRepos grows a UnitOfWork; PluginRegistryService (the other
production caller, registry.service.ts) picks it up the same @Optional()
way PluginRuntimeService already does, with the same guard-throw. Updates
the two hand-constructed test call sites that actually exercise discovery
(discovery.test.ts, registry.test.ts) to pass a real UnitOfWork.
* refactor(server): plan 4 — facade inline sweep part B (rosterUserIds/getPlaceWithTags)
Inline DatabaseService.rosterUserIds onto TripMembersRepository.rosterUserIds
directly in budget.service.ts, budget.mcp.ts and todo.service.ts, and
DatabaseService.getPlaceWithTags onto PlacesRepository.findWithTagsAndRatings
in accommodations.service.ts (placesRepo was already injected there).
BudgetService/BudgetMcp/TodoService collapse their constructors, dropping
the now-fully-unused DatabaseService param (budget.module.ts/todo.module.ts
register TripMembers on forFeature). PackingService keeps DatabaseService as
a vestigial param behind an optional TripMembersRepository with a fallback
in tripRosterIds: an in-flight Task 5c file
(tests/unit/services/conflictUpdate.test.ts) hand-constructs PackingService
positionally without the new repository, so its arity cannot change this
task; the fallback keeps that file's DatabaseService.rosterUserIds spy
working until Task 4 drops the param with the class itself.
* test(server): plan 4 — constructor ripple for the rosterUserIds/getPlaceWithTags inline
Hand-built BudgetService/BudgetMcp/TodoService call sites drop the removed
DatabaseService constructor arg (budgetRepoArgs() now appends a
TripMembersRepository so most spread call sites need no other change);
budget.service.test.ts's rosterHas() helper and svc() factory move from a
spied DatabaseService to a stubbed TripMembersRepository. The
AccommodationsService helper (tests/helpers/accommodations-service.ts) and
its two direct callers drop the same arg. PackingService call sites are
untouched (its new param is optional, so every existing hand-built instance
still resolves rosterUserIds through the DatabaseService fallback).
* fix(server): plan 4 — thread the discovery UnitOfWork through PluginRuntimeService too
Complements the just-landed discovery atomicity fix (DiscoveryRepos.uow):
PluginRuntimeService's own discoveryRepos getter still built the bundle
without a uow field. Threads its already-injected, @Optional() uow through
so its own onApplicationBootstrap/reload/dev-link discovery calls also get
the transactional DI5-DI8 refresh, not just PluginRegistryService's.
No throwing guard here, unlike PluginRegistryService's: several hand-built
partial-DI-graph test instances of this class (boot-registry-order.test.ts,
in a sibling task's held file window) construct it with uow omitted and
exercise onApplicationBootstrap's discovery call directly. Discovery must
never refuse outright just because a test built a partial graph, so it
degrades to the pre-existing untransacted sequence when uow is absent
(DiscoveryRepos.uow's own docstring covers this), the same "boot must
never block app init" defensiveness this file already applies elsewhere.
* fix(server): plan 4 — guard-level single parse: verify + correct the overclaiming verifyTripAccess docstring (L-2)
TripAccessGuard/TripOwnerGuard already parse :tripId exactly once
(Number(), guarded by Number.isFinite) and hand the resolved TripAccess
row to the handler via request[TRIP_REQUEST_KEY], TripOwnerGuard reusing
TripAccessGuard's own lookup when both are on the same route rather than
re-querying — verified by reading both guards directly, no change needed
there.
PlacesService.verifyTripAccess's own docstring separately claimed its
returned `tid` gets "returned... for every downstream call to reuse
(never re-parsing)" (3c's Task 9 review, L-2, carried to Plan 4). Reading
every call site (not assumed) shows this is false: `.tid` has exactly one
reader (PlacesController.requireTrip itself), which discards it — none of
requireTrip's 13 callers read `.tid` off the trip it returns; each passes
the original raw tripId string on to the write/read method it calls next,
which toRowId-parses it again itself. That second parse cannot be
eliminated by this gate alone: the same service methods are called
directly by places.mcp.ts too, an entry point with no requireTrip gate at
all (its own numeric tripId is re-stringified, String(tripId), to call
them), so each method owns its own id validation regardless of entry
point. Threading `tid` all the way through would mean giving every one of
~15 methods a second, number-only call shape for the REST path alone to
use — a real refactor, out of this docstring fix's own scope. Corrected
the docstring to say so plainly instead of repeating the false claim.
* refactor(server): plan 4 — relocate JS7/10/13/14 off the journey-share fallback stub
JourneyShareTokensRepository's own JS7/JS10/JS13/JS14 Kysely reads were a
3g Task 3 fallback (Task 2's JourneyPhotosRepository/JourneyEntriesRepository/
JourneyEntryPhotosRepository were still mid-flight). Now that those
repositories are stable, relocate: JS7/JS10 (photo/asset validation joins)
onto JourneyPhotosRepository (already injected into JourneyShareService as
photosRepo, no constructor change); JS13 (public entry list) onto
JourneyEntriesRepository.listPublicEntries; JS14 (public per-entry photo
read) onto JourneyEntryPhotosRepository.listForPublicJourney, deduped
against that repository's own JP_COLUMNS select list instead of a
hand-duplicated copy. JourneyShareService gains two new @InjectRepository
params (both already registered in journey-domain.module.ts's forFeature),
rippled through mcp-test-controllers.ts and journey-share.service.test.ts
(P04/P05 parity tests now pin the new homes).
* refactor(server): plan 4 — document unreachable ?? default arms after single-row aggregates
3f Task 7's fix wave flagged a cluster of `row?.x ?? default` arms after
unqualified, ungrouped aggregate reads (MAX/COUNT/COALESCE(SUM,0)) as
"same class as OauthClients:147" but left them unexamined per-site. Decided
per site: document, don't delete — TypeScript still types
executeTakeFirst()'s result as `| undefined` even though SQL guarantees
exactly one row for these unqualified aggregates, so the optional chain
can't be removed without a non-null assertion (worse, not safer). Six
sites: JourneyEntries.repository.ts (maxSortOrderForDate JG42,
countStatsPlaces JG67), JourneyEntryPhotos.repository.ts
(maxSortOrderForEntry JG91), JourneyPhotos.repository.ts (maxSortOrder
JG88/JG99), VacayEntries.repository.ts (sumFraction VC1, sumCompFraction
VC2). No behavior change — comments only.
* refactor(server): plan 4 — unify the two user-data-erasure enqueue paths
UserCleanupService.erasePluginUserData (the account-deletion transaction's
own UC2/UC3 enqueue) and PluginRuntimeService.enqueueUserErasure (the
emitUserDeleted sink's post-commit enqueue) independently walked the
identical SELECT id, permissions FROM plugins + JSON.parse + hook:user-data
filter + INSERT OR IGNORE — and both genuinely run for every account
deletion, since every emitUserDeleted call site (auth.service.ts,
admin.service.ts, trip-members.service.ts's guest removal) fires
immediately after deleteUserCompletely.
Extracted the shared filter into enqueueHookUserDataErasures
(nest/plugins/user-erasure-enqueue.ts) and pointed both call sites at it.
UC2/UC3 also come off DatabaseService onto PluginsRepository/
PluginUserErasureQueueRepository in the same change (they were the last
raw statements standing in the way of sharing one implementation) — this
narrows the Plan 3b Task 5 "stays raw, owned by Plan 3j" ruling to UC1
only (plugin_user_config/plugin_oauth_tokens/plugin_oauth_state), which
still stays raw, unaffected. UserCleanupService's orphan-data-directory
scan (a plugin uninstalled with data retained) has no equivalent on the
enqueueUserErasure side and stays there, now via the same repository.
Both enqueue calls still run per deletion (INSERT OR IGNORE keeps this
idempotent) — this unifies the IMPLEMENTATION, not the call count; not
touching the emitUserDeleted event-sink wiring itself was a deliberate,
smaller-blast-radius choice for this task.
* refactor(server): plan 4 — narrow TripMembersRepository.remove/TripsRepository.setOwner to number
Both accepted trip_id: number | string per 3c's carry note (their "stays
loose for other callers" reasoning), pending the guard-level single parse
landing first. Re-checked every caller directly: setOwner's one production
caller (trip-members.service.ts#transferOwnership) already passes trip.id,
a real number resolved earlier in the same transaction; remove's callers
are trips.rpc.ts (num(), already a validated number), trips.mcp.ts (Zod-
typed tool input, already a number) and TripMembersController.removeMember
(the one genuine raw-string caller, :id off the route with no class-level
TripAccessGuard on this controller by design).
TripMembersController.removeMember now parses :id once (toRowId, not
Number() — rule 15's NaN-into-SQL trap) and threads the parsed number to
both its own access check and removeMember, instead of passing the raw
string through twice. TripMembersService.removeMember and both repository
methods narrow to number accordingly; setOwner's WHERE also moves off its
raw-bind escape hatch onto nativeUpdate now that trip_id is typed, and
remove's trip_id half moves onto a typed .where({ trip: trip_id }) filter
(user_id keeps its own raw bind — a different, still-live NaN-safety seam,
unrelated to this narrowing).
TRIPREPO-018 and TMEMREPO-023, which pinned the retired string-bind seam
on setOwner/remove, no longer typecheck against the narrowed signatures
and are removed with an explanatory comment in their place.
* refactor(server): plan 4 — U6 gate-level id parsing for todo item ids
Rule 21's carry from 3e Task 8's review: TodoController.update/.remove
passed the raw :id route string all the way to TodoItemsRepository,
relying on SQLite's column-affinity CAST to match it instead of a typed
filter. Parse :id ONCE at the controller (toRowId, matching Task 8a's
TripAccessGuard/removeMember shape), 404 on a non-canonical id, and thread
the number down through TodoService.updateItem/deleteItem into
TodoItemsRepository.findInTrip/existsInTrip/update/deleteById, all
narrowed from number | string to number. todo.rpc.ts's num()-derived
todoId no longer needs its String() wrapper. tripId stays string | number
— a separate, already-accepted carry (
|
||
|
|
81ebc31698 |
build(docker): OCI labels on the images, so Renovate links an update to its release notes
The image carries org.opencontainers.image.source, url, documentation, licenses, title, description and version; the release and pre-release workflows add the commit and the build time, and the multi-arch index gets source, version and licenses as annotations. (#1498) |
||
|
|
5d04f9c400 |
fix(docker): move start-up into a script so an edited container still boots
The image carried its whole start-up sequence as one inline `sh -c` string containing four single-quoted echo messages. Container management UIs render Config.Cmd back into a text field and re-split it on submit with a quote-aware, escape-unaware tokenizer, so editing a container in place — the usual way to change an environment variable — shredded the quoting and left dash half an if-block to parse. The container then died before Node started, with only `TREK: 1: Syntax error: end of file unexpected (expecting "fi")` in the log: the word TREK is the stranded fragment of the first echo, reported as $0. The logic moves to server/scripts/entrypoint.sh unchanged, and CMD becomes a single bare token, which has nothing left to mangle. A unit test pins that shape — both exec-form lines parse, and every argv element stays one token free of quotes and spaces — and the Dockerfile joins the test workflow's path filter so that guard actually runs when someone edits it. Closes #2374 |
||
|
|
f513881c01 |
ci: pin the platform order in the multi-arch manifest
The manifest list was assembled from a glob over the downloaded digest files, which are named after the digest itself. The order was therefore alphabetical by hash, and it changed from one release to the next: 3.4.1 lists amd64 first, 4.0.0 lists arm64 first. That is not supposed to matter — a client is meant to pick the entry matching its own platform — but several people updating from 3.4.1 to 4.0.0 on amd64 reported that `:latest` still resolved to 3.4.1 and only a pinned `:4` or `:4.0.0` pulled the new image. Unraid came up twice. Content, format and digest of the three tags are identical, so the flipped order is the only thing that changed between the release that worked and the one that did not. The digests are now collected per platform in a fixed order, amd64 first. That needs the artifacts unmerged, since the per-platform folder name is the only thing that says which digest belongs to which architecture. Second thing the loop fixes: the glob happily built a single-platform manifest if one of the two builds had failed to upload. It now fails loudly instead of publishing a `:latest` that half the users cannot pull. |
||
|
|
d194d2cf6f | chore(workflows): paths target on push | ||
|
|
9bbbef6279 | chore(workflows) add concurrency with cancel in progress | ||
|
|
b58515e47e | chore(workflow): test workflow how now concurrency key | ||
|
|
b98363b2b0 | chore(workflow): single workflow that runs the test AND the sonar gate (#2046) | ||
|
|
981a634dfb | chore(sonar): switch to cloud | ||
|
|
fe5c6f182e |
fix(ci): restore the peer dep the swc install drops, and close a test race (#2040)
Two separate reasons v4.0.0 (#1719) is red. The SonarQube workflow killed all 616 client test files with "Cannot find module '@testing-library/dom'". That package is a peer dependency of @testing-library/react and not a direct dependency of the client, and the step that installs @swc/core's Linux binary passes --legacy-peer-deps, which tells npm not to put peer dependencies back when it re-resolves the tree. In test.yml that step lives in the server job, which never runs client tests, so folding the jobs into one (#2039) is what brought the two together. It now installs alongside the swc binary, with its version read from the peer range rather than pinned, and a one-line resolve check right after fails fast with the module name instead of six hundred identical failures twenty minutes later. MAdminStoragePanel's FE-MOB-MSTOR-015 was a genuine race in the test, not in the panel. Its POST handler incremented postCount, then awaited request.json(), and only then pushed to migrationPosts — so between the two there was a window where one POST had been counted but migrationPosts was still empty. The GET handler reports `migrations: []` for an empty migrationPosts, i.e. "nothing running", so a poll landing in that window told the queue it was free to start the second migration, and the waitFor(postCount === 1) above could resolve inside it too. Parsing before counting closes it. Verified by running the whole mobile suite at --maxWorkers=8, where it used to surface. |
||
|
|
6fe5009c20 |
ci(sonar): fold the four coverage jobs into one, on the root scripts (#2039)
Review feedback on #2038: the root already has a script that does all of this, so the five jobs and the artifact shuffling between them were doing by hand what `npm run test:cov` does on its own. It ends with `--prefix plugin-sdk`, so all four packages are covered, and each writes its lcov to <pkg>/coverage/ — exactly where sonar.javascript.lcov.reportPaths looks. 131 lines become 28. Three things the plain root sequence still needs on a runner, kept as their own steps so the reason stays visible: - plugin-sdk is not a root workspace (workspaces = client, server, shared), so the root npm ci never reaches it and it needs its own. - The lockfile was generated on Windows and omits @swc/core's Linux optional binary, so the server's SWC transform cannot load. test.yml carries the same step for the same reason. - Only shared is built, not the root `build`. server and client run their suites from source through vitest, so building them adds a tsc pass and a full vite bundle that nothing here reads. The cost is wall-clock. The first run of the parallel version took 16 minutes, with client (11) and server (9) overlapping; sequentially that is closer to 25. Nobody waits on this job, so the simpler file is the better trade — but it is a trade, not a free win. |
||
|
|
616b39a9f0 |
ci: run the SonarQube analysis from Actions instead of by hand (#2038)
The scan has been a manual step on somebody's machine, which is why the dashboard could sit on a stale commit without anyone noticing. This runs it on every push to dev. Push to dev only, and that is the point: the instance is Community Edition, so it has no branch or pull-request analysis and the project holds exactly one view. Analysing a feature branch would overwrite that view with the branch's numbers until somebody scanned dev again, so the one branch that may publish is the one everybody reads. The four coverage jobs mirror the ones in test.yml because Sonar needs the lcov each package emits — without it every line reports as uncovered and the dashboard shows a collapse that did not happen. They run in parallel and hand their reports to the scan job, which needs a full clone: Sonar reads git blame to date each issue and to decide what counts as new code. sonar.projectVersion stays pinned here rather than read from package.json. package.json is still on 3.4.1 while the project has been analysed as 4.0.0 all along, and since New Code is "previous version", taking the lower number would silently re-base every new-code metric. Needs two repository secrets before it can run: SONAR_TOKEN and SONAR_HOST_URL. |
||
|
|
4548743874 | chore(packages): add test coverage support to plugin sdk and shared package | ||
|
|
eadb3703bc |
fix(shared,tests): bound the book contract and close the gaps that hid all this
Book page setup and element geometry were unbounded numbers, so a shared book could be saved with zero, negative or astronomical dimensions and every consumer downstream - the canvas, the PDF export - had to cope. Day-note caps had drifted from the limits their own doc comment states, and icon had none. On the test side: the path filter meant a PR touching only the root lockfile ran no jobs at all, which is exactly the change class most likely to break a native binding; the canAccessTrip mock is hand-copied into 72 files and had already drifted from the real query; the path-traversal test asserted only status !== 200, so a 500 from a broken guard passed it; and one unit test shelled out to a real kitinerary binary if the host happened to have one. |
||
|
|
76c8c349b1 |
Fold nest mcp (#2017)
* refactor(server): fold @trek/nest-mcp into server/src/nest-mcp * docs(server): point nest-mcp references at server/src/nest-mcp * remove tracked md file |
||
|
|
305b113d5f |
Feat/storage driver (#1949)
* feat(storage): storage key validation, driver contract suite, LocalDriver * feat(storage): MirrorDriver composite backend * feat(server): add TREK_UPLOADS_DIR env var for the uploads root * feat(storage): settings-backed registry, StorageService facade, module wiring * test(storage): pin src/nest/storage coverage thresholds * feat(storage): multer upload options factory * refactor(files): spool trip-file uploads through storage * refactor(collab): spool note attachments through storage; normalize files/ prefix * refactor(journey): spool journey media uploads through storage * refactor(trips): spool trip cover upload through storage * refactor(places): spool place-image and collection uploads through storage * refactor(auth): spool avatar upload through storage * fix(server): consolidate allowed_file_types onto AllowedFileTypesService * fix(storage): default defParamCharset to utf8 in the upload factory * test(db): re-point the trailing-migration guard at the trip_files prefix strip * fix(storage): age-gate the boot spool sweep * test(files): key FILE-P02 residue checks on upload size * test(storage): raise src/nest/storage coverage pin * refactor(platform): serve /uploads routes through storage * refactor(files): serve trip-file downloads through storage * refactor(memories): serve photo bytes through storage * refactor(maps): stream place-photo bytes through storage * refactor(share): stream shared place-photo bytes through storage * refactor(journey): serve public journey assets through storage * fix(share): answer place-photo cache miss with 204 like the maps proxy * test(server): close serving-swap gaps surfaced by the full tier * test(storage): raise src/nest/storage branches pin * refactor(place-photos): route place-photo cache through storage * refactor(places): reclaim place images and collection covers through storage * refactor(trips): trip covers through storage * refactor(auth): avatar reclaim through storage * refactor(collab): note-file deletion through storage * refactor(files): trash deletion through storage * refactor(memories): trek photo cache internals through storage * refactor(memories): thumbnail service category-addressed * refactor(memories): immich upload reads through storage; drop uploads-root * refactor(journey): photo deletion through storage * fix(memories): reclaim orphaned trek photo cache objects in the ttl sweep * feat(journey): journey-thumbs orphan sweep and delete-site derived-artifact reclaim * test(server): close slice-4 gaps surfaced by the full tier * test(coverage): raise slice-4 domain pins * refactor(backup): list, existence and delete through storage * refactor(backup): download through storage sendToResponse * refactor(backup): create backup through per-category storage enumeration * refactor(backup): restore and upload-restore through storage * refactor(backup): auto-backup retention through storage * refactor(server): boot dir-creation to driver init * test(server): close slice-5 gaps surfaced by the coverage tier * test(coverage): raise slice-5 backup pin * test(server): resolve async backupFileExists mocks for the tests typecheck * test(server): pin ranged-read stat and zero-byte roundtrip in the storage driver contract * feat(server): TREK_S3_* env wiring — schema, s3 namespace, all-or-nothing boot rule * feat(server): S3 driver core — aws-lite client seam, stat/delete, driver-owned deadlines * refactor(server): type the aws-lite S3 client via s3-types reference * feat(server): S3 driver getStream and list — ranged full-object stat, iterator pagination * feat(server): S3 driver put — bounded-peek routing, Upload inactivity deadline * fix(server): S3 driver Upload monitoring — Transform interposition, stream cleanup * feat(server): env-declared s3-main storage backend; settings-declared s3 rejected * test(server): env-gated S3 driver contract suite against MinIO * fix(server): S3 driver zero-byte put — work around aws4 duplicate Content-Length signing defect * fix(server): scope the zero-byte put workaround to buffered bodies; bind test MinIO to localhost * ci(server): MinIO-backed S3 driver contract job * ci(server): align s3-contract shim comment and MinIO bind with references * docs: S3 storage backend configuration and off-box backup mirroring * docs: restore README formatting, keep S3 additions only * ci(server): make live-MinIO contract runs manual-only (AIStor licensing) MinIO's community line is discontinued entirely; the maintained successor, AIStor, requires a license key that can't safely live in a shared CI runner. The s3-contract CI job is removed; live-provider contract runs against MinIO/AIStor are now a manual, license-gated procedure documented in docker-compose.minio-test.yml's header. * chore(server): final-review polish — stale CI refs, dockerignore key, boundary pins, zero-byte tempfile contract case * expose admin ui * feat(shared): storage backend type registry, wire schemas, category relocation * feat(server): accept settings-declared s3 backends — shared-schema options, decrypt-at-build, registry preview() * feat(server): remove TREK_S3_*/TREK_UPLOADS_DIR — tombstone boot rule, built-in override as the relocation path * feat(server): deriveSecurity.encryptionKeySet and storage secret plumbing (gate, encrypt, unmask, mask, redact) * chore(server): drop storage-secrets import that only Task 8's probe helper will use * feat(server): seed-once storage-config.json boot import — fail closed on any invalid seed * feat(server): registry snapshot() — effective storage config with provenance * feat(server): api/admin/storage GET/PUT — masked secrets, preview-gated saves, wired reload, audits * feat(server): api/admin/storage/test — per-target ephemeral probes, mirror targets probed individually * test(server): e2e coverage for api/admin/storage — guards, managed refusal, envelope, probes * test(server): give e2e harness DBs the app_settings table the storage registry reads at boot * test(coverage): raise pins after the storage admin-config slice * fix(server): final-review polish — encryption-gate wording, all-field sentinel guard, README/e2e notes * test(server): storage slice-1 review follow-ups — mirror s3-replica probe decrypt, all-encrypted seed without key, test-route 401 * feat(i18n): storage admin domain + tab label in every locale * feat(client): adminApi storage endpoints — getStorage, updateStorage, testStorageBackend * feat(client): storage admin draft-document helpers + relative-time util * feat(client): schema-driven storage BackendForm — fields by kind, mask echo, encryption banner * feat(client): admin storage panel — backends list, category map, health strip, one-PUT save * feat(client): storage tab on the admin page * feat(client): storage admin panel and section on the phone shell * fix(client): block renaming a storage backend onto an existing name * feat(server): allow mirrors on any storage category — lift the backups-only rule * feat(i18n): replicas-on-primary mirror strings in every locale * feat(client): mirror fold/synthesize model helpers for replicas-on-primary * feat(client): BackendForm mirror-targets composer; raw mirror type hidden * feat(client): desktop storage panel folds mirrors into their primaries * feat(client): phone storage panel folds mirrors into their primaries * fix(client): strip removed backends from remaining mirror replicas — no phantom refs * feat(storage): retire photos as a configurable category — served-only via the ServedCategory seam * feat(i18n): descriptive storage category names and descriptions in every locale * feat(client): descriptive category names, descriptions and id badges in the storage panel * docs(storage): fix stale nine-category comments after the eight-category shrink * docs: storage configuration via the admin UI and seed file — README + compose * docs(wiki): Admin Storage page and storage cross-links * docs(server): storage domain blueprint entry; TREK_PLACE_PHOTO_DIR tombstone clarifier * refactor(server): drop the storage env-var tombstones — the variables never shipped * docs: storage config is simply new in v4 — drop the removed-variables story * feat(shared): backfill-status and usage wire schemas on the storage admin state * feat(server): MirrorDriver.backfill — prefix-scoped, size-compared, cancellable replica catch-up * feat(server): one-at-a-time storage backfill jobs + the storage events bridge * feat(server): storage usage scan — nightly cron, on-demand service, persisted with computedAt * feat(server): replica_failure admin notifications — events bridge, hourly debounce, channel registration * feat(i18n): replica-failure notification text in every locale * feat(server): storage backfill and stats-refresh admin endpoints * feat(client): adminApi backfill and stats-refresh endpoints * feat(i18n): storage sync and usage strings in every locale * feat(client): desktop storage panel — sync now, usage display, backfill polling * feat(client): phone storage panel — sync now, usage display, backfill polling * fix(client): per-category usage line on the phone storage panel * docs: sync now, usage stats and replica-failure notifications * test(server): admin scope now includes replica_failure, not just version_available Commit |
||
|
|
1aa1aea57e |
ci: retarget Dependabot pull requests onto dev instead of labelling them (#1926)
Dependabot cannot be pointed at dev. \ arget-branch\ in dependabot.yml applies to version updates only, and security updates always open against the default branch. Security updates are all this repository gets, since there is no dependabot.yml at all, so every one of them arrives on main. The branch check then labelled them wrong-base-branch and close-stale-wrong-branch shut them 24 hours later. Thirteen of the seventeen Dependabot pull requests died that way; the four that survived were retargeted by hand. The comment the check leaves asks the author to click Edit, which a bot cannot do. So retarget them instead. If that fails, because the branch was cut from main and dev has moved on, it falls through to the existing path so a human sees it rather than the check quietly passing. Note this only takes effect once it is on main: pull_request_target loads the workflow from the pull request's base branch, and these arrive on main. |