4 Commits

Author SHA1 Message Date
Evil0ctal d6b9fd90de feat: author paging with a depth, a real logo, collection stats, identity history
Five things, and a check for the bug the fifth one shipped with.

The author mode pages the feed now, to a depth chosen from a menu -
newest 20, 50, 100, 200, or everything the feed gives up. A menu rather
than a number field because the cost is the thing worth showing: every
post is a download and every page is a read through the pool, and a
free-text depth invites "99999" typed without a thought. It walks pages
rather than trusting one, because a page is whatever the platform felt
like returning - 23 when 20 was asked for - and "the newest 100" has to
mean 100.

It also takes a bare sec_user_id, which is what the archive and the
playground hand you. Both platforms spell it the same way, so the
`MS4wLjABAAAA` prefix is the test - a recognition check, not a validator;
the platform decides whether the id exists.

The overview counted requests and nothing else: how many, how well, how
fast, and not one number about what the instance is accumulating. A
second row says what was kept - archived posts, media downloaded, disk
used against its ceiling, downloads in flight, watchlist size - all of it
from endpoints the page simply was not calling.

The logo is the 2022 mark redrawn as an SVG. The braces are the part
people recognise and they stay; the two dot clusters do not, because at
the 24px this renders in the sidebar five dots collapse into a smudge. A
downward arrow survives the size and says the other half of what this is.
It draws in currentColor, and the accent tile it used to sit inside is
gone - a line drawing needs its strokes apart, and a third of the box was
going to padding. dtk is DTK everywhere the product names itself.

Clicking an identity opens its history. It used to open the probe dialog
and only if a probe had already been run, so a click usually did nothing.
The drawer answers "degraded by what": a per-endpoint breakdown, because
one endpoint refusing while the others answer is a signature problem or a
sign-protected path, and every endpoint refusing is the identity itself -
opposite responses, and a flat list of thirty rows does not tell them
apart. Verified on a live identity: 30 calls to content_detail with one
payload.withheld refusal, author_posts and comments clean, and the banner
naming content_detail.

That drawer shipped asking for two keys no catalogue has, which rendered
as raw "identity.column.streak" in the middle of it. check-i18n verified
en/zh parity and never looked at a t() call site, so nothing saw it - not
tsc, not eslint, not the test suite. It scans them now, literal keys
only, skipping call sites that pass their own defaultValue. Confirmed by
reintroducing the exact typo.
2026-09-09 04:22:43 -07:00
Evil0ctal 87e4e42066 fix: implement the queue ceiling, and guard the error-code seam
Three settings-shaped defects, each the same shape: a control that reads as
configured and behaves as unconfigured.

sched.queue_max was exposed by the console and read by nothing, and
RejectReason.QUEUE_FULL was defined and never raised - so the task queue grew
without bound and callers waited instead of being told to come back. Doc 03 is
explicit that rejecting early beats queueing: a caller left hanging is worse off
than one that gets a Retry-After. Enforced now in operations.submit, after the
coalescing attempt (joining queued work adds nothing to the backlog) and exempt
for the six operator-triggered jobs, because a deep queue is exactly when
someone runs the self check.

That needed a code of its own. IDENTITY_POOL_EXHAUSTED is the only other 503
with a retry_after, and its message says no identity is available, which is not
what happened.

web/src/lib/api.ts keeps a hand-written copy of the backend ErrorCode enum, and
isErrorCode() gates on it: a code missing there is silently rewritten to
INTERNAL, so the console says "an unexpected internal error" for a condition the
server explained precisely. NOT_CONFIGURED, added earlier this session, was
already missing from it. check-i18n.mjs now compares the two in both directions.
The tone map in status.ts is a Record<ErrorCode, Tone>, so TypeScript catches
that half by itself - which is why only the array needed a guard.

Also: the console's proxy scheme list offered socks4, which
ALLOWED_SCHEMES rejects, so a pasted socks4 line previewed as valid and was then
refused on import - the preview exists to prevent exactly that.
2026-09-07 19:21:46 -07:00
Evil0ctal 52f9fc98d3 feat: negotiate the console's language, and translate what the server writes
Three asks: every page localized, a switcher in the top right, and the default
taken from the request header. The switcher was already there on all 14 routes
plus Login and Setup; the other two were not, and finding out why turned up more
than the pages themselves.

The console's language was decided entirely client-side, from navigator.language.
The API had a complete negotiator - ?lang= then Accept-Language then the
configured default - and the console never reached it, so a browser asking for
Chinese got Chinese error messages inside an English shell. index.html is now
served with the negotiated language stamped onto <html lang>, marked
data-language-source="negotiated" so the SPA can tell a real signal from the
value baked in at build time, with Vary: Accept-Language and no-store so a proxy
cannot hand one visitor's language to the next. Hashed assets stay cacheable. An
explicit choice still wins, before first paint, as it already did.

A lint rule now flags user-visible literal text in JSX - text nodes, translatable
attributes, object-literal labels, {cond ? 'a' : 'b'}. It reports zero across
web/src, which is the useful finding: the pages were already done. What it
cannot see is prose the SERVER writes and the console prints verbatim, and that
was the real gap:

* all 36 setting descriptions, ten of which had no text at all;
* the entire six-step diagnostic report, 25 outcomes;
* circuit-breaker trip reasons, shown in the landing page's red banner;
* cookie-import warnings, including the one saying the pasted jar is a live
  session - the warning that most needs to be understood;
* component health details on the System page;
* scheduler reject reasons and proxy import rejections, rendered as snake_case;
* failed-task error messages, frozen in the worker's English and preferred over
  the console's own catalogue because they were non-empty.

Each now carries a stable code plus its arguments and is rendered per reader,
following what envelope.py already did for HTTP errors. Codes stay untranslated;
so does evidence - raw driver text and upstream output are shown verbatim in a
mono block, because a user pasting them into an issue needs the original.

Also: ?lang= now works in the console and is remembered per tab rather than
persisted, so a shared link does not overwrite a returning user's choice; the
document title follows the page and the language; the query cache is invalidated
on a switch so server-rendered text already on screen refreshes; language names
are endonyms in both catalogues, checked by CI, because "Chinese" is invisible to
someone scanning for 中文; and the switcher now reaches the two full-screen
states that had no shell - the fatal network screen and the error boundary -
which are exactly the screens a user is stuck on.

One fix is a security fix, not a localization one. Moving the diagnostic report
to render from already-redacted values dropped an outer redact(str(...)) pass, so
a detail value that is neither str, dict nor list - an upstream driver error
object, the single likeliest thing to carry a live session - reached the report
verbatim. redact_value now stringifies and redacts anything it does not
recognize, scalars excepted, and the text renderer re-runs the pattern pass.
Reproduced before the fix and covered by a test.

Verified: ruff and mypy clean (147 files), 1392 unit+replay, 209 integration,
100 browser_rpc, 25/25 Docker smoke, frontend typecheck/lint/i18n. In a live
container: Accept-Language: zh -> <html lang="zh"> with a Chinese title and
shell on first paint, fr -> en, ?lang= overrides the header without persisting,
Vary and no-store present on the shell and absent from /assets, and one failed
task rendered "请求参数不合法。" or its English counterpart from the same stored row.
2026-09-07 16:24:48 -07:00
Evil0ctal cac61daf2f feat: routes, MCP, CLI, ops, worker and containers; close three integration gaps
Six modules built in parallel, then each reviewed by an independent adversarial
pass. 40 OpenAPI paths, an MCP server, a CLI, the operational features, the task
worker and a multi-stage image with production compose.

Three gaps only visible where the modules meet, all found by running the stack
rather than by reading it:

- MCP was never mounted. register_routes now calls mount(), which also chains
  the streamable-http session manager onto the application lifespan: a mounted
  ASGI app receives no lifespan events of its own, and without that the endpoint
  fails with "Task group is not initialized".
- The worker had no definition for the logical "parse" endpoint, so both
  POST /api/v1/parse and the MCP parse_url tool - the two entry points a
  first-time user is most likely to try - failed with "unknown endpoint".
  dtk.worker.parsing expands the short link, identifies the resource and maps it
  onto a registered endpoint. Expansion is the security-sensitive half: a short
  link cannot be validated before it is followed, so the target is checked
  against the allowlist after every hop and the hop count is bounded. A test
  asserts a short link redirecting to the cloud metadata address is refused.
- The console was not served. The build output is copied to /app/web/dist and
  the image already exported DTK_CONSOLE_DIR; the code simply derived its path
  from __file__, which inside the image points into site-packages. It now reads
  the variable, with fallbacks that keep a source checkout working. The SPA
  fallback declines anything under the API prefixes, so a mistyped endpoint
  still returns a JSON envelope rather than the console's HTML with status 200.

Verified against the real stack, not a mock: docker compose up, all services
healthy, migrations producing 12 tables, 3 hypertables and 2 continuous
aggregates, the setup token read from the container log, the admin created and
the endpoint then permanently closed, login, the error envelope, three rejected
SSRF shapes, both languages, OpenAPI, the docs and the console. 25 of 25.

1316 unit and replay tests pass.
2026-09-07 06:08:08 -07:00