8 Commits

Author SHA1 Message Date
29e4b08947 fix(web): pnpm 11 renamed the build allowlist to allowBuilds
Some checks failed
deploy / build-web (push) Successful in 1m36s
deploy / build (push) Successful in 6m26s
deploy / deploy (push) Successful in 18s
deploy / deploy-web (push) Failing after 13s
The runner is pnpm 11.24.0; this workstation is 10.30.3. The key was
renamed across that major: pnpm 10 reads `onlyBuiltDependencies` (a
list), pnpm 11 reads `allowBuilds` (a package -> bool map). Each ignores
the other's key, which is why the runner kept reporting ignored builds
while every local check passed.

The diagnostic step found it in one run, after three failed guesses.
`pnpm config get onlyBuiltDependencies` on the runner returned the list
correctly, which was the misleading part -- the value resolved fine, it
simply is not the key pnpm 11 consults. `pnpm approve-builds @swc/core
esbuild` then wrote the pnpm 11 form and named it outright.

Declares both keys so neither version is left out, rather than pinning
the fleet to one. Verified by reproducing the failure locally under
pnpm 11 (npx pnpm@11.24.0), then confirming install, lint and build pass
under both 11.24.0 and 10.30.3.

Drops the package.json `pnpm` field entirely: pnpm 11 warns it is no
longer read for anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 13:32:23 +03:00
78d35cfd29 ci(web): declare the build allowlist in both places, and report pnpm's view
Some checks failed
deploy / build (push) Successful in 6m31s
deploy / deploy (push) Successful in 19s
deploy / build-web (push) Failing after 1m10s
deploy / deploy-web (push) Has been skipped
Third build-web failure with the same ERR_PNPM_IGNORED_BUILDS. The
setting was first in package.json (pnpm 10 here does not read it) and
then in pnpm-workspace.yaml (verified read here, still ignored on the
runner), so the runner's pnpm evidently resolves it differently and I
have been guessing at which.

Declares it in both locations -- four duplicated lines against a failure
that only manifests on CI -- and adds a step printing pnpm --version and
the resolved value, so if this still fails the log says why instead of
costing another round trip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 13:21:54 +03:00
05c3e56dba fix(web): put the build-script allowlist where pnpm 10 actually reads it
Some checks failed
deploy / build (push) Waiting to run
deploy / build-web (push) Failing after 1m8s
deploy / deploy (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
The previous attempt set pnpm.onlyBuiltDependencies in package.json.
pnpm 10 does not read it from there -- `pnpm config get
onlyBuiltDependencies` returns undefined -- so the runner failed exactly
as before.

It looked fixed locally because the pnpm store had already built both
packages, so a reinstall printed no warning whether or not the allowlist
was being read. "No warning" was not evidence; `pnpm config get`
returning the values is, and it does now.

Moves the allowlist to pnpm-workspace.yaml, which is where pnpm 10 takes
its settings, and drops the inert package.json field rather than leave
two places appearing to configure the same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 13:18:08 +03:00
8595529051 fix(web): approve the two dependencies that need install scripts
Some checks failed
deploy / build (push) Waiting to run
deploy / build-web (push) Failing after 54s
deploy / deploy (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
pnpm 10 refuses to silently run a dependency's build script. Locally
that is a warning; on the runner it is ERR_PNPM_IGNORED_BUILDS and the
job fails, so build-web broke on run 13 while everything passed here.

@swc/core and esbuild both fetch a platform-native binary at install
time and do not work without it, so they are allowlisted explicitly via
pnpm.onlyBuiltDependencies. Everything else in the tree stays denied,
which is the point of the feature -- an install script is arbitrary code
from a transitive dependency.

Also sets strict-dep-builds in web/.npmrc so a skipped build script is
an error here too. Same reasoning as the release-age policy added
earlier: a local install that passes where the runner fails is a
difference only discoverable by pushing, and that is a slow way to find
out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 13:11:31 +03:00
3790f2589c fix(core): the per-key manifest lock did not actually exclude
Some checks failed
deploy / build (push) Waiting to run
deploy / build-web (push) Failing after 55s
deploy / deploy (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
The lock added alongside compare-and-set removed its map entry inside
the critical section. A queued waiter still held that mutex, so the next
arrival found no entry, created a fresh one, and the two then ran
concurrently -- defeating the lock exactly when there was contention,
which is the only time it does anything.

I previously reported this fix as working on the strength of ten clean
release-mode runs. That was wrong: in debug, where the timing differs,
the guarding test fails 3 times in 12. Retiring the entry only after
dropping our own reference, under the same shard lock `entry` takes,
gives 0 failures in 25 -- and restoring the old behaviour reproduces 3
in 12, so the test really does catch it.

The consequence was the one the test names: a file served to a client
but never recorded, and so invisible the moment the instance is sealed.
Real against the local filesystem store, which has no conditional put
and relies on the verify-after-write retry; MinIO's compare-and-set
covers the same race, which is why the deployed path never showed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 13:03:55 +03:00
5a94d90b79 fix(ci): restore the format gate and match the runner's package policy
Some checks failed
deploy / build-web (push) Failing after 58s
deploy / build (push) Successful in 6m34s
deploy / deploy (push) Successful in 17s
deploy / deploy-web (push) Has been skipped
Run 12 failed both build jobs, for two unrelated reasons of mine.

cargo fmt: the previous commit ran clippy and the test suite but not
the formatter, and inventory.rs was written by hand. Formatted.

pnpm: the runner enforces a minimum release age on lockfile entries --
a supply-chain safeguard against a package published minutes ago -- and
rejected electron-to-chromium@1.5.417 and ignore@7.0.7, both published
inside its 24h cutoff. My local install had no such policy, so the
lockfile resolved to them and the failure only surfaced on the runner.

Adds web/.npmrc pinning the same 1440-minute policy locally, so a
lockfile resolved here is one CI will accept, and re-resolves against
it (1.5.416 and 7.0.6). Setting it in the repo rather than fixing the
two versions by hand is what stops this recurring on the next
dependency change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 12:52:32 +03:00
99e099994d chore(web): stop tracking tsc incremental build state
Some checks failed
deploy / build (push) Failing after 5s
deploy / deploy (push) Has been skipped
deploy / build-web (push) Failing after 57s
deploy / deploy-web (push) Has been skipped
tsconfig.*.tsbuildinfo is a cache tsc regenerates from the sources; it
carries absolute paths from whoever built last and would conflict on
every branch that touches a .ts file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 12:37:35 +03:00
acd47a31c8 feat(web): add the operator UI and the /v1 inventory API it reads
Three routes, per architecture/generic.md §4 (Vite + React + SWC + TS,
static, served by nginx): / renders the repository readme, /models is a
paged and filterable listing of what the bucket holds, and
/{namespace}/{name} shows one repository -- its pinned refs, the files
actually stored, and its model card.

The API it reads is /v1/, deliberately not /api/. That surface is the
Hub's, recorded and replayed verbatim, and adding routes of our own to
it risks a client mistaking one for the real thing. /v1/ answers a
question the Hub has no equivalent for -- what is in this bucket --
which is inventory, not the model search the spec rules out.

The listing shows only what has actually been fetched, never upstream's
siblings. A repository pulled one file at a time shows one file, which
is the honest answer to "what can I get from here offline".

Model cards are third-party content, so their HTML is parsed and then
sanitised against GitHub's allowlist. The plugin order is load-bearing
and commented as such. Verified against a card crafted with <script>,
onerror, a javascript: href, an <iframe>, an SVG-embedded script and an
inline handler: none execute and ordinary markdown still renders. Card
images are not loaded at all -- fetching them would leak the viewer's
address to a third party and make an offline registry's pages depend on
the internet.

Routing: rustingface's URL space is the Hub's, so /Qwen/Qwen3-0.6B is
both a page and the prefix of a file. The vhost splits them the way the
Hub does -- /resolve/ anywhere in the path, plus /api/ and /v1/, go to
the service; everything else is the app. A repo legitimately named
v1/repos is handed back to the resolve path by the router, and there is
a test for it. The /resolve/ test is on the repo type rather than the
path substring, because a repo may contain a directory called resolve
and /api/models/a/b/tree/main/resolve/f must stay a tree request.

Deployment: the frontend ships to hanzalova:/var/www/rustingface, so
that host now gets its own scoped gitea_ci drop-in -- narrower than the
service host's: a webroot rsync, a relabel, nginx -t and a reload. The
health check probes rf.internal from the proxy rather than from the
runner, because a runner is a plain Fedora container with no internal
root CA (verified: fedora:43 gets 000, the proxy gets 200).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
2026-08-31 12:37:26 +03:00