Every limit the fetcher was using was a constant. They are settings now, and
CollectOptions became the single place policy lives: AppSettings.ToCollectOptions
clamps them, and the HTTP layer's FetchOptions is projected from that. One
clamping site rather than two sets of ceilings drifting apart.
Clamping rather than validating, for the reason the proxy options already do it:
a hand-edited file must not stop the app from starting. A MaxItemBytes edited to
zero would otherwise refuse everything, and a zeroed concurrency would deadlock
the run outright - so both are pulled into range instead. An empty format filter
is read as "everything", because switching every format off is far more likely
to be a slip than an instruction to collect nothing.
The media root has an ordering problem - it is a setting that decides the paths
the container is built from - so the file is read once before the container
exists rather than making every path lazy for one value.
Purge is scoped to a source and lives on the Collect page, where the source is
already chosen. Content another source also holds survives, which is what the
index's reference count was for.
The showcase hint says out loud what a hard link means: editing the browsable
copy edits the original, and deleting it frees nothing until the last name goes.
That is surprising enough to belong in the UI rather than only in the code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The collector becomes usable. The page is a close copy of the parse page's
shape - same proxy gate, same batched flush to the observable collection, same
truncation cap that reports rather than truncates silently - because that shape
was built for streaming outcomes and the collector produces exactly those.
Two differences that are not cosmetic. The batch drops from 512 to 64: items
arrive at network speed, roughly one a second, and a batch of five hundred would
mean the list never visibly moved. And progress is explicitly indeterminate
until a source finishes listing, because a paginated listing genuinely does not
know its total until the last page - a bar pretending otherwise would be lying.
The input swaps shape with the source: an endpoint source wants one address, a
pasted-list source wants many lines. The proxy gate is unchanged in substance -
only network sources are gated, and the banner keeps its x:Name because the
headless tests find it that way.
Rendering the page caught a defect the tests could not: IsVisible sat on the
caption inside the header border rather than on the border, so hiding the text
left its padding and divider behind as an empty bar above the input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pool now warms up from what the previous run learned instead of starting
cold every launch. Startup probes the remembered proxies first, stops as soon
as ProxyMinimumLive of them answer, and writes the survivors to
proxies.state.json after the warm-up and again on shutdown. Only proxies that
ever answered are stored: the feed republishes a few thousand dead addresses
every five minutes, and "was dead an hour ago" says almost nothing.
Remembered state is a hint, not a verdict. A restored proxy sorts first in the
warm-up queue but is not counted live until it answers in this session -
otherwise a launch a week later would report live proxies it had never spoken
to, the warm-up would skip the very entries it exists to re-check, and the
parser gate would open on week-old evidence.
That gate is the other half: a parser declaring RequiresNetwork will not run
while the pool has nothing live. The Parse page disables the run button and
shows a banner that leads to the Proxies page. Parsers that work on pasted text
are never gated - they have nothing to route, and blocking them would make the
app useless whenever the public lists are down. Two new settings cover the
escape hatch and the target: "allow network parsers without a proxy" and how
many live proxies to find at startup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Strings move into Localization/Strings.resx plus a Russian satellite. XAML uses
a {l:Loc Key} markup extension that yields a binding through the localizer's
indexer rather than a resolved string, so changing the language raises
PropertyChanged for the indexer and every caption in the app re-reads at once.
Resolving strings once at load would have been simpler and would have needed an
app restart to take effect.
Three places where this is more than a string swap:
Russian has three plural forms, so counted messages are assembled from .One /
.Few / .Many keys via Localizer.Plural instead of "{0} records" with an English
plural glued on. "1 запись", "3 записи", "7 записей".
Enum captions in pickers go through LocalizedOption<T>. A value converter would
resolve the caption once and never notice a language change; the wrapper keeps
identity on the enum value so the selection survives, while the label follows
the localizer. It needed a non-generic base for DataTemplate x:DataType, since
Avalonia 12 compiles bindings by default and cannot infer one for an open
generic.
Text originating in the domain would otherwise have stayed English under a
Russian UI — the screenshots showed exactly that. AvParser.Core still knows
nothing about languages: ParseError now carries a Code and Arguments, and the UI
translates Parse.Error.{Code} with a fallback to the English message. Parser
names work the same way (Parser.{id}.Name falling back to DisplayName), which
keeps "add a parser = one registration line" true — an untranslated parser shows
its own name rather than a missing-key marker.
Both .resx files are generated from one table so a key cannot exist in one and
be missing from the other, and the tests assert that, plus no blank translations
and identical {0} placeholder sets — a translation that drops a placeholder
throws at runtime rather than merely reading oddly. A headless test switches
language on a live shell and asserts the rendered text changes without the tree
being rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>