0d204e6837780794302d48f818e47981a7a96003
1324
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d204e6837 |
Merge pull request 'Extension packaging: one source of truth, shadow-mode versioning, real XPI verification' (#235) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
CI / frontend-build (push) Successful in 34s
extension / lint (push) Successful in 40s
CI / backend-lint-and-test (push) Successful in 2m1s
CI / integration (push) Successful in 4m3s
Build images / build-web (push) Successful in 4m21s
Build images / build-ml (push) Successful in 5m39s
Build images / build-agent (push) Successful in 12m49s
|
||
|
|
11dd324f89 |
fix(extension): exclude the test/ and scripts/ directory entries from the XPI
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 3s
extension / lint (push) Successful in 19s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 3m57s
extension / lint (pull_request) Successful in 34s
The XPI-content check added in
|
||
|
|
1c6452e10e |
ci(extension): shadow the derived version + verify real XPI contents
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
extension / lint (push) Failing after 28s
CI / backend-lint-and-test (push) Successful in 47s
CI / integration (push) Successful in 4m1s
Milestone #271 steps 2 and 3. Neither changes what gets published. STEP 2 -- shadow mode. build.yml's sign-extension and ci.yml's extension-version guard now log the version that WOULD be derived from git history alongside the hand-maintained one. Nothing reads the derived value, and neither site can fail because of it. This exists because `web-ext sign` is one-shot per version: AMO 409s on a repeat, so a wrong formula burns a real version number that cannot be reclaimed. Comparing the two across real builds is the only way to validate it at zero cost. sign-extension runs on main only, so main pushes are the sole source of truth for whether the derived number moves exactly when the shipped extension changes -- the dev-side log is a convenience, not the evidence. sign-extension now checks out with fetch-depth: 0. The derived version is a commit count and a depth-1 clone cannot produce one. STEP 3 -- XPI content verification. Every other packaging assertion checks our declaration against itself. This is the first that asks web-ext what it ACTUALLY wrote into the archive. That assumption was both unverified and fragile: `test/**` only survives to web-ext because callers `set -f` before substituting it, so losing that quoting would silently start shipping dev files with no other signal. The step builds the XPI and asserts test/, scripts/, vitest.config.js, package.json, package-lock.json, README.md and node_modules are absent -- and, because an over-matching exclusion would break the extension at runtime rather than at build time, that manifest.json, all four lib/*.js and every UI directory are present. unzip is installed only when missing; node:24-bookworm-slim may not carry it. Refs #2399, #2400 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
597b91d29b |
refactor(extension): one definition of what ships in the XPI
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
extension / lint (push) Successful in 20s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 44s
CI / integration (push) Successful in 3m59s
Milestone #271 step 1. Groundwork for deriving the extension version from git;
no behavior change yet -- nothing consumes `version` so far.
"Which files end up in the XPI" was stated in two places and about to become
three. Three hand-kept copies of one fact is what allowed #2397, where the
publish path could republish a stale XPI because its cache key had no link to
the content it stood for.
New extension/scripts/packaging.sh holds the single declaration and exposes:
ignore web-ext --ignore-files values
pathspec :(exclude)extension/... for git
version <MAJOR.MINOR from manifest>.<commit count over packaged files>
major-minor / patch
Consumers now delegate instead of restating it:
- extension/package.json -- all four web-ext scripts
- .forgejo/workflows/ci.yml -- the extension-version guard's exclusions
- (step 4) the rev-list that derives the version
scripts/** joins the non-packaged set; the script must not ship to users.
Two shell hazards, both load-bearing:
The script runs `set -euf`. Its lists are iterated with deliberate word
splitting, and without -f the shell ALSO globs them -- invoking `pathspec`
from a directory where test/ exists (exactly how ci.yml calls it) would expand
`test/**` into the individual spec files and silently stop covering anything
added later. A caller's own `set -f` cannot prevent this: the script is a
separate sh process and does not inherit it.
Callers additionally need their own `set -f` for the substituted RESULT, which
is a different expansion. version.spec.js asserts every --ignore-files caller
sets it, that the pathspec comes through with `test/**` literal and no
.spec.js paths, and that neither consumer has reinstated a hardcoded list --
the easy future regression is "simplifying" by inlining one again.
Verified: all five subcommands plus the usage/exit-2 path. Derived version on
main (
|
||
|
|
8300029741 |
Merge pull request 'Extension: fix credential-push 405, guard the publish path, add a unit suite' (#234) from dev into main
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 32s
extension / lint (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 1m14s
Build images / sign-extension (push) Successful in 2m23s
Build images / build-ml (push) Successful in 3m53s
CI / integration (push) Successful in 3m56s
Build images / build-web (push) Successful in 2m34s
Build images / build-agent (push) Successful in 13m33s
|
||
|
|
f9111c06a7 |
test(extension): unit suite for lib/ + version-consistency specs
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 21s
extension / lint (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 43s
CI / integration (push) Successful in 3m53s
extension / lint (pull_request) Successful in 19s
extension/ had no test harness at all -- web-ext lint was the only signal, so
the URL-normalization fix in
|
||
|
|
c37a180c3c |
ci: guard the extension publish path against a missed version bump
build.yml's sign-extension keys its AMO-signing cache purely on the version string in extension/package.json. If an ext-<version> release already has an XPI, signing is skipped and build-web bakes that OLD signed XPI into :latest. Nothing in that path inspects whether extension/ actually changed, so a forgotten bump ships a stale extension on a fully green build -- silently, and as the default outcome of forgetting. AMO can't backstop it either: it 409s on re-signing a version, which is precisely why the cache exists. New extension-version job, pure git + text, no deps or services: 1. Unconditional consistency check. manifest.json and package.json versions must match. web-ext sign reads manifest.json (package.json is in --ignore-files and isn't even inside the XPI), so AMO signs the manifest version; build.yml keys its cache, release tag, XPI filename -- and so the version /api/extension/manifest reports to the update prompt -- on package.json. Divergence either 409s at AMO or ships an XPI whose update prompt lies about what's installed. 2. Changed-without-bump check. If any PACKAGED file under extension/ differs, the version must have moved. Exclusions mirror --ignore-files so a Renovate web-ext devDep bump in package.json doesn't falsely demand one. Compared against main rather than the previous push: the publish decision is made at merge-to-main against whatever ext-<version> exists, so "differs from main" is the question that matters. Diffing against the previous dev push would demand a fresh bump on every iteration, inflating the version to buy nothing. Bumping stays manual -- making it automatic requires rewriting the version in CI and committing back to a protected branch, which this workflow deliberately avoided. This only ensures a missed bump can no longer be silent. Refs #2393 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8214afee1e |
fix(extension): normalize FC URL so credential push doesn't 405
The stored apiUrl was required to already carry the `/api` suffix, since
api.js builds requests as `${baseUrl}/credentials`. The options label read
"FC base URL", so entering the instance root -- the natural reading --
sent every request one path segment short: POST /credentials hit the Vue
SPA catch-all and came back 405, and GET /extension/manifest 404'd.
Worse, Test Connection reported success on it: the catch-all answers GET
/credentials with 200 HTML, so `r.ok` was true and the only affordance
meant to catch this misconfiguration actively masked it.
Normalize instead of validate (rules 92, 26):
- New lib/url.js: normalizeApiUrl / webRootFromApiUrl, one source shared
by the background client and the options page. Accepts either the
instance root or the API root.
- api.js normalizes on read, so configs already stored in the broken form
heal themselves without the operator reopening Settings.
- options.js stores the canonical form, echoes back what it saved, and
the test now asserts a JSON content-type -- killing the false green.
- 404/405 in request() now names the URL and points at the setting.
- Options label/placeholder state that both forms work.
Version 1.0.9 -> 1.0.10 in BOTH manifest.json and package.json; build.yml
resolves the release version from package.json, and a stale value there
would hit the cached ext-1.0.9 asset and republish the old XPI unsigned
against the new code.
Refs #2393
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
306de50f61 |
docs: Fabled-Git, not Forgejo, in ci-requirements
The instance has run Gitea since the migration. Also fixes a dead rulebook pointer: the topic was renamed forgejo.md -> fabled-git.md, so the "CI philosophy" reference pointed at a file that no longer exists. Prose only — no workflow or path change. Scribe issue #2272. |
||
|
|
b5b437ca80 |
Merge pull request 'feat(agent): idle-unload GPU models to free VRAM when the queue is idle' (#233) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 34s
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 25s
Build images / build-web (push) Successful in 3m20s
Build images / build-ml (push) Successful in 4m29s
CI / integration (push) Successful in 4m26s
Build images / build-agent (push) Successful in 11m55s
|
||
|
|
57e52433d0 |
feat(agent): idle-unload GPU models to free VRAM when the queue is idle
The SigLIP embedder + YOLO proposers load lazily then stay resident for the container's whole lifetime — a 24/7 agent with an empty queue squats on ~5GB of VRAM doing nothing (operator-observed: 4900MiB held at GPU-util 8% / P8). Sleep mode only sheds downloaders + poll cadence; even a UI Stop left the models loaded. Add a monitor thread that unloads the torch-owned models after cfg.idle_unload_seconds (env IDLE_UNLOAD_SECONDS, default 300; 0 disables) with the GPU genuinely idle (active==0, buffer drained, no job completed in the window), then torch.cuda.empty_cache() to hand the blocks back to the driver. They reload lazily on the next job via the existing _ensure_embedder / _proposers_for. Covers both sleep-mode idle and a full Stop. Surfaced in /status (models_loaded) and the agent UI pipe line; the VRAM meter drops too. Residual: imgutils CCIP/person ONNX sessions + the CUDA context stay resident (no clean unload API) — idle VRAM drops substantially, not to zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbrA36zNczjVhrM6cWThQa |
||
|
|
ce0dac3524 |
Merge pull request 'DRY + stability pass — extension, ML/settings backend, frontend cards (#161)' (#232) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 12s
extension / lint (push) Successful in 11s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 50s
CI / integration (push) Successful in 3m47s
|
||
|
|
ec66ea5f83 |
refactor(ui): settings-card primitives + fix threshold clamp / card misgroup (#161)
Tier-3 frontend DRY for the ML settings cards, plus the F-D2 clamp bug and the F-D3 card misgrouping. New primitives (components/common + composables): - <SettingToggleRow> — the accent-icon + .fc-section-h label + right-aligned switch row (HeadsCard x3, CropProposersCard). iconColor prop absorbs the on/off dim. - <SettingNumberField> — compact numeric field that CLAMPS to [min,max] on commit. This fixes F-D2: HeadsCard/CropProposersCard previously sent Number(raw) straight to the API, so an out-of-range threshold bounced off the 400 validator (only TranslationCard clamped). density prop for the grid cards. - useSettingSave(patchFn) — the busy + patch + toast + revert-on-failure flow each card hand-rolled (HeadsCard x6 handlers, CropProposersCard, MLBackfillCard, VideoEmbeddingCard). Returns ok/false for the optimistic-switch revert. Adopted in HeadsCard, CropProposersCard, MLBackfillCard (handler only — its plain labelled switch is a different affordance), VideoEmbeddingCard. F-D3: MLThresholdSliders.vue actually rendered a "Video embedding" (frame- sampling) card but sat under "Tagging → Suggestion thresholds". Renamed it VideoEmbeddingCard.vue and moved it to the "GPU agent & embeddings" section. Left deliberately (over-DRY guard): TranslationCard uses an inline error ALERT (not a toast), already clamps its confidence with a NaN fallback, and lives on the ImportStore — a genuinely different save pattern, so forcing it onto useSettingSave would change its UX. Behaviour-preserving refactor; CI has no Vue type-check so this needs a live UI pass (toggles persist + revert on failure, thresholds clamp on blur, video card now under Embeddings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
e92570a31e |
refactor(extension): DRY the web-root transform + cookie-export flow (#161)
Behavior-preserving (extension JS has lint-only CI + your manual test): - api.webRoot() single-sources the baseUrl→web-root transform (strip trailing slash + /api) that was copy-pasted in background.js's self-update check and OPEN_ARTIST_PAGE, whose comments even cross-referenced each other. - exportPlatformCookies(key) shares the extract→verify→upload spine between EXPORT_COOKIES (single) and EXPORT_ALL_COOKIES; it returns a structured outcome so each caller keeps its own response/skip messages verbatim. - popup.js mutedNote(text) replaces the "centered muted note" div hand-rolled in the platform-loading, sources-loading, and empty-sources renderers. No version bump — no behavior change, so it rides the next real ext release rather than forcing an AMO re-sign + reinstall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
a2d1ed935d |
refactor(ui): DRY the settings-card CSS tokens + fix unstyled headers (#161)
- Promote .fc-section-h to a global token (app.css). It was copied identically into 4 cards, and TranslationCard used the class with NO local def — so its section headers rendered unstyled. Now fixed everywhere. - Promote .fc-good / .fc-weak status colours to globals; delete the local copies in the GPU/heads cards. (.fc-ok stays local — divergent: on-surface in HeadsCard vs success in QueuesTable. .fc-bad stays — different name.) - Delete 10 identical local .fc-muted redefinitions that crept back after the 2026-06-09 sweep; the global utility already covers them. - DbMaintenanceCard: opacity:0.6 muted text → the .fc-muted token (the exact anti-pattern that token's comment forbids). - HeadsCard: collapse byte-identical ratePct() into pct(). CSS-only + one template class swap; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
05df51b749 |
fix(ml): drop unnecessary quotes on MLSettings.load annotations (UP037)
Python 3.14 evaluates annotations lazily, so the self-referential return type needs no forward-ref quotes — matches ImportSettings.load. Fixes the ruff lint lane on the DRY-pass push. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
099e1e664c |
refactor(patreon): DRY the campaigns-API request (#161)
_lookup_via_api and resolve_display_name shared ~90% of their body (same endpoint, params, headers, error handling — differing only in which field they pluck from data[0]). Extract _campaigns_api_first(vanity, cookies_path) -> dict|None; callers pluck the campaign id vs the display name. Return-value behavior preserved (the display-name path additionally gains the helper's more granular warning logs). Covered by the existing test_patreon_resolver.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
c87f8a1bb3 |
refactor(maintenance): DRY the recover_stalled head-run twins (#161)
recover_stalled_head_training_runs and recover_stalled_head_auto_apply_runs were near-exact copies (coalesce-flip + keep-last-N prune, differing only in model + two constants). Extract _recover_stalled_runs(model, stall_minutes, keep_runs, label); the two tasks become thin wrappers. The other two recover tasks are deliberately NOT folded in (library-audit has no prune tail; backup uses a single started_at cutoff). test_recover_stalled_head_runs.py covers both wrappers (stalled→error, fresh survives) — previously untested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
666b3a2ec8 |
refactor(ml): DRY pass — shared sweep helpers + table-driven settings (#161)
Consolidate duplication accrued across the ML tagging + settings backend, behavior-preserving (over-DRY guard applied — the three auto-apply sweep BODIES stay separate; only their shared inner helpers are extracted). - _sigmoid / _conflict_scores / _insert_presentation_review (heads.py): the score→prob transform (6 inlined sites), the presentation conflict signal (2 sites), and the ring-loud PresentationReview insert (2 sites, single- sourced so the mode column can't drift on the shared composite PK). - _applied_or_rejected (training_data.py): the per-tag "applied ∪ rejected" skip-set, byte-identical at 3 sweep sites (heads.py x2, tasks/ml.py ccip). - ccip sweep divergence fixes: import ccip._FIGURE_KINDS + training_data._l2norm instead of local copies that silently drift when the canonical changes. - MLSettings.load / .load_sync classmethods (mirror ImportSettings); route all 8 scalar_one singleton reads through them (the session.get None-path stays). - GET serializers for MLSettings + ImportSettings are now table-driven off the same _EDITABLE tuples PATCH writes, so a new field can't be silently absent from GET (the split that historically dropped fields). - AUTO_APPLY_THRESHOLD_MIN/MAX constant single-sources the [0.5,0.999] operating range across the service clamp + the 5 API validators. - test_ml_dry_helpers.py pins _applied_or_rejected + _sigmoid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi |
||
|
|
9b5ec86222 |
Merge pull request 'feat(extension): in-app update prompt + v1.0.9 (#1489)' (#231) from dev into main
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 30s
Build images / sign-extension (push) Successful in 3m34s
Build images / build-web (push) Successful in 8s
CI / integration (push) Successful in 3m50s
|
||
|
|
d80a5255ed |
feat(extension): in-app update prompt — popup banner + toolbar badge (#1489)
CI / lint (push) Successful in 3s
extension / lint (push) Successful in 10s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 39s
CI / integration (push) Successful in 3m51s
extension / lint (pull_request) Successful in 10s
The extension is installed per-instance from the operator's FC host, so Firefox's
static update_url can't apply (each instance has a different host) and updates
were fully manual. Add a self-hosted-friendly update surface that reuses the
existing public GET /api/extension/manifest ({version, latest_url, sha256}):
- lib/api.js: getExtensionManifest().
- background.js: checkForUpdateInfo() compares the instance's latest published
version against runtime.getManifest().version (dotted-numeric compare so
1.0.10 > 1.0.9); CHECK_UPDATE message handler; refreshUpdateBadge() sets a
toolbar badge via browser.action; a daily browser.alarms check plus on
startup/installed. New 'alarms' permission (non-prompting).
- popup: an 'Update available — vX' banner with an Update button that opens the
signed XPI (web root, /api stripped like OPEN_ARTIST_PAGE) → Firefox's native
install prompt. Never blocks the popup on a failed check.
No backend changes (endpoint already exists). Bump 1.0.8→1.0.9 so this ships;
from here on updates surface themselves instead of needing a manual reinstall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
89c83ee5de |
Merge pull request 'fix(extension): Add-to-FC button on subscribed Patreon creators + v1.0.8 (#1485)' (#230) from dev into main
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m46s
Build images / sign-extension (push) Successful in 5m8s
Build images / build-web (push) Successful in 10s
|
||
|
|
69b5637bd6 |
fix(extension): show Add-to-FC button on subscribed Patreon creators (#1485)
CI / lint (push) Successful in 3s
extension / lint (push) Successful in 11s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m47s
extension / lint (pull_request) Successful in 9s
Symptom (operator-flagged): the extension injected the Add-as-source button on a Patreon creator you had NOT subscribed to, but it disappeared once you were subscribed — the opposite of when it's useful. Root cause (extension logic, not Patreon security): Patreon serves a creator under three URL shapes — bare patreon.com/Atole, patreon.com/c/Atole, and patreon.com/cw/Atole (the 'creator workspace' URL you land on once subscribed; documented in patreon_resolver._VANITY_RE). The button's artist-page gate (PLATFORM_ARTIST_PATTERNS.patreon in platforms.js) and its byte-mirror probe pattern (_PLATFORM_PATTERNS in extension_service._derive) only matched the bare single-segment form and explicitly excluded c/. So the subscribed-view URL failed the gate → no button. The ingestion resolver already handled all three; only these two gates were too narrow. Fix: both regexes now accept optional cw/ and c/ prefixes and drop the strict single-segment end-anchor, so a creator's inner page (/cw/Atole/posts, /Atole/membership) also matches — robust to whatever exact shape the subscribed view uses. Nav-page exclusions (home/search/messages/notifications/library/ settings/posts + post permalinks) preserved. New unit test covers all three prefixes, sub-paths, and nav-page rejection (both regexes validated identically). Bump extension 1.0.7→1.0.8 so a fresh signed XPI ships the fix (also exercises batch-5 web-ext-10's AMO sign path end-to-end on the main build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d3192f1843 |
Merge pull request 'chore(deps): web-ext 8→10 (Renovate batch 5) (#1450)' (#229) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
extension / lint (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m48s
|
||
|
|
51749e05db |
chore(deps): update web-ext 8→10 (batch 5) (#1450)
CI / lint (push) Successful in 2s
extension / lint (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m58s
extension / lint (pull_request) Successful in 9s
Renovate dep-dashboard batch 5. web-ext is the extension's build/lint/sign CLI (devDependency only; no extension source changes). Verified against the 8→10 changelog + FC's actual usage: - All CLI flags we use survive unchanged: --source-dir, --no-config-discovery, --ignore-files, --overwrite-dest (build), --channel/--api-key/--api-secret (sign). No removed/renamed flags for lint/build/sign. - v9's one breaking change (.js config files rejected) does NOT apply: we pass --no-config-discovery on every command and ship no config file. - Node: web-ext 10 baselines Node 22. The lint job runs on node:24-bookworm-slim; the load-bearing AMO sign job runs on ci-python:3.14 which installs Node 24 (CI-runner NODE_MAJOR=24) — both satisfy it. Sign is cache-skipped this push (extension version unchanged at 1.0.7) but is verified compatible for the next version bump. - The bundled addons-linter jumps to 10.1.0 — the extension.yml lint job (web-ext lint over the MV3 manifest) is the CI verifier for any new manifest findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5a5694f200 |
Merge pull request 'fix(ui): softer chrome-gradient falloff + segmented media toggle (#1478)' (#228) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m42s
|
||
|
|
50d6c42207 |
fix(ui): softer chrome-gradient falloff + segmented media toggle (#1478)
Two operator-flagged polish items from the Vuetify-4 review:
Gradient: reshape the nav + sub-header fade from a near-linear ramp to a
hold-then-soft-drop profile — hold high opacity (0.92 → seam 0.68) through the
bulk of the chrome, then ease to transparent over a small section at the bottom
with an intermediate stop, so it tails off softly instead of running a straight
line into a hard edge. Raising the shared --fc-chrome-seam also makes the tab
strips more legible over scrolling content.
Media toggle: FC's global VBtn { rounded: 'pill' } default made Vuetify 4
pill-round each SEGMENT of the All/Images/Videos v-btn-toggle individually, so
the rounded ends collided at the joins. Square the inner segments and clip the
group to one 8px outline — a proper segmented control.
Both are colour/border-radius only — no control height changes, so the filter
bar height and the nav offset (--fc-nav-h) are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bb1a938cc0 |
Merge pull request 'fix(ui): measure real nav height (--fc-nav-h) — Explore fits on 1080p, no breadcrumb tuck (#1481)' (#227) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m50s
|
||
|
|
67c7ca8603 |
fix(ui): measure real nav height (--fc-nav-h), stop Explore breadcrumb tucking under nav (#1481)
The app uses a plain sticky TopNav (no v-main), and the nav's height was hardcoded as 64px in ~6 places: the Explore + Subscriptions full-height workspaces (height: calc(100vh - 64px)) and every sticky sub-header pinned beneath the nav (top: 64px — Gallery filter bar, Browse/Series/Settings tabs). Vuetify 4's MD3 sizing changed the real nav height, so 64px was wrong: the Explore workspace was sized taller than the space below the nav, overflowed the viewport, and its breadcrumb tucked under the (taller) nav on 1080p. TopNav now measures its own height via ResizeObserver and publishes it as --fc-nav-h on documentElement (default 64px in app.css). Every consumer uses var(--fc-nav-h) instead of the magic number, so the layout self-corrects to the nav's real height and stays correct as it reflows (per-view teleported actions, mobile breakpoint). Also tightens the new chrome-gradient seam — sub-headers now pin at the nav's exact bottom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fc0293029d |
Merge pull request 'fix(ui): Vuetify-4 review polish — active-tab indicator + one continuous chrome gradient' (#226) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m44s
|
||
|
|
eed42a260a |
fix(ui): one continuous chrome gradient across nav + sticky sub-headers (#1478)
The TopNav and each sticky sub-header pinned beneath it (Gallery's filter bar, the Browse/Series/Settings/Subscriptions tabs bars) each painted their OWN dark-to-transparent gradient (Gallery) or a solid surface band (the rest), so the fade read as happening twice — dark, fade out, then dark again — instead of one gradient flowing from the nav down through the sub-nav. Operator asked to treat the sub-nav as part of the nav with a single gradient. New shared .fc-chrome-continues primitive (app.css): the nav fades from opaque to a shared --fc-chrome-seam alpha (on views flagged meta.stickyChrome), and the sub-header continues from that exact seam alpha to transparent over its own height. Both reference the same var so the alphas meet at the 64px boundary — no re-darkening, no doubling. Percentage stops keep it spanning the filter bar's expanding refine panel; the primitive's blur keeps tabs/controls legible where the old solid bars had none. --fc-chrome-seam is the single tuning knob. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
61b14e8f65 |
fix(ui): cleaner active-tab indicator (drop odd v4 slider) (#1480)
Vuetify 4's MD3 v-tab slider underline rendered wider than the tab and floated below it (operator-flagged in the v4 review). The active tab's text is already accent-coloured, so drop the slider and mark the active tab with a subtle accent fill + rounded top — a clean highlight, app-wide across all tabbed views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7d1c701b67 |
Merge pull request 'fix(ui): remove redundant app-shell top gap (#1480)' (#225) from dev into main
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m48s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 7s
|
||
|
|
447bf73519 |
fix(ui): remove redundant app-shell top gap (sticky nav double-offset)
Every view showed a large empty band at the top (operator-flagged during the Vuetify-4 review; pre-existing). Cause: .fc-content had padding-top:64px to clear the navbar, but TopNav is position:sticky and already reserves its own space in the v-app flex column — the 64px was a fixed-navbar leftover that double-counted the offset. Removed it; content now flows directly below the sticky nav (and the full-height calc(100vh-64px) views no longer overflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b638382cd5 |
Merge pull request 'Ship: UI framework batch 4 — Vuetify 4 + vue-router 5 + pinia 3 (#1449)' (#224) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 28s
CI / frontend-build (push) Successful in 34s
CI / backend-lint-and-test (push) Successful in 53s
CI / integration (push) Successful in 3m46s
|
||
|
|
6104452d2e |
feat(deps): vuetify 3→4 (batch 4 phase B, #1449)
Vuetify 4 is a Material-Design-3 styling refresh with revert snippets, not a component-API overhaul. FC's exposure was small: - Bump vuetify ^4, vite-plugin-vuetify ^2.1.0, vue ^3.5, engines node>=24. - Restore the dropped global CSS reset (minimal reset from the upgrade guide) in Vuetify's own low-precedence reset layer, so FC's margin-zeroing assumptions hold. - v-row prop→utility: 'dense' → density=compact (×3), align=center → class=align-center. - v-snackbar: multi-line removed → min-height=68. - v-autocomplete #item slot: item→internalItem (item now aliases raw) in TagPicker + GalleryFilterBar (item.raw.* → internalItem.raw.*). ACCEPTED (cosmetic, operator reviews live per plan #158): MD3 typography (text-body-2 ×73), non-uppercase buttons (v4 dropped the uppercase default), MD3 elevation. CI verifies BUILD only — the LOOK is the live-review pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b59828635e |
chore(deps): pinia 2→3 + vue-router 4→5 (batch 4 phase A, #1449)
Both are Vue-3-compatible majors with no code impact for FC: - vue-router 5: no breaking changes when not using file-based routing (FC uses a plain createRouter in router.js). - pinia 3: drops Vue 2 + deprecated APIs; FC uses string-first setup-syntax defineStore + no custom pinia plugins, so nothing to change. Phase A of the Vuetify-4 UI framework migration (milestone #158); Vuetify 4 lands in phase B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
17903068b4 |
Merge pull request 'Ship: system-tag refactor + soft WIP tier + Explore reach (#1464, #1474, #1476)' (#223) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m45s
|
||
|
|
fac5ae6ce5 |
feat(explore): reach dial to escape dense clusters + anti-revisit (#1476)
The Explore walk got stuck in dense signatures — neighbours all too similar, so forward-arrow couldn't escape and Random was the only exit. Root cause: MMR only diversifies WITHIN the nearest ~400 pool; in a dense cluster that whole pool is near-identical, so there's no escape route in it. - gallery_service.similar(reach=0.0, exclude_ids=None): reach>0 widens the pool (cap 400→1000) and _reach_sample strides across an outward-growing distance span so the set handed to MMR spans near→mid-far (guaranteed escape routes), not just the tight cluster. exclude_ids drops already-walked images. Gallery 'more like this' (reach=0) is unchanged. - api/gallery similar: parse reach + exclude_ids. - explore store: default reach 0.4 (auto-diversifies without touching the dial), pass the breadcrumb as exclude_ids, setReach action. - ExploreView: a Near↔Far reach slider in the trail. - tests: _reach_sample math (deeper ranks with higher reach, near kept); similar exclude_ids drops walked + reach path runs clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af0d39ed52 |
feat(wip): soft title tier — sketch/doodle vocab + ring-loud audit (#1474)
Extends WIP title-tagging to lower-precision cues (sketch/doodle/scribble) safely. - wip_title.py: soft matcher (word-anchored; sketchbook/kadoodle don't trip it); WIP_TITLE_SOFT_SOURCE + soft SQL prefilter; apply_wip_image_tags takes a source arg. - training_data._AUTO_SOURCES += 'wip_title_soft' → the soft tier is PROVISIONAL and never trains the wip head (a finished "sketch" can't pollute it). Only the hard tier (wip_title) + manual train. - ImportSettings.wip_soft_title_tagging_enabled (OFF by default, opt-in). Migration 0087. - importer: hard tier wins, soft is the fallback (source wip_title_soft). - backfill: refactored into a shared _backfill_wip_tier; hard always, soft when enabled. - heads.soft_wip_conflict_audit + daily beat: score soft-tagged images against content heads, flag ring-loud ones (PresentationReview mode=process) for the review strip — the operator's "measure if they got falsely tagged" safety. - api settings toggle; ImportFiltersForm soft toggle. - tests: soft matcher pos/neg; soft source not a training positive; audit flags ring-loud + spares quiet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d9a14e890d |
feat(system-tags): process auto-tag settings UI + mode-aware review strip (#1464)
Frontend for the system-tag refactor (milestone #157 step 6). - HeadsCard: new 'Auto-tag work-in-progress' section (enable + tag-confidence + conflict knobs) for wip/editor process auto-apply, mirroring the chrome card; copy notes they stay VISIBLE and the head only learns from titles/manual (no runaway). Presentation copy narrowed to banner-only. - HiddenReviewStrip: mode-aware — chrome flags read 'hidden as X / Keep hidden / Un-hide'; process flags read 'auto-tagged X / Keep tag / Remove tag'. Same endpoints (the backend returns mode), different words. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ad2a5fc5fe |
feat(system-tags): process vs chrome groups + WIP provisional auto-apply (#1464)
Backend for the system-tag behavior refactor (milestone #157). editor screenshot moves from chrome (hidden) to the PROCESS group (shown, like wip); wip+editor gain provisional auto-apply so they stop needing endless manual identification — without a runaway loop. - tag.py: split PRESENTATION_SYSTEM_TAGS → CHROME_SYSTEM_TAGS (banner) + PROCESS_SYSTEM_TAGS (wip, editor screenshot). - heads.py: generalize presentation_auto_apply_sweep → system_tag_auto_apply_sweep (mode chrome|process). Same Guard 1 (skip human/confirmed) + Guard 2 (ring-loud conflict → PresentationReview). process mode uses source 'process_auto' and does NOT hide (hide is a gallery-query effect of group membership). - training_data._AUTO_SOURCES += 'process_auto' → the head never trains on its own auto-applied output; only wip_title/manual train it (the runaway break). - ml_settings: process_auto_apply_enabled (OFF, opt-in) + threshold + conflict threshold. presentation_review.mode ('chrome'|'process'). Migration 0086. - gallery_service: default-hide reads CHROME only (editor now shows); Explore neighbors exclude the whole PROCESS group. - tasks/ml + celery beat: scheduled_process_auto_apply (daily, opt-in); prune covers both modes. - api: ml_admin process_* CRUD+validation; hidden-review returns mode. - tests: rename chrome sweep calls; new test_process_auto_apply (apply, guards, mode flag, no-self-train); gallery test asserts editor now visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
31d400ab0a |
Merge pull request 'Ship: title-based WIP auto-tagging (#1458)' (#222) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m50s
|
||
|
|
0da0e47784 |
fix(wip-title): count inserts via pre-SELECT, not driver rowcount
apply_wip_image_tags relied on result.rowcount, but psycopg reports -1 for a multi-row INSERT ... ON CONFLICT DO NOTHING (executemany path), so the return count (and the backfill's reported total) was wrong. Compute the count from a pre-SELECT of already-tagged ids within the same transaction; keep ON CONFLICT DO NOTHING as a race-safety belt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
503c8854bc |
style(importer): sort wip_title import into the local-import block
Ruff I001 fixup for
|
||
|
|
571938781a |
feat(tagging): title-based WIP auto-tagging (#1458)
Auto-apply the `wip` system tag to posts whose TITLE explicitly declares
work-in-progress ("WIP" / "work in progress") — a deterministic, high-precision
complement to the image-based ML `wip` head. WIP images are excluded from the
Explore/gallery browse, so honouring the artist's own label keeps unfinished
pieces out of the main browse.
- services/wip_title.py: precision-first token-anchored matcher (swipe/wiped
never trip it) + sync apply helpers (source='wip_title', ON CONFLICT DO
NOTHING, chunked under the psycopg param ceiling).
- importer: live hook on FRESH import only (never on deep-scan/supersede), so a
manually-removed WIP tag is never re-applied by a routine re-scan.
- maintenance.backfill_wip_title_tags: operator-triggered back-catalogue sweep
(coarse SQL prefilter + regex confirm, keyset-paginated). Deliberately NOT a
beat — a periodic re-run would silently undo manual removals.
- ImportSettings.wip_title_tagging_enabled (default ON, migration 0085) gating
the live hook; GET/PATCH + POST /settings/wip-title/scan.
- Settings UI: toggle + "Scan existing posts" button.
- Tests: pure matcher unit tests + integration (apply idempotency, backfill
precision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d45ce5e426 |
Merge pull request 'Ship: Renovate onboarding + dependency modernization batches 1–3' (#221) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 8s
extension / lint (push) Successful in 16s
CI / frontend-build (push) Successful in 29s
CI / backend-lint-and-test (push) Successful in 1m4s
Build images / build-web (push) Successful in 1m24s
Build images / build-ml (push) Successful in 2m36s
CI / integration (push) Successful in 3m46s
|
||
|
|
0cf3a02797 | Merge pull request 'chore(deps): update node.js to v24' (#220) from renovate/node-24.x into dev | ||
|
|
6ab495292a |
chore(deps): update node.js to v24
renovate/stability-days Updates have met minimum release age requirement
CI / lint (pull_request) Successful in 2s
CI / frontend-build (pull_request) Successful in 16s
CI / backend-lint-and-test (pull_request) Successful in 28s
CI / integration (pull_request) Successful in 3m49s
|
||
|
|
c98db303d0 |
chore(frontend): drop dead vue-tsc devDep + check script
Frontend is pure JS (no .ts/JSDoc); CI never ran vue-tsc. Removed the devDep and its orphaned `check` script instead of bumping to v3, and updated the ci.yml comment to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |