mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
5133aad76daca5d1494bc18bf3adacf7f3727ee2
474 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
57744aeb14 |
feat(cursor): proactively renews Cursor sessions and fixes manual refresh (#9173)
* refactor(cursor): extracts token extraction into shared lib
Moves tryIdeAuth/tryAgentAuth and supporting helpers out of the
auto-import route into src/lib/cursor/tokenExtractor.ts, and adds
an agent-cli-state.json fallback candidate path to tryAgentAuth
(alongside the existing auth.json candidate) so the extraction
logic can be reused by the upcoming renewal orchestrator.
* feat(cursor): adds cursor-agent-backed token renewal orchestrator
Builds the renewal orchestrator in src/lib/cursor/renewal.ts: a
bounded, unattended-safe --list-models nudge, a side-effect-free
status availability check, an in-flight spawn lock keyed by
command, and renewCursorConnection() which nudges cursor-agent
then independently re-scrapes the IDE and cursor-agent credential
sources to detect whichever refreshed. Extends cursorAgent.ts's
binary resolution and spawn helper with fixed-paths-only mode and
a SIGKILL follow-up for background use. Adds a generic keyed-mutex
utility (src/shared/utils/keyedMutex.ts) for serializing a
connection's renew-then-persist cycle, and forwards a busy-timeout
through driverFactory's node:sqlite fallback path.
* feat(cursor): proactively renews Cursor sessions in the sweep
Adds src/lib/tokenHealthCheckCursor.ts, sweep-side glue that calls
the renewal orchestrator and persists the result, wired into
tokenHealthCheck.ts's checkConnection() via a new Cursor-specific
branch placed ahead of the generic no-refresh-token fallthrough.
Carves out a non-terminal exception for a Cursor connection that
already landed at testStatus "expired" via the request-time 401
path, excluding permanently-dead account_deactivated connections.
Extends buildRefreshFailureUpdate() with an overrides param so
Cursor's failure path can use a distinct, non-terminal errorCode
instead of the generic refresh_failed/expired taxonomy.
* feat(cursor): adds local-only manual refresh route
Adds POST /api/providers/[id]/refresh-cursor, a dedicated
loopback-only route that calls the renewal orchestrator on demand
for a single Cursor connection, bounded by a 30s per-connection
cooldown. Classifies the new route in LOCAL_ONLY_API_PATTERNS and
closes the manage-scope-bypass gap for dynamic-segment spawn-capable
routes under /api/providers/ via a new SPAWN_CAPABLE_PATTERNS /
SPAWN_CAPABLE_PATTERN_ANCESTORS mechanism, which also retroactively
covers the pre-existing /login route. The existing shared
/api/providers/[id]/refresh route is untouched and stays
remote-reachable for every other provider.
* feat(cursor): surfaces a dismissible cursor-agent nudge
Adds GET /api/providers/cursor/agent-availability, a credential-free
LOCAL_ONLY route returning only { cursorAgentAvailable: boolean },
backed by a 5-minute cached wrapper around the renewal orchestrator's
existing availability check. Surfaces a dismissible dashboard banner
on the Cursor provider page suggesting cursor-agent installation
when it isn't detected, following the existing dismissible-banner
convention. Also fixes a pre-existing bracket character in a
routeGuard.ts comment that was silently truncating
check-openapi-security-tiers.mjs's view of LOCAL_ONLY_API_PREFIXES.
* fix(cursor): wires manual refresh button to the new route
Branches handleRefreshToken to call the dedicated Cursor refresh
route instead of the generic /refresh route, which silently 502s
for Cursor connections today since they carry no refresh token.
Every other provider's refresh behavior is unaffected. Adds the
cursorSessionUnchanged i18n key and syncs it (plus a pre-existing,
unrelated 28-key backlog) across all 42 locale files.
* fix(cursor): addresses Phase 4/4.5 review findings
Restores the legacy stdout/stderr auth-pattern fallback in
checkCursorAgentAvailability() that the plan's Task 2 Step 4
required but the implementation had dropped. Threads an optional
deps parameter through checkCursorConnectionIfNeeded() so its
error branch is reachable in tests, and switches both it and the
manual-refresh route to exhaustive switch statements over the
renewal result. Adds a short-lived host-keyed dedup cache around
tryIdeAuth() so multiple due Cursor connections sharing a host
don't each open the same state.vscdb file in one sweep tick.
Adds opportunistic eviction to the manual-refresh cooldown map,
an outer try/catch to the availability route for defense-in-depth
consistency with the plan's other routes, and corrects a stale
JSDoc claim about the /login route's auth check. Documents the
now-empirically-confirmed agent-cli-state.json schema mismatch
found while validating against a real cursor-agent install.
* docs(cursor): adds changelog fragments for the renewal plan
Adds one fragment per user-facing outcome per changelog.d/README.md's
convention for a PR that both fixes and adds. PR number placeholder
to be filled in once the PR is opened.
* fix(i18n): translates the new Cursor keys into Vietnamese
The i18n:sync-ui run in an earlier commit left __MISSING__
sentinels for the 4 new Cursor keys in every locale, but
Vietnamese has a dedicated completeness test requiring zero
internal missing markers. Provides real translations for
cursorSessionUnchanged, cursorAgentNudgeTitle,
cursorAgentNudgeBody, and cursorAgentNudgeDismiss.
* fix(cursor): addresses quality-gate Layer 1.5 findings
Restores a comment that misrepresented execFile's actual argv shape
after an earlier bracket-removal fix, this time avoiding literal
closing-bracket characters entirely so the openapi checker's naive
array parser can't be broken by either version. Bounds the sweep-
and manual-route-triggered tryIdeAuth() busy-timeout to 250ms
(down from the interactive auto-import path's 2000ms), since both
share the main event loop with all other in-flight requests and
should fail fast on a WAL-lock collision rather than block the
whole instance for up to ~4s. Has the manual refresh route bypass
the sweep's IDE-auth dedup cache so a click always sees a fresh
read, consistent with this plan's existing "manual actions never
see stale cached data" convention. Documents the previously-missing
agent-availability route in ROUTE_GUARD_TIERS.md's spawn-capable
table.
* fix(cursor): adds SIGKILL follow-up to the status-check spawn
Matches the nudge spawn's existing SIGTERM+SIGKILL pattern so an
unresponsive cursor-agent status check can't leak a lingering
process if it ignores SIGTERM.
* docs(cursor): fills in the PR number for changelog fragments
Renames the 3 changelog.d fragments to their PR-numbered filenames and replaces the (#PR) placeholder with #9173, now that the PR exists.
* fix(cursor): corrects changelog fragments to reference PR #9173
The prior commit only staged the git mv rename — a git add invocation with a stale (pre-rename) pathspec aborted before the actual (#PR) -> (#9173) content edit was staged, so the rename landed without the fix it was meant to carry. This captures the actual content change.
* docs(cursor): regenerates the agent-skills catalog for the new route
check:agent-skills-sync (CI's Merge integrity gate) requires SKILL.md files to stay in sync with the live route catalog. Adding /api/providers/cursor/agent-availability in an earlier commit needed a regen this branch never ran.
* chore(quality): rebaselines file-size caps grown by agentrouter merges
Two already-merged agentrouter commits (
|
||
|
|
2e799b33a7 |
fix: skills & memory — tool-name encoding, schema normalization, warm-cache, combo id, Ponytail catalog (#9058)
* feat(skills): add Ponytail minimalism skill as external catalog entry
- Add 'external' SkillCategory + SkillArea
- Register ponytail (MIT, DietrichGebert/ponytail) in CURATED_SKILLS
- Generator: external skills carry content in custom block, no api/cli body
- Generate skills/ponytail/SKILL.md with original content preserved
- Update catalog test counts 45 -> 46
* fix(skills+memory): builtin handler fallback in executor, skip vector upsert for deleted memories
- skills: Next.js compiles SkillExecutor into multiple chunks (own singleton
each); route chunk lacked builtin handlers registered at startup via
instrumentation. execute() now falls back to builtinSkills registry, so
POST /api/skills/executions works for file_read/web_fetch/etc.
- memory: scheduleVectorUpsert is fire-and-forget and embeddings are slow;
health-check verify (create->delete test memory) left queued upserts
failing with 'memory not found' every 30s. Check existence before embedding
and skip quietly.
* fix(skills): encode tool names with @ and . for providers rejecting them
Skill tools were advertised as 'name@version' (e.g. test-fr2@1.0.0), but
DeepSeek/Groq/OpenAI reject function names not matching ^[a-zA-Z0-9_-]+$.
Names already valid are left untouched; invalid ones are reversibly encoded
as omr_skill_<base64url> and decoded in interception before registry lookup.
* fix(combos): include DB id column in combo records for dashboard links
getCombos() selected only data/sort_order/context_cache_protection, so
combos whose JSON blob lacked an id field returned id: undefined. The
dashboard then linked to /dashboard/combos/undefined and Combo Control
Center failed with 'Combo not found'. Merge the id column into parsed
rows (authoritative, only when the blob has no id).
* fix(skills): normalize flat skill schemas to object schema for Gemini/Claude
Stored skill schemas are flat property maps ({ text: { type: string } }),
which OpenAI-compatible providers tolerate but Gemini
(function_declarations[].parameters) rejects with 'Unknown name ... Cannot
find field'. Wrap bare maps into { type: 'object', properties: {...} } for
all three tool formats.
* fix(skills): warm registry cache before skill injection in chat path
injectSkills() lists the in-memory skillRegistry, which is empty after a
cold start until something calls loadFromDatabase(). The interception path
already warms the cache (#2815); the injection path did not, so skills
were silently skipped (no_enabled_skills) for the first requests after
restart. Warm the cache for the chat owner before injection.
---------
Co-authored-by: Egor <egorich-print@users.noreply.github.com>
|
||
|
|
75c6a18a9b |
[v3.8.50] fix(api): serve stale model catalog during refresh (#8728)
* fix(api): make model catalog refresh response-safe * fix(api): invalidate model catalog mutation paths * fix(db): preserve aliases backup import after catalog rebase --------- Co-authored-by: Erick Kinnee <erick@ekinnee.dev> |
||
|
|
5e02cf7623 |
fix(radar): refresh entitlement-sensitive state (#9776)
* fix(radar): refresh entitlement-sensitive state * chore(changelog): assign Radar fix to PR 9776 * test(radar): localize canonical feed fixture * test(radar): refresh canonical feed hash --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
8fc4023f94 |
fix(migrations): allow fresh install past mass-migration guard (#9934) (#10022)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
fed0858f89 |
fix(encryption): identify failing credential in decrypt errors (#9927) (#10019)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
0dbc34ea56 |
feat(cursor): exclusive live listing + verbatim AgentRun model ids (#9911)
* feat(cursor): prefer live synced catalog for listing and Test All When an active synced Cursor catalog exists, list only live models plus injected auto routers (and customs). Keep the static registry as offline fallback. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): send live-catalog model ids verbatim on AgentRun Skip #7289 effort/reasoning splits when the exact id is in the active synced Cursor catalog so AgentRun does not rewrite flattened live ids into missing bases that return AI Model Not Found. Also wires auto-cost/balance/intelligence to default + optimization for the injected routers. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: yansigit <yansigit@users.noreply.github.com> |
||
|
|
ee0073dc4a | fix(db): invalidate stale LKGP pins on connection delete (#9936) | ||
|
|
9bcb48a2d2 | fix(db): avoid skipping pending job registry migration 146 (#9965) | ||
|
|
f390327c88 |
fix(cleanup): prune mcp_tool_audit/a2a_task_events by created_at column (#9963)
Both tables (002_mcp_a2a_tables.sql) store their row timestamp in created_at; the cleanup queries used WHERE timestamp < ? which does not exist, so every boot-time cleanup logged: Error cleaning mcp_tool_audit: SqliteError: no such column: timestamp Error cleaning a2a_task_events: SqliteError: no such column: timestamp and retention pruning for these two tables never ran. Fix the DELETE columns and align the log labels/doc comments with the real table names. Adds source-level invariant tests (cleanup-column-fix.test.mjs) asserting the created_at column for both tables. |
||
|
|
0d36801f20 |
Merge remote-tracking branch 'origin/release/v3.8.50' into fix/release-v3.8.50-post-sweep-base-red
# Conflicts: # src/i18n/messages/vi.json |
||
|
|
754ba0fa86 | fix(release): repair post-sweep base regressions | ||
|
|
2afaab52a1 |
maint: final follow-up cherry-pick #9619 (#9901)
* fix(quality): clears two release/v3.8.50 base-red gates Unblocks Merge integrity and Docs Gates for every PR against release/v3.8.50, not just this branch: - changelog.d/features/9415-newapi-sub2api-aggregator-balance.md had a non-standard YAML frontmatter header that no other fragment in the tree uses. check-changelog-integrity.mjs reads a fragment's first non-blank line to validate it starts with a markdown bullet; the frontmatter's leading `---` made that check fail regardless of the actual bullet content further down. Removed the frontmatter and reformatted the body to match the documented changelog.d/README.md bullet convention. - docs/ops/VM_DEPLOYMENT_GUIDE.md documented OMNIROUTE_MAX_POOL_SIZE and OMNIROUTE_DB_POOL_SIZE as tunable env vars, but neither is read anywhere in the codebase (confirmed via full-repo grep) — this repo uses SQLite, which has no connection-pool concept these vars could plausibly control. check:fabricated-docs --strict correctly flags fabricated env-var claims; removed the bullet rather than implementing a feature to match invented documentation. * fix(i18n): completes Vietnamese parity, fixes empty migration query Two more release/v3.8.50 base-red items, both surfaced while chasing CI failures on unrelated PRs: - vi.json was missing 8 keys that #9539 (NewAPI/Sub2API aggregator balance) added to en.json without a matching i18n:sync-ui run — pt-BR.json already had all 8, only Vietnamese drifted. Added translations for the 6 provider-settings strings, the feature-flag description, and the quota tooltip; verified against tests/unit/i18n-vi-completeness.test.ts (parity, placeholder preservation, ICU parse — all 5 assertions pass). - src/lib/db/migrations/120_interception_rules.sql was pure comments documenting a no-schema-change key_value namespace, with no executable SQL statement — the migration runner logged "FAILED: 120_interception_rules — Query contained no valid SQL statement" on every fresh DB init. 118_provider_param_filters.sql (same pattern, two migrations earlier) already ends with a bare `SELECT 1;` no-op for exactly this reason; 120 was just missing it. Verified directly against better-sqlite3 that the file now executes without error. * fix(types): clears 6 pre-existing release/v3.8.50 typecheck errors typecheck:core is its own blocking CI job (quality.yml), separate from Docs Gates/Merge integrity. Confirmed pre-existing and unrelated to any current work by branching this worktree directly from upstream/release/v3.8.50 with no other merges applied. - accountSemaphore.ts: isBypassed() already excludes null/<=0 maxConcurrency before ensureGate() is called, but a boolean- returning helper isn't a type predicate TS can narrow through. Added a targeted `as number` at the one call site, with a comment explaining why it's safe. - combo/comboStructure.ts: two module-scope `const HARD_COMPAT_REASONS` declarations with different values — a genuine "can't redeclare" compile error, not a narrowing gap. The first (4-item set including "output_tokens") had zero usages between its own declaration and the second; the second (3-item set, matching the CompatFilterOptions doc comment exactly) is what hasHardCapabilityFailure/ describeCapabilityFilterExhaustion/the third call site all actually use. Removed the dead first declaration. - combo/comboStructure.ts + combo/fusionPanel.ts: both accessed `.prompt`/`.model` on a `ComboModelStep | ComboProviderWildcardStep` union after only excluding `combo-ref`, but `ComboProviderWildcardStep` has neither field — a real latent bug (fusionPanel would have pushed `undefined` into a fusion panel for a wildcard step). Narrowed to `step.kind === "model"` in comboStructure, and switched to the already-existing `getComboModelString()` helper in fusionPanel (which correctly resolves to null for unsupported step kinds, mirroring how combo-ref is already skipped there). Verified directly via a standalone script exercising both branches (wildcard vs. model step). - combo/quotaStrategies.ts: imported `preferAntigravityConnectionsWithStoredProject` from a module that never existed (`../antigravityProjectPersistence.ts`, distinct from the real `antigravityProjectPersist.ts`) — the function itself was referenced nowhere else in the codebase. Wrote the missing implementation: prefers Antigravity connections with a discovered `projectId` for reset-aware routing, failing open to the full list when none have one yet (per the file's own "Exclude... from reset-aware pool" changelog note, softened to a preference — strict exclusion would empty the pool entirely for a fleet of freshly-added accounts). Verified directly via a standalone script. - compression/engines/ccr/index.ts: `enforceGlobalBudget(owner, bytes)` was called with only `bytes` at one of its two call sites, missing the `owner` argument the other call site (and the function's own doc comment on preferring the calling principal's LRU eviction) already uses correctly. Added the missing `entry.principalId` argument. - firecrawlQuotaFetcher.ts: `fetchFirecrawlQuota` was annotated to return `Promise<QuotaInfo | null>` but every return path constructs a `FirecrawlQuota` (QuotaInfo extended with remainingCredits/planCredits/ extraCreditsInferred/overPlan) — the type the file already defines and the type `parseFirecrawlCreditUsage` already correctly returns. Widened the annotation to match; `FirecrawlQuota extends QuotaInfo` so this stays compatible with the `QuotaFetcher` contract. npm run typecheck:core and npm run check:dashboard-typecheck both pass cleanly. A subset of DB-backed tests in this area also fail, but 100% attributably to an already-tracked, unrelated migration version collision (134 -> [ccr_blocks, proxy_logs_egress_ip], see _tasks/features-v3.8.4/9route/POST-MERGE-AUDIT.md) — confirmed by every failure's stack trace bottoming out at that exact error, not at anything touched here. * fix(sse): update stale ALL_ACCOUNTS_INACTIVE test assertions to ALL_TARGETS_SKIPPED Two combo-routing-engine.test.ts cases assert the pre-dispatch-skip scenario (isModelAvailable always false, zero dispatch attempts) returns ALL_ACCOUNTS_INACTIVE. Production code already distinguishes this case via the recordedAttempts === 0 branch and returns the more precise ALL_TARGETS_SKIPPED -- the tests were never updated when that branch shipped upstream, so they fail on a clean release/v3.8.50 checkout independent of this PR's changes. * fix(sse): update second stale ALL_ACCOUNTS_INACTIVE assertion (T24) Same pre-existing upstream test-drift as |
||
|
|
54bba33e2f |
maint: follow-up cherry-pick fix-in-place #9629 (conflict-resolved fallback) (#9885)
* fix(compression): add Lite tool truncation toggle * fix(antigravity): add missing antigravityProjectPersistence.ts module The quota-strategy engine (quotaStrategies.ts) imports from antigravityProjectPersistence.ts, but only antigravityProjectPersist.ts existed in the tree. Add the missing module with the expected preferAntigravityConnectionsWithStoredProject() helper and re-export the existing persistDiscoveredAntigravityProjectId(). Co-authored-by: diegosouzapw <diegosouza.pw@outlook.com> * fix(file-size): rebaseline strategySelector.ts for Lite truncation toggle The PR adds one line to threading options?.config?.lite into applyLiteCompression. Update the frozen size from 1060 to 1061. Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Refs #9629 --------- Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: xz-dev <xz-dev@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
a54c1f73af |
fix(db): resolve ccr migration version collision (#9884)
Renumber the CCR block-store migration from 134 to 139, reconcile databases that already applied the legacy slot, and add regression coverage for both upgrade paths. Co-authored-by: fenix007 <fenix007@users.noreply.github.com> |
||
|
|
5eba045175 |
maint: follow-up cherry-pick fix-in-place #9510 (fallback resolution) (#9880)
* feat(api): add GET /api/resilience/connections for per-account state The three temporary-failure mechanisms each have their own scope -- the provider circuit breaker covers a whole provider, connection cooldown covers one account, model lockout covers a provider/connection/model triple -- and until now nothing showed them side by side. Diagnosing "why is this key being skipped" meant reading three separate surfaces and correlating by hand, which is exactly what the docs' own debugging guidance asks an operator to do. The route returns all three keyed by connection, plus the breaker's transition history so a flapping provider is visible as a sequence rather than a single current state. getStatus() already assembled everything except that history; it now returns a copy of it and carries an explicit CircuitBreakerStatus type instead of an inferred one. Reading raw connection rows for this meant widening getRawProviderConnections' column projection, so the existing allowlist is exported and the route selects through it. A test asserts every column the route names is in that allowlist, which turns a future typo into a failure here rather than a silent empty field. Each of the three data sources is wrapped independently: one of them throwing degrades that section and sets meta.degraded rather than failing the whole response, since a partial view still answers most of the questions the page exists for. Loopback-gated. It spawns nothing, unlike every other entry on that list, but it exposes per-account operational state and the comment says so to keep it from being read as precedent for gating read-only routes generally. Tests are real isolated-DB integration tests rather than mocks -- ESM mocking is unavailable here (no mock.module, non-configurable exports) and the codebase already has the isolated-DB pattern, which exercises more than a mock would anyway. Signed-off-by: Minxi Hou <houminxi@gmail.com> * feat(dashboard): add the per-account resilience connections page Renders what the API added: every connection with its cooldown, its provider breaker, and its model lockouts in one table, with a detail view per connection and the breaker's transitions drawn as a timeline. The timeline is the part that is hard to get from the existing surfaces -- a breaker sitting at CLOSED right now looks healthy, and only the sequence shows it has opened four times in the last hour. Polls rather than streams. The state it displays changes on the order of seconds to minutes and the page is loopback-gated, so an SSE channel would buy nothing over an interval. ModelCooldownsCard had its own formatRemaining. The new table needs the same countdown format and two copies would drift, so it moves to shared/utils/formatRemaining.ts and both import it -- behaviour unchanged, the extracted version differs from the deleted one only in local variable names. DataTable's column and row interfaces are exported for the same reason: the new table types against them rather than restating their shape. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(i18n): translate new resilience-connections screen strings PR #9510 added the "Connection Resilience" dashboard screen but the sync-added i18n keys (sidebar.resilienceConnections/Subtitle and the full resilienceConnections namespace) were left as __MISSING__: in every non-English locale, dropping i18nUiCoverage.pct below the 99 ratchet baseline. Translate all ~78 new leaf strings into all 41 non-English locales. Pre-existing unrelated __MISSING__ debt (hermesRole*, apiProtocol*, grokAutoTopUp*, featureFlagExposeFunctionalGatewayMirrorsDescription) is left untouched — out of scope for this fix. Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com> |
||
|
|
5f75abe4a2 |
cherry-pick(pr-9634): fix(test): reconcile base-drifted test expectations on release/v3.8.50 (#9874)
* fix(combo): restore routing module load * fix(db): resolve ccr migration version collision Renumber the CCR block-store migration from 134 to 139, reconcile databases that already applied the legacy slot, and add regression coverage for both upgrade paths. Co-Authored-By: GPT-5 <noreply@openai.com> * fix(changelog): format the aggregator balance fragment as a bullet The fragment landed with YAML frontmatter rather than the bullet the aggregator reads, so check:changelog-integrity exits 1 on every branch and takes the merge-integrity job down with it regardless of what the branch changed. Only the format changes. The entry text is the author's, unedited, and now carries the link to the pull request that shipped it. * fix(test): update expected auth/vision/provider schema for base-drifted expectations * fix(test): narrow this branch to the drifted test expectations Three other PRs already cover what this one was carrying. #9618 renumbers the colliding ccr_blocks migration, #9632 repairs the malformed aggregator changelog fragment, and #9676 restores the combo module load by implementing the selection helper the import was reaching for, rather than deleting the caller the way this branch did. Keeping any of it here would put two files back on the same migration slot and overwrite a better fix with a worse one. What survives is the part none of them touch. Once the combo barrel loads again, three assertions in the context-window filter suite start failing: they demand that catalog-too-small targets be dropped, while the file's own header and its four neighbouring tests say those targets stay available as runtime fallback. The unresolved import was masking them. A new case pins the output-token limit as a genuine hard requirement so the relaxation cannot drift further. The provider count assertion kept one literal at the old value after the rest of the file moved to 198, so the partition check failed on a sum that was correct. * chore(quality): re-time migrationRunner for the 139 guard on the new tip --------- Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru> Co-authored-by: GPT-5 <noreply@openai.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
c1d951e5c5 |
cherry-pick(pr-9572): fix(providers): reject the dashboard password as a connection API key (#9877)
* fix(providers): refuse to store the dashboard password as a connection API key A browser autofilled the management password into a connection's API-key field. The resulting credential authenticates against nothing, so every request routed through that connection came back 401, and because the field looks like any other password input the same autofill fired again while the connection was being repaired by hand. The refusal belongs on the write path rather than in the form. Twenty routes create or update connections and all of them funnel through createProviderConnection and updateProviderConnection, so one check there covers every entry point including a future one. The two other places that write api_key are left alone on purpose: one re-encrypts rows that already exist and the other is the one-time db.json import, and neither takes a value an operator just typed. Update checks the incoming value, never the merged one. A connection that already holds the password has to stay editable or the operator cannot repair the exact state this prevents, and re-checking the merged value would spend a bcrypt round on every unrelated field edit. Only a real match blocks the write. An unreadable settings row or a throwing bcrypt call logs and allows, because a guard against one specific mistake must not turn into a way to lock out every connection write. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(providers): compare the untrimmed credential, and cover the guard's branches The guard trimmed the incoming value before comparing it, which catches a paste carrying whitespace the password does not have. It missed the mirror case: neither the login route nor the set-password route trims, so a dashboard password may itself begin or end with a space, and an autofill reproducing it exactly was trimmed into a value that no longer matched the stored hash. The write then went through, which is the state this guard exists to prevent. Both forms are compared now, the second only when the first fails on a string that differs, so an ordinary key still costs a single bcrypt round. Two branches carried no coverage and both are load-bearing. The catch that logs and allows is the only path that lets a write through; a stored hash bcrypt cannot parse reaches it without needing a mock, since the shape check accepts an impossible cost factor that the comparison then rejects. The early return is what keeps a token renewal -- a write carrying tokens but no apiKey -- from paying for a settings read and a bcrypt round every time it fires, and the same unparseable hash makes that path observable, so an absent warning is proof the return happened. The narrower scope is deliberate and now says so in the code: the OAuth tokens arrive from a provider's token endpoint rather than from a form, so extending the comparison to them would charge every renewal for a field no autofill can reach. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
5e5919dcc0 |
maint: follow-up cherry-pick fix-in-place #9631 (conflict-resolved fallback) (#9886)
* feat(db): add a job registry for scheduled background work Background jobs each ship their own timer today, so there is no list of what is scheduled, no history of what ran, and no way to pause one without an environment variable and a restart. The registry gives them one home: a jobs table holding the schedule, a job_runs table holding the outcomes, and a loopback-only API to inspect and control both. Cron jobs read their expression through an optional cronGetter rather than the stored column, so an operator changing OMNIROUTE_WARMUP_CRON does not need the row rewritten. register() is an idempotent upsert that refreshes the schedule but never overwrites `enabled` or `created_at`, which is what lets a job be re-registered on every boot without discarding the operator's toggle. Run history is pruned per job rather than globally, and safeRun records a failure for a handler that throws as well as one that returns success:false, so a crashing job leaves a trail instead of a gap. The API is under /api/jobs and gated to loopback in the route guard. It can trigger a run and flip a job off, which is runtime administration and does not belong on a remotely reachable surface. Signed-off-by: Minxi Hou <houminxi@gmail.com> * feat(jobs): move the budget reset and token health check onto the registry Both jobs owned their own timer and started themselves as an import side effect, so nothing could report whether they were running, when they last ran, or why a run failed. They now register with the job registry and are started from it, which also means their schedule and run history are visible through /api/jobs. startAll() runs each interval job's first tick synchronously, so both entry points start the registry only after initializeCloudSync() has been awaited. The old wiring reached that ordering two different ways: the budget reset was started after the init call, and the health check's first sweep sat behind a 10s timer. Replacing both with one startAll() would otherwise have moved the two handlers in front of the initialisation they run against. Both entry points also register the same pair of jobs. Registering one and not the other is how a background job goes missing without anything failing. sweep() now returns how many connections it swept, so the health check can record a real records_affected the way the budget reset does. The migration documents that column as a per-job count, and hardcoding zero would have left one of the two jobs reporting a number the schema promises but the code never produces. A skipped or empty sweep reports zero. Every existing caller ignores the return value. The token health check keeps its own disable semantics: the handler still calls isHealthCheckDisabled() before sweeping, so OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK, the production-build phase and the automated-test guard behave as before. Its registry adapter lives in src/lib/jobs/ next to the budget reset rather than in tokenHealthCheck.ts, which is already above its frozen size ceiling on the base branch and should not grow further. The adapter lets a failing sweep throw rather than reporting it itself, matching the budget reset: safeRun records a thrown error as a failure run with its message. The warmup job is seeded disabled. Its handler arrives with the warmup scheduler, and startAll() filters on enabled before it looks for a handler, so seeding it enabled here would warn about the missing handler on every boot. * fix: allowlist cron-parser dep and document OMNIROUTE_RUNNOW_TIMEOUT_MS env var Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
a39f78c6f8 |
maint: follow-up cherry-pick fix-in-place #9707 (conflict-resolved fallback) (#9890)
* fix(db): renumber ccr_blocks migration 134 -> 139 134 was taken by 134_proxy_logs_egress_ip, so two migrations shared the same numeric prefix and check-migration-numbering failed. Move ccr_blocks to the next free slot and add the retroactive isSchemaAlreadyApplied guard so a DB that already applied it under 134 skips the re-run. * fix(combo): restore missing preferAntigravityConnectionsWithStoredProject quotaStrategies imported the reset-aware pool filter from ../antigravityProjectPersistence.ts, a module that does not exist — the helper belongs in antigravityProjectPersist.ts and was never added there, breaking typecheck. Add the helper alongside the persist path, point the import at the real module, and cover the filter with unit tests. * chore: add Makefile wrapping the canonical npm scripts * fix(compression): remove duplicate Antigravity project helper The release branch already includes the generic project-aware connection selection helper. Keep that implementation and remove the duplicate introduced while cherry-picking #9707. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Matias Baglieri <168452313+matiasbaglieri@users.noreply.github.com> |
||
|
|
ed7a68e1a9 |
maint: follow-up cherry-pick fix-in-place #9719 (conflict-resolved fallback) (#9893)
* fix(db): clear combo pins when connections are deleted * docs: add changelog entry for #9719 --------- Co-authored-by: Zartharas <1402357+Zartharas@users.noreply.github.com> |
||
|
|
04b4690f84 |
cherry-pick(pr-9730): fix(compression): persist RTK renderer configuration (#9867)
* fix(compression): persist RTK renderer configuration * docs(changelog): add fragment for #9730 Adds the changelog.d/fixes/9730-persist-rtk-renderers.md fragment required by check:changelog-integrity for the RTK enableRenderers persistence fix in PR #9730. --------- Co-authored-by: Isaac <isaaclyons98@gmail.com> |
||
|
|
a2eab58dde |
fix(types): expose SQLite transaction state (#9848)
Co-authored-by: backryun <bakryun0718@proton.me> |
||
|
|
580162548a |
cherry-pick(pr-9818): feat: generic OpenAI-compatible video custom provider (#9844)
* feat: generic OpenAI-compatible video custom provider
Adds a generic OpenAI-compatible video generation path so users can add
custom video providers (base URL + API key) without per-provider code.
Changes:
- open-sse/handlers/videoGeneration/openai.ts (new): generic handler
with resolveVideoEndpoint, fetchVideoEndpoint, handleOpenAIVideoGeneration
- open-sse/handlers/videoGeneration.ts: added resolveVideoBaseUrl(),
dispatch for 'openai-video' format before 'vertex-veo', synthetic config
for custom providers, fallback for resolvedProvider
- src/app/api/v1/videos/generations/route.ts: scans custom models for
supportedEndpoints.includes('videos'), resolves credentials via
getProviderCredentialsWithQuotaPreflight, passes resolvedProvider
- src/shared/validation/schemas/provider.ts: added 'videos' to
supportedEndpoints enum
- tests/unit/video-generation-handler.test.ts: handler-level test for
custom provider
- tests/unit/video-custom-provider-route.test.ts (new): route-level tests
covering custom provider with/without videos endpoint, unknown provider
All verification:
- typecheck:core passes
- 17 video tests pass (3 new route tests + 1 new handler test)
- no regressions in image generation tests
* test(video): drop duplicated test.after cleanup in custom-provider route test
* feat(video): declarative job presets + dispatcher, route, and handler test coverage (#9818)
- job.ts: presets (agnes-video-job, muapi-video-job) with submit→poll→done executor
- videoGeneration.ts: generationConfig.preset dispatch branch + mediaGenerationRoute pass-through
- provider-models/route.ts + models.ts: generationConfig persisted on addCustomModel
- provider schema: generationConfig optional field
- tests: resolvedProvider bare-model + job preset happy/failed/unknown paths
- docs/video-preset-generation.md
* fix(video): restore dashscope + novita handler imports dropped in refactor
* refactor(video): extract Runway helpers
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
|
||
|
|
77cce62357 |
fix(ci): restore current release test integrity (#9819)
* fix(ci): restore base test integrity * fix(quality): clear inherited fast gate drift * fix(ci): clear inherited unit test reds * fix(i18n): preserve Vietnamese catalog parity * fix(i18n): preserve Portuguese catalog parity --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
df1ea5bd77 |
fix(db): align domain_cost_history cleanup cutoff with millisecond column (#9625)
Refs: base-red #9737 |
||
|
|
064a19b2d1 |
fix(quota): clean managed combos when deleting pools (#8906)
Merge-train validated |
||
|
|
a1c864373a |
referrals from standalone /v1/referrals feed (no 30-day delay) (#9762)
* feat(radar): sync referral links from standalone /v1/referrals/latest feed Referral links previously came from the catalog feed cache, which on the community tier can be up to 30 days stale -- a newly-added referral would not reach a free/community user for up to a month. Adds a new sync module (syncRadarReferrals), Ed25519-verified feed schema, and a dedicated radar_referrals_cache table (migration 142) so referrals sync on their own, much shorter cadence instead of inheriting the catalog's delay. getRadarReferrals()/getDefaultReferralFor() now read the new cache instead of the catalog feed's embedded referrals field (kept on RadarFeedSchema for backward-compat with already-cached catalog feeds, but no longer read). * feat(radar): wire sync-on-read + scheduler side-sync for referrals GET /api/radar/referrals now triggers syncRadarReferrals() inline whenever the cache is missing or older than 1h (shouldSyncReferralsOnRead), so fixed links show up promptly on the next dashboard load instead of waiting on a background timer. The route itself still never talks to the upstream feed server directly -- syncRadarReferrals() remains the only network touchpoint. radarSchedulerTick() also evaluates referrals staleness on the same hourly tick used for the catalog, independent of the catalog's own due-ness, as a best-effort side effect that never changes RadarTickResult's shape and is swallowed on error. * docs(radar): document the standalone referrals feed sync Explains the /v1/referrals/latest feed, its no-tier-field-in-body design (x-omniroute-feed-tier header is the only tier source), the sync-on-read + scheduler side-sync triggers, and the self-hosting note for forks that only serve the catalog feed. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
52a6b04f8e | fix(types): preserve video URL override contracts (#9747) | ||
|
|
71c85f31cd |
feat(guardrails): modality bridge core — vision mode/task-aware/cache/input_image + modalityBridge settings (#9759)
* feat(sse): unified media-part detection helper (image+audio, input_image) * refactor(guardrails): extractImageParts/comboStructure delegate to unified media detector * fix(sse): media detector — audio parts no longer shadow sibling/nested image indicators * fix(guardrails): close extract↔replace contract for input_image (allowlist + splice) * perf(guardrails): skip media traversal when bridge disabled; short-circuit combo image check * feat(guardrails): in-memory LRU bridge cache (sha256 keyed) * feat(settings): modalityBridge* schema with legacy visionBridge* fallback * feat(db): migrate visionBridge* settings to modalityBridge* (idempotent) * refactor(guardrails): harden bridge cache key/config + settings resolution (review minors) * feat(guardrails): vision bridge mode selector (auto/describe/reroute) short-circuit * feat(guardrails): task-aware vision description prompt (default on) * feat(guardrails): describe-path cache integration * docs(guardrails): review polish — cache-key coupling notes + helper header * feat(guardrails): in-memory bridge stats + modality-bridge response header * feat(api): modality bridge stats endpoint + header wiring in chat handler * docs(guardrails): document modality bridge mode/task-aware/cache/header + stats endpoint * chore: untrack _tasks symlink (inherited from base tip; blocks pre-commit tracked-artifacts gate) * fix(db): renumber modality bridge migration 139->140 (base renumbered ccr_blocks to 139) * docs(guardrails): migration filename touch-up 139->140 * docs(db): stale comment touch-ups after 139->140 renumber and #9688 landing * fix(db): renumber modality bridge migration 140->141 (base renumbered connection_runtime_state to 140) * test(db): migration test titles 139->141 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
acfb844852 |
fix(db): resolve migration version 135 numbering collision (#9745)
Two files both claimed migration version 135: 135_connection_runtime_state.sql (#9449, landed 2026-08-07) and 135_migrate_model_capability_max_token.sql (#8908, landed 2026-08-05). #9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50. This is not cosmetic: getMigrationFiles() throws "Migration version collision detected" the moment ANY code path first touches the database (getDbInstance() -> runMigrations()), which means a completely fresh install/deploy from this branch cannot even boot — confirmed live against a freshly built container while testing unrelated live-verification tooling. Renumbered the later-landing file to 140 (the next free slot) and added the matching isSchemaAlreadyApplied("140") retroactive guard in migrationRunner.ts, so a DB that already ran this migration under the old 135 number isn't treated as needing a fresh application. This matches the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file (also caused by the same recurring branch-before-merge numbering race). Test plan: - TDD: new tests/unit/migration-135-numbering-collision.test.ts (2/2) — spins up a hermetic fresh DB and confirms getDbInstance() applies every real on-disk migration without throwing, plus confirms both formerly-135 migrations' effects are present. Confirmed failing (reproducing the exact live crash) with the pre-fix colliding filenames restored, passing after the rename. - npm run typecheck:core — clean - npm run lint — clean - npm run check:file-size — clean (migrationRunner.ts rebaselined 1084->1094 for the new guard case) - Full migration-runner + migration-numbering test suites (64 tests across 6 files) — all pass, no regressions |
||
|
|
552f2e1563 |
fix(backend): stop reasoning replay placeholder from self-poisoning (#9573) (#9610)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679). |
||
|
|
217ac4c829 |
feat(warmup): proactive Claude warmup scheduler (#8848) (#9449)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679). |
||
|
|
74cf860ccc |
feat(providers): make video_url passthrough configurable per provider/model (#9248) (#9419)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679). |
||
|
|
c50c783549 |
fix(proxies): resolveProxyForConnection now returns the proxy name, so the dashboard badge shows the name instead of the hostname (#8995)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
4299085da1 |
fix(db): stream DB backup export instead of buffering entire file into memory (#9045)
The GET /api/db-backups/export route used fs.readFileSync + new Response(buffer) which buffered the entire database backup into memory — for a 280MB DB this spiked RSS to ~1.5GB (5.3x the DB size), causing timeouts on constrained machines. Fix: stream the backup file as a ReadableStream response body using fs.createReadStream + ReadableStream, keeping peak RSS under 0.5x the DB size. Includes cleanup on stream completion, error, and client abort. Also: changed fs.copyFileSync to await fs.promises.copyFile in node:sqlite, bun, and sql.js adapters so the backup() call does not block the event loop during a large DB copy. Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
c40d4b17ee |
fix(ci): clear the NEW base-reds from the 08-06 merge batch (migration collision #2 + broken import) (#9688)
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed Continuing the base-red drain — every one of these reproduces on the pure tip. - tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1. The diff is ADDITION-ONLY — the unorouter block from #9009; no existing provider entry changed. 3/3. - tests/unit/provider-models-route.test.ts: |
||
|
|
e4e0c254ea |
fix(db): add transient-error retry to corruption probe to prevent data loss under concurrent load (#9541)
Closes #9541 |
||
|
|
5f471181fa |
refactor(db): add combo repository boundary (#8757)
Validated in local merge-train (tomni-proxmox-113) |
||
|
|
d69f521491 |
fix: reconcile active live model catalogs (#9294)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip) |
||
|
|
a598fbb090 |
fix(combo): least-used quota strategy and wildcard UI preservation (#8894)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip) |
||
|
|
8e27f5ec8d |
fix(sse): back the CCR block store with a durable tier (#9061) (#9198)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip) |
||
|
|
f2e36ad0ce |
fix(ci): clear base-reds on release/v3.8.50 (migration collision + 4 masked gates) (#9600)
Validated in local merge-train (diegosouzapw batch) |
||
|
|
2d617325e7 |
feat(catalog): add hideAutoCombos and hideNoThinkVariants settings toggles (#9418) (#9535)
Validated in local merge-train (diegosouzapw batch) |
||
|
|
b553ac4d14 |
feat(db): add provider-scoped model aliases (#9068) (#9469)
Validated in local merge-train (diegosouzapw batch) |
||
|
|
2ddbbc61a6 |
[v3.8.50] feat(memory): MemoryBackend provider pattern with generic HTTP connector (#8752)
Validated in local merge-train T7 (ungrouped batch 2) |
||
|
|
51f9ffc007 |
[v3.8.50] feat(services): add Dario as a 5th embedded service (Claude Code toggle/failover) (#8523)
Validated in local merge-train T7 (ungrouped batch 2) |
||
|
|
5dc8631fe4 |
[v3.8.50] fix(db/apiKeys): respect provider parameter in group model permission checks (fixes #8803) (#8817)
Validated in local merge-train T7 (ungrouped batch 2) |
||
|
|
c3ae5b8893 |
refactor(db): preserve normalized combo model type (#8809)
Validated in local merge-train T7 (ungrouped batch 2) |
||
|
|
e7f6b1d130 |
feat(radar): flag-gated signed free-model catalog overlay (#9515)
* feat(dashboard): add RADAR_ENABLED flag (default off) * feat(db): radar feed cache + settings with encrypted supporter key * feat(radar): signed feed sync with pinned key and version floor - feedSchema.ts: Zod v4 schema mirroring the server feed format (discriminated union on budget.kind, enum constraints, etc.) - pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks - verify.ts: signature verification over exact wire bytes, never throws - sync.ts: full download/verify/validate/cache pipeline with injectable deps, feature-flag gate, opt-in gate, version floor (numeric compare), and sanitized error reasons (no stack traces) - 40 tests covering: contract hash, key handling, sig verification, schema validation, version compare, all sync paths (disabled, opt_out, invalid_signature, invalid_schema, stale, updated, error), auth header injection, and cache-untouched assertions for every failure mode * feat(radar): read-time overlay merge rules over the free catalog Pure function applyFeed() merges the cached Radar feed over the static baseline catalog at read time, honoring 4 rules: 1. Feed never overwrites a local override field. 2. enabled:false disables the entry with disabledBy:"radar" provenance. 3. User-added entry NOT in the feed survives untouched. 4. User deletion tombstone prevents feed resurrection. getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt payload all fall back to baseline. Valid cache applies the overlay and returns feed metadata (version, tier, fetchedAt). TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/ valid/bad-feed + baselineToMergedEntries converter). * feat(dashboard): radar catalog and guided setup screens - API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings - All gated on RADAR_ENABLED flag (404 when off) - Error responses via buildErrorBody(), never raw stack/message - Settings never echoes clear supporter key (masked omr_****<last4>) - Sync delegates to syncRadar() server-side, never proxies feed URL - Dashboard pages: - /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated) - /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection - Uses existing Card component and next-intl patterns - Sidebar: radar entry in costs group with icon - i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces - Tests: - radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization) - radar-page-state.test.ts: 5 tests (pure state logic) - All 90 radar tests pass (including prior 74) * docs(radar): module doc and flag-off inertia test Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync opt-in and privacy promise, the Ed25519 signature/pinned-key security model, tiers, the read-time overlay merge rules, and the self-hosting env vars — plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md. Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was failing on this branch since the sync.ts commit added the reads. Add tests/unit/radar-inertia.test.ts as the single canonical place asserting the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three /api/radar/* routes 404, the flag resolves to the definition default with no override, getRadarCatalog() returns exactly the baseline without touching the cache, and computeFreeModelTotals() keeps its pinned values with the Radar module imported alongside it. * fix(db): renumber radar migration to 135 after collision with 134 The base branch introduced 134_proxy_logs_egress_ip while this branch carried 134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes. This migration has never been applied to a real database (the PR is unmerged), so no retroactive isSchemaAlreadyApplied guard is needed. * i18n(radar): translate radar catalog and setup strings to all locales The UI-coverage ratchet measures (present - placeholder) / total_en, so the __MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only real translations restore the metric. Scoped to this PR's namespaces (radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would have pulled ~978 unrelated pending keys into this diff. Placeholders and code identifiers verified preserved across all 1682 strings. * fix(radar): trust the served-tier header instead of the signed body field The signed feed body always carries tier:"live" by design (one signed artifact per version — rewriting the field server-side per request would break the exact-bytes Ed25519 signature). The server now returns the tier ACTUALLY served via the x-omniroute-feed-tier response header, so free users on a delayed community snapshot no longer see "Ao vivo (tempo real)" in the UI. sync.ts now reads and validates that header (falling back to the body's tier only when the header is absent or holds an unrecognized value) and stores the served tier in the cache; index.ts already surfaces cache.tier to the UI unchanged. * test(combo): shorten an assert message that exceeded the line limit The assertion added by #9507 was 104 chars, so prettier reformatted it into five lines on the next commit that touched the file, pushing it past its frozen size (3449) and failing check:file-size. The message is shortened (the issue reference stays in the comment directly above); the assertion itself is unchanged, and the file is back to 3448 lines and prettier-clean. * i18n(radar): use the canonical zh-TW glossary terms The machine translation produced retired renderings the glossary gate blocks: 供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件). Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts is back to 17/17. * fix(radar): point the default feed URL at the domain that exists radar.omniroute.dev was a placeholder for a domain that was never registered, so an out-of-the-box sync would fail DNS resolution for every user. The live feed is served from radar.omniroute.online (the subdomain the design always specified), now behind Cloudflare TLS. Forks still override it via RADAR_FEED_URL. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |