TaricHive — Product Review & Improvement Plan
2026-07-15. Principal-architect review of TaricHive against Taric Support (app bundle app.20260508.075508.js, freshly re-downloaded and mined — 923 KB, browserify/AngularJS 1.x — plus the authenticated walkthrough of 2026-07-15) and Norwegian Customs Tolltariffen (live walkthrough). Companions: taricsupport-ux-analysis.md (UI walkthrough detail) and multi-country-tariff-gap-analysis.md (data layer — its roadmap items #1–17 and #19 are DONE). This report is the product/UX/architecture layer on top of those, based on a full read of TaricHive.Web + TaricHive.Core.
1. Executive summary
Where TaricHive stands: the data foundation is now the strongest of the three products (11 jurisdictions including NO and TR, which Taric Support does not have at all; structured DutyTerm engine; point-in-time queries; import verification). The commodity detail page is genuinely competitive — evidence chips, verdict lines, reactive calculator, copy buttons and the dark-mode design system are all in place, several of them better executed than Taric Support.
Where it loses: everything around the detail page.
- You cannot browse. There is no sections → chapters → tree path anywhere; the only entry point is a search box, and the tree component on the detail page shows only the ancestor chain — never children or siblings. Both reference apps treat top-down browsing as half the product. This is the single biggest functional gap.
- Search is a
LIKE '%text%'over four description columns, ordered by code — no ranking, no autocomplete, no synonyms, no typo tolerance, no combined text+code, no multi-source hub. Taric Support searches 11 evidence sources at once and counts BTIs per heading; Tolltariffen offers direct go-to plus guided search. - Context is trapped in page state. Date, origin, active tab and language never reach the URL or any per-user store. A shared link silently loses the date/origin the sender was looking at; every new session resets language and origin. Taric Support persists all of it server-side per user (
/preferenceAPI — this is how "Remember origin" and favorites work). - The professional-workflow layer is missing: no favorites, no recent codes, no quota panel (the order-number link is a dead button), no print/export, no changes feed, no batch screening. The bundle mining shows Taric Support ships ~20 auxiliary tools around its good page; a handful of them (quota search, compliance batch jobs, correlation tables, validation of EORI/VAT numbers) map directly onto what a declarant does all day.
- Two real engineering defects: the measure query runs four collection
Includes withoutAsSplitQuery()(cartesian row explosion — the query returns Components × Conditions × Footnotes rows per measure), and the detail page issues ~9 dependent queries sequentially per navigation. Fixing both is a day of work and will visibly change page-load feel.
Recommended arc (full roadmap in §14): first make the existing page fast and shareable (URL state, split query, parallel loads, caching) — days, not weeks. Then build the two missing pillars: browsable nomenclature tree and real search. Then the declarant workflow layer (favorites/recents/quota/print/validation tab). Then differentiators Taric Support cannot match: cross-jurisdiction comparison on one screen (unique to TaricHive's unified model), a change-diff feed computed from delta imports, and AI-assisted classification grounded in the BTI/CNEN corpus already in SQL.
2. Product comparison
| Dimension | TaricHive | Taric Support | Tolltariffen (toll.no) |
|---|---|---|---|
| Audience | Internal customs specialists | Professional declarants (subscription) | Citizens + professionals (public) |
| Jurisdictions | EU, BE, NL, FR, DE, IT, PL, SE, GB, CH, NO, TR, US(HTS) | EU, DE, BE, NL, FR, GB, CH, SE, IT, US — no NO/TR | NO only |
| Data model | Unified TARIC-shaped SQL, one duty engine | Server-side normalised (inferred), thin client | Simple national model |
| Browse (sections→tree) | ✗ missing entirely | ✓ tree w/ evidence chips, collapsed ranges | ✓ the whole product is the tree |
| Search | LIKE %text%, no ranking |
11-source hub, BTI counts per heading, text+code combined | Go-to box + guided search, NO-language only |
| Autocomplete | ✗ | Partial (typeahead filters) | ✗ |
| Point-in-time | ✓ date picker (in page only) | ✓ global date, not in URL | ✗ (current only) |
| Movement toggle | ✓ in URL | ✓ global | ✓ routed tabs in overlay |
| Origin filter | Free-text ISO code input | Autocomplete + Remember + group expansion | CHOOSE COUNTRY routed view |
| Duty calculator | ✓ reactive units, trace, ≈EUR | ✓ reference implementation (incl. value calculator) | ✗ (rates only) |
| Declaration validation | Service exists, no UI | ✓ dedicated tab, hard add-code validation | n/a |
| Quota info | Imported, dead link in UI | ✓ quota search + balances + order-number links | Quota remarks in prose |
| Favorites / recents | ✗ | ✓ server-persisted | ✗ |
| Print / export | ✗ | ✓ print buttons everywhere | Browser print, clean layout |
| Changes / news feed | ✗ (deltas are imported!) | ✓ regulations news, case-law news, landing "recent changes" | ✓ annual changes page |
| Batch tools | ✗ | ✓ compliance jobs (template upload, polled status) | ✗ |
| Business-info checks | ✗ | ✓ EORI / REX / VIES / customs-decision | ✗ |
| Correlation tables | ✗ | ✓ HS2022 forward/backward | ✓ annual correlation |
| Accessibility | Mixed (dialogs good, tabs/labels weak, dense type) | Poor (H1-as-tab, coordinate-click territory) | Best in class (aria-labels everywhere, 16 px) |
| Dark mode | ✓ (only one of the three) | ✗ | ✗ |
| Multi-jurisdiction per code | ✓ flag bar on one page — unique | Re-navigate per tariff | n/a |
Positioning: Taric Support wins on breadth of evidence and workflow tooling; Tolltariffen wins on clarity and guidance; TaricHive already wins on data unification and jurisdiction coverage. The plan below is: close the browse/search/workflow gaps, keep stealing Tolltariffen's guidance patterns, and press the unified-model advantage (comparison, diffing, one calculator for 11 tariffs) that neither competitor can copy cheaply.
3. UX review
Clicks per task (task: third-country duty for wine 2204 21 09, origin CN, in BE):
- TaricHive: type code → Enter → click result → type "CN" in origin → read. 4 interactions, but the origin must be re-typed every session, and if you started from a colleague's link you inherit neither date nor origin.
- Taric Support: code in global search → result → origin remembered from last time. 2–3 interactions.
- The fix is not a new screen; it is memory: remembered origin, recent codes, favorites, URL state.
Readability / density. TaricHive is the densest of the three: base 0.875rem but nearly all content is styled 0.66–0.82rem (≈10.5–13 px). Taric Support runs ~13–14 px; Tolltariffen 16 px. For users who read tariff rows eight hours a day, tiny-but-uniform beats big-but-scrolly, yet 0.66 rem labels are below comfortable-reading thresholds and hurt the 40+ segment of the user base. Recommendation: raise the floor — nothing below 0.72 rem; body/measure rows to 0.82–0.875 rem; keep density via spacing, not glyph size. Offer a compact/comfortable density toggle next to the theme toggle (one CSS custom property).
Whitespace/typography. The paper-graphite + amber system is distinctive and calmer than Taric Support's navy Bootstrap. Two flaws: (1) heavy reliance on inline style="…" in Razor markup (dozens in TariffDetail.razor, Home.razor) undermines the design system's consistency and dark-mode correctness — move into app.css classes; (2) uppercase-letterspaced micro-labels are used both for section titles and card headers, flattening the visual hierarchy — cards need a stronger title tier.
Colour/icons. Badge semantics (duty grey, cert C/Y/N, footnote teal) mirror Taric Support's proven vocabulary — keep. But colour is the only carrier of badge meaning (accessibility §11) and the legend is hidden behind a ? button; add a one-char prefix or shape difference per badge family.
Error handling. Home.razor renders raw ex.Message on search failure — replace with a friendly message + logged detail. The Taric Support lesson (leaked i18n keys in their value-check panel) is that QA of error/empty states is what separates "professional" from "internal tool".
Empty states. Good verdict line ("Duty free commodity code.") already adopted from Tolltariffen. Extend the pattern: "No restrictions registered for this origin", "No preferential rate for {country} — third-country duty applies" instead of silently absent sections; each computed from the measure set (data is already there).
Guidance sentences (Tolltariffen's master pattern): one static localised line above the duties column — "Use the third-country duty unless a preferential rate below applies to your country of origin." Zero engineering cost, large comprehension payoff for junior declarants.
4. Information architecture review
Current shape:
/ search box only
/tariff/{src}/{code}/{movement} the workhorse (2 tabs: Measures | Calculate)
/hts/{code} US, separate world
/legislation EurLex browse
/admin/sync admin
Problems and fixes:
- Missing top of the funnel. Add
/browse/{src}(sections → chapters) and/browse/{src}/{chapter}(expandable tree). Non-declarable rows expand; declarable leaves link to the detail page — with the Taric Support behaviour of collapsing non-declarable intermediate ranges, and the Tolltariffen behaviours of open/close-all (above and below), copy button per row, and prev/next heading pager on the detail page. - Context dimensions belong to the app, not the page. (jurisdiction, movement, date, origin, language) should live in one cascading
TariffContextreflected in the URL (?date=2026-07-15&origin=CN) and (for origin/language) in per-user preferences. This is Taric Support'sapplicationStateService.filterdone properly — theirs never reaches the URL either; putting it there beats both apps and makes every screen shareable. - Grouping of measures (measure-type → duty-string → geo list, two columns) is validated by both references — keep. Within it, adopt per-source layer flags (EU flag on TARIC rows, BE flag on national rows) —
MeasureCardshows one flag per card fromMeasures.First().Source, which mislabels mixed cards; derive per row. - US/HTS is a parallel universe (
HtsDetail, own search branch, excluded from the flag bar's availability logic). Either integrate HTS into the detail-page frame (same context bar, same tree panel, measures rendered from HTS rate columns) or clearly badge it as a separate reference dataset. Half-integration confuses. - Reference pages are missing as first-class citizens: footnotes, certificates, additional codes, geographical groups (with member lists — the
1011 ERGA OMNESlink in Taric Support opens exactly this). All four tables are populated in SQL; each is an afternoon of Blazor. They also serve as link targets from measure rows (geo area → group page). - Section/chapter notes exist only as panels on the detail page; the browse pages should surface them at chapter level (chips like Taric Support's
N/Eat chapter rows).
Information grouping verdict (per the questions in the brief): primary grouping by measure category, then measure type, then identical duty string (current model, validated); secondary pivots worth adding as views, not defaults: by regulation (already in the Legislation panel — add measure counts and link back), and by effective date (the timeline, §8.6).
5. Search experience review
Current implementation (GoodsService.cs): code → StartsWith; text → Contains on 4 description columns, Take(50), ordered by source + code. Fresh bundle mining shows Taric Support's server exposes nomenclature-search/by-code, by-text, by-text-and-code, headings, search-combined, search-tree-combined — i.e. dedicated shapes per search intent, plus the 11-source hub.
Recommended target, in implementation order:
- Ranking + full-text. Add a SQL Server full-text index on the description columns (or a computed, language-concatenated search column). Rank: exact phrase > all-words > any-word; boost matches at lower indent (headings before deep leaves) and declarable leaves; group results per heading like the TS hub so 40 leaf hits under 2204 render as one expandable heading row.
- Autocomplete (typeahead under the search box, 250 ms debounce, min 3 chars): top-10 mixed code + description matches with the matched substring highlighted, keyboard navigable (
↑↓ Enter Esc). Blazor: aSearchBoxcomponent withCancellationToken-cancelled service calls; no JS needed. - Combined text+code: "2204 sparkling" → prefix-filter by code, text-filter within. Trivial once tokenised: digits-only tokens → code prefix, rest → text.
- Synonyms/aliases. A
SearchAlias(Term, CnCodePrefix, Language)table seeded from the EU alphabetical index dataset (Taric Support searches it as a source; it is published openly) plus in-house additions ("sneakers" → 6404, "laptop" → 8471 30). Check the alias table first; show alias hits as labelled suggestions ("wine ⤳ 2204"). - Typo tolerance: cheap and effective — trigram similarity on the alias + heading-description tables only (small), not on 300k leaf rows. "Did you mean tomatoes?" beats silent zero results.
- Multi-source hub (phase 2): tabs for Nomenclature | BTIs | ATaRs | Legislation | Notes, each a count-first lazy tab. BTI counts per heading ("11,479 BTIs under this heading") are classification evidence — TaricHive has 122k BTI rows to do the same.
- National-code lookup: searching a CH 8-digit or TR 12-digit code should hit that source's nomenclature directly (works today via prefix) and offer the EU equivalent via the HS6 stem — a one-line addition to result grouping.
Also fix: multi-word queries currently require the exact substring; "red wine" finds nothing unless contiguous. Tokenise before full-text lands.
6. Navigation review
- Persistent context bar: promote the flag row + date + movement + search from
TariffDetailintoMainLayout(it currently vanishes on Home/Legislation). Taric Support's single most effective structural idea is that (jurisdiction, movement, date) never leaves the screen. - Flag-only switching repeats Taric Support's learnability mistake, and emoji flags render as plain letter pairs on Windows/Chrome anyway. Use flag + short name (
🇧🇪 BE), tooltip with full name, and a visible disabled style (currentlydisabled+ title only). - Breadcrumb shows only codes — add truncated descriptions on hover/after the code (
2204 — Wine of fresh grapes…), because codes alone force a mental lookup. - Cross-reference links in descriptions ("other than that of heading 2009") — Taric Support renders these as links. Regex
(heading|subheading)\s+(\d{4}(?:\s?\d{2})?)over descriptions → link. Cheap, delightful. - Prev/next heading pager + "go directly to" input on detail page (Tolltariffen): an afternoon.
- Keyboard:
/focuses search,←/→prev/next heading,m/cswitch tabs,Esccloses panels (panels already handle Esc). Blazor: one small JS interop keydown listener at layout level. - Redirect non-declarable codes to the browse tree positioned on them (Taric Support behaviour) once the tree exists; today opening an intermediate code shows a measures page that is mostly inherited rollups without saying so — at minimum badge inherited measures with the ancestor code they attach to ("from 2204").
- Back-button safety: tab and panel state are invisible to history. Make
_taba route segment or query param, and panels routed (?panel=bti) per the Tolltariffen overlay lesson — back closes the overlay instead of leaving the page.
7. Data presentation review
Row anatomy is already the Taric Support duty-chip pattern (amount-first badge, geo code+name, footnote/cert chips, regulation popover right). Verified gaps against both references:
- Quota order numbers are a dead
<button>(MeasureCard.razor:102 — no handler). Data (GB/NO/BE/SE definitions + balances) is imported. Build the quota popover/panel: definition, validity, initial/available volume, unit, blocking/suspension status, and a critical-balance bar. This is the single most valuable dead-UI fix — quota exhaustion determines whether a declarant claims the preference at all. - Exclusions are invisible.
MeasureExcludedGeographicalAreais imported and filters correctly when an origin is chosen, but with no origin the row shows "ERGA OMNES" without "excluding US, JP…" — Taric Support renders exclusions in italics under the area. Render them (expandable if long). - Geo group membership is one click away in TS (
1011→ member list) and absent here. Popover on the geo chip listing members on the reference date, fromGeographicalAreaMemberships. - Compound duty readability: format
12 % + 30.4 EUR / 100 kg MAX 18 %on one line but with the MIN/MAX clauses visually subordinate (smaller/tintedMAX …), the way Tares verbalises and TS superscripts —DutyFormatteralready emits the string; the component should split on term kind rather than print a flat string. - Condition tables (
MeasureConditionsTable) exist — good, TS hides these behind chips too. Add the negative action text ("if not fulfilled: import prohibited") which is the part declarants actually need, and show condition-specific duty amounts (imported via condition components) as "reduced duty when C990 presented: 0 %". - Supplementary units should speak — render
NomenclatureUnitrows as words ("Litre + kg net") near the header, not only as a measure row; the calculator already derives inputs from them. - VAT presentation (Tolltariffen lesson): both standard and reduced national VAT rows with the applicable one highlighted per selected additional code, rather than an undifferentiated national-tax list.
- Stale-data banner: the release + incomplete-import chip exists. Missing: the GB-style date mismatch warning — when requested date > last dataset date for that source, banner "shown data was valid on {import date}".
TaricSyncTrackerhas the date; one comparison. - Seasonal measures: validity windows are stored but not shown per row. A small
→ 31 Decsuffix whenValidityEndwithin 12 months, and a 🍂 chip for intra-year windows (CH periods, NO seasonal rates). - Footnote text at chapter level: nomenclature-attached footnotes (TN-type) surface on the detail page, good; also chip them in the future tree per row (TS pattern
TN 701).
8. Feature gap analysis
From the fresh bundle inventory (routes + 170 components + 60 services — §10), classified for TaricHive:
Have, keep improving: commodity page, calculator (theirs adds a transaction-value sub-calculator — port later into DutyCalculatorPanel), BTI/ATaR panels (theirs have image galleries — EBTI images need a new dataset, already parked in the Tarbel backlog), legislation/regulation details, exchange rates (we have the table; they have a page — trivial to add), point-in-time.
Gap — adopt (ordered by declarant value):
- Quota search & panel (§7.1) —
/quotapage: search by order number/code, table of definitions + balances. Data present. - Favorites + recent codes — TS persists server-side via
/preference; we need aUserPreferencetable (Entra user id → JSON). Star on detail page; landing shows favorites + last 10 visited (with jurisdiction+date context). - Validate-declaration tab —
MeasureService.ValidateDeclarationAsyncexists with zero UI. Third tab; certificates checklist + hard additional-code validation like TS. - Print / export — print stylesheet +
window.print(); XLSX export of the measure set via the existing CustomsHive-style static XLSX builder pattern. - Changes feed — unique leverage: taric-opendata deltas are already imported. Compute per-release diffs (new/ended measures per chapter/code) into a
MeasureChangetable at import time; render a news page + per-code "changed on …" chip + landing-page recent changes. TS ships regulations-news; ours can be per-code and filterable by favorites — better. - Timeline visualisation (the brief's question: yes, but scoped) — on the detail page, a horizontal validity band per duty-bearing measure (start → end, coloured by category, "now" line, click = jump date picker to that day). Answers "what changes next month?" visually. Moderate effort; build after the changes feed (same data).
- Compliance batch check — TS's
/compliance/job(template upload → server job → paged status → downloadable result) is the power feature for screening a whole product list. TaricHive equivalent: paste/upload codes+origins, background job (channel worker exists) producing a grid + XLSX of duty/restrictions/certs per line. High effort, very high payoff; also the natural CustomsHive integration point. - Business-info validators — EORI (EU SOAP/REST), VIES VAT, REX: free public APIs, small forms, big trust win for declarants. Low effort each.
- Customs offices directory (COL dataset, public) — nice, later.
- Correlation tables (HS 2022→2027 transposition) — needs the WCO/EU correlation dataset in taric-opendata first; valuable each HS revision year.
- Reference pages for footnotes/certificates/additional codes/regions (§4.5).
Skip (deliberately): case-law corpus, German EZT texts, CROSS rulings, chemicals/ECICS, trade statistics, customs-value plausibility check — each needs a dataset acquisition with low payoff for the internal audience; revisit on demand.
Features neither reference has (TaricHive originals):
- Cross-jurisdiction comparison view —
/compare/{code}: one table, a column per jurisdiction (third-country duty, preference for chosen origin, VAT, restrictions verdict). Only possible on a unified model; kills the "open five tabs" workflow. This should become TaricHive's signature screen. - Per-code diff between two dates — "what changed for 2204 21 09 between Jan and Jul?" — same
MeasureChangedata, rendered as added/removed/amended rows. - Origin-optimisation hint — for a code, list origins with preferential zero/reduced rates (invert the geo-membership expansion). A sourcing question every trader asks and no tariff UI answers.
- Declaration-ready export — copy-as-CustomsHive-segment (measure set → IDMS-ish fields), leveraging the H2B mapping work.
9. Frontend architecture recommendations
Current: Blazor Server (.NET 10), interactive server render mode, component decomposition already good (17 shared components), DbContextFactory + transient services (correct for Blazor Server), design tokens in app.css.
- Introduce
TariffContext(source, movement, date, origin, language) as a cascading value owned byMainLayout, synced two-way with the query string viaNavigationManager/SupplyParameterFromQuery, persisted (origin, language, density) toUserPreference. Removes the per-page state duplication inTariffDetail,Home, and future browse pages, and is the precondition for URL shareability (§4.2). - Parallelise the detail-page load.
OnParametersSetAsyncawaits ~9 queries serially (entry, availableSources, hierarchy, measures, stat keys, app settings ×2, BTI count, ATaR count, notes ×2). WithIDbContextFactoryeach can own a context:await Task.WhenAll(...). Expect the largest perceived-latency win of any single change. Render the header + tree as soon as entry+hierarchy resolve; stream the rest (StreamRenderingor stagedStateHasChanged). - Fix the measure query shape:
GetMeasuresAsynchas four collectionIncludes in EF single-query mode → cartesian explosion (a measure with 6 components × 4 conditions × 3 footnotes materialises 72 joined rows). Add.AsSplitQuery()(or configureQuerySplittingBehavior.SplitQueryas the context default) andAsNoTracking(). - Cache the immutable: measure types, geo areas + memberships, footnote/certificate/additional-code texts, duty expressions change only at import.
IMemoryCachekeyed by(source, release)and invalidated by the sync worker on import completion. Eliminates 4 lookup queries per detail view. - Virtualise long lists (
<Virtualize>): search results, BTI panel (a heading can carry thousands), future browse tree. - Kill inline styles — move the dozens of
style="…"attributes intoapp.cssutility/component classes; they bypass the token system and complicate the dark theme. - Dead code/props cleanup:
FilterPanel._currency(never read),MeasuresView.UseUcc(plumbed, unused), quota button (§7.1),_uccfield inTariffDetail. - Keep Blazor Server — the interactivity/latency profile fits an internal LAN/VPN tool and the codebase; WebAssembly/auto modes would buy nothing but payload pain. Revisit only if external users appear.
- Component boundaries to add:
SearchBox(typeahead, reused Home + navbar),GeoAreaChip(popover w/ members),QuotaChip,ContextBar,BrowseTree(recursive, virtualised, lazy children per node — children query =WHERE Source=@s AND CnCode LIKE @prefix+'%' AND Indent = parent+1). - API project:
TaricHive.Apiexposes oneGoodsController; either grow it into the real integration surface (CustomsHive will want measures/duty-calc endpoints) or fold it until needed — half-alive projects rot.
10. Reverse-engineered behavioural observations (fresh, 2026-07-15 bundle pull)
Method: public static asset https://app.taricsupport.com/js/app.20260508.075508.js (served unauthenticated), string/structure mining only — concepts, not code, were taken.
- Full route inventory confirms the workflow-tool thesis:
/sections,/chapter/:n,/good/:code,/search/*(11 sub-searches incl. alphabetical index, WCO opinions, CCC conclusions, annotations),/bti/*,/atr/*,/quota/search,/exchange-rates,/referenceprices,/regions,/footnotes,/certificate-codes,/additional-codes,/correlation-table/*(HS2022 fwd/back + news),/customs-offices/:ref,/compliance-check/*,/transaction-value-calculator,/validate-business-info/*(EORI, REX, VIES, customs-decision),/news/*,/case-law/*,/texts-german-customs/*,/us-measures/:code,/tares-measures/:code,/trade-statistics,/dashboard,/landing. - Search API shapes:
nomenclature-search/by-code,by-text,by-text-and-code,headings, plussearch-combinedandsearch-tree-combined— evidence they special-case mixed queries and tree-scoped search server-side (§5.3). - Compliance jobs:
/compliance/job?template=,/compliance/job/status?psize=20&pindex=,/compliance/jobtypes?referencecountry=— template-driven, paginated polling, per-jurisdiction job types (§8.7 blueprint). - Favorites/recents:
favoritesServiceinitialised at boot frompreferenceService(/preferenceAPI) → server-persisted per user;recent-code-buttoncomponent alongside. Confirms per-user server storage, not localStorage (§8.2). taric-statuswidget + service: a data-currency indicator surfaced in the chrome — same intent as TaricHive's release chip, but always visible.- Pickers as a component family (country, country-region, currency, city, measure-type, order-number, preference-code, additional-code, certificate-code pickers): every filter is a reusable modal/typeahead component, not a bare input — the pattern
FilterPanelshould grow into. - Print is plain
window.print()+modal-print-button(print-styled modal content) — no PDF generation server-side; a print stylesheet suffices (§8.4). - Duty display contract unchanged from the earlier analysis: server-composed
groupedDuties[]strings; clientformatDutyonly decorates (superscripts, line breaks). Validates TaricHive's sharedDutyFormatter. - Origin resolution (
hasCountry): region groups expanded client-side from acountryData.regionsmap minus per-measureexcluded_regions— TaricHive now does the same in SQL (GetMeasuresAsync), correctly. - No client caching/debounce of note: AngularJS
$httpwithout a cache layer; state re-fetched wholesale on any filter change (their top-bar model). TaricHive can afford smarter granularity (only measures reload on origin change — already the case).
11. Accessibility review
Tolltariffen is the benchmark; TaricHive should beat Taric Support easily (their tabs are H1[ng-click]).
Present and good: SlidePanel = role="dialog" + aria-label + aria-hidden + Esc; buttons are <button>s; cert chips set aria-expanded; lang semantics via language picker; dark mode.
Fix list (ordered):
1. Dialogs lack focus management — on open, focus the panel (tabindex="-1" exists; call .focus() via a FocusAsync ref); trap Tab inside; restore focus to the trigger on close. Without this, keyboard/AT users never enter the panel.
2. Tabs (th-sub-tab) → role="tablist"/"tab" + aria-selected + arrow-key movement.
3. Icon-only buttons (copy, search, clear, close) need aria-label, not just title (titles are mouse-only). Tolltariffen labels every icon.
4. Colour-only badge semantics (§3) — add textual/shape redundancy; verify amber-on-paper and the 0.66 rem muted text against WCAG AA (several --text-3 on --bg-2 combos look borderline).
5. Type floor 0.72 rem minimum (§3).
6. Emoji flags are announced erratically by screen readers and render as letters on Windows — pair with text (§6) and aria-hidden the emoji.
7. Skip link to main content; aria-live="polite" on search-result count and calculator result so updates are announced.
8. Date input and origin input lack associated <label> elements (title only).
12. Performance opportunities
AsSplitQueryonGetMeasuresAsync(§9.3) — the top data-layer fix.- Parallel detail-page loads + streaming (§9.2) — the top perceived-latency fix.
GetAvailableSourcesAsyncon every navigation runsLIKE code%+ ancestorINover all sources' nomenclature. Cache per(code, date)for minutes, or maintain a tinySourceCodeCoveragesummary table at import (source, 2-digit chapter, min/max code) to answer availability without touching the big table.- Lookup caching (§9.4).
SearchHtsByCodeAsyncdoesh.HtsNumber.Replace(".", "").StartsWith(...)— unsargable, full scan. Store a normalisedHtsNumberDigitscomputed+indexed column.- Full-text index doubles as the search-perf fix (
LIKE '%x%'scans ~300k rows × 4 columns today). - Indexes from the gap analysis §5.8 (covering index on
MeasureComponents(MeasureId) INCLUDE(...),Measures(Source, OrderNumber)) — apply and measure. - Virtualize (§9.5) to keep render trees flat on 1,000-row lists.
- SignalR payloads: staged rendering means smaller diffs; also gate
StateHasChangedstorms while typing in filter inputs (@bind:event="oninput"on Home's search re-renders per keystroke — bind onchangeuntil typeahead lands, then debounce).
13. AI-powered enhancements
Direction (decided 2026-07-15): MCP-first, not embedded AI. Rather than TaricHive calling the Claude API itself (per-token cost on the app, key management, one fixed UX), expose the tariff database as an MCP server — tools like search_nomenclature, get_measures(code, source, origin, date), calculate_duty, find_btis, get_notes, compare_sources — so each specialist points their own Claude (Desktop/Code, their subscription) at it. The grounding corpus stays the moat; the LLM cost and model choice move to the user. A thin C# MCP server (stdio or SSE) over the existing Core.Services is days, not weeks, and every use case below works through it as prompts/skills instead of app features. Embedded in-app AI (e.g. the explain-this-page button) can come later for users without a Claude seat, reusing the same tool layer. The list below is therefore the use-case catalogue the MCP tools must serve:
Grounding assets already in SQL: 122k BTIs (classification precedents with justifications), explanatory notes (CNEN), section/chapter legal notes, alphabetical-index-style descriptions, measure/footnote/regulation texts, EurLex acts. This is a classification-evidence corpus competitors would kill for. Ordered by value/effort:
- AI classification assistant — user describes goods ("frozen boneless chicken thighs, marinated"); pipeline: embedding/full-text retrieve candidate headings + similar BTIs + governing notes → Claude ranks candidate codes and cites BTI numbers and note clauses; UI presents candidates as links with evidence chips, never as an oracle. This directly attacks the hardest daily task (classification) and is defensible because the evidence is in-house.
- Plain-language measure explanation — "Explain this page": one prompt over the rendered measure set → "From China you pay 32 €/hl unless you present certificate C014…; the quota 091512 is 83 % used." Cache per (code, origin, date, release). Junior-declarant training value is enormous.
- Ask-the-notes Q&A — RAG over CNEN/legal notes/BTI texts scoped to the current chapter, with citations; the panel data is already fetched.
- Natural-language search rewrite — fallback when lexical search returns few/zero hits: LLM maps the query to candidate headings/synonyms (feeding the alias table over time — human-approved aliases become permanent, so AI cost decays).
- Change summarisation — weekly digest of the
MeasureChangefeed filtered to the user's favorites ("2 of your codes gained anti-dumping duties"). - Duty-calc sanity narrative — explain the calculation trace in words, flag suspicious inputs (net > gross weight, value/kg far from typical) — the useful core of TS's broken customs-value check without needing their statistics dataset.
Implementation notes: via MCP, each use case is a skill/prompt over the tool layer — strict grounding ("answer only from tool results; cite BTI numbers and note clauses") lives in the skill text. If an embedded in-app phase comes later, it reuses the same tool layer behind a TaricAiService (claude-sonnet-5 interactive, batch for digests), labelled and feature-flagged. Nothing here blocks or is blocked by the non-AI roadmap.
14. Prioritised implementation roadmap
Critical — make what exists correct, fast, shareable (≈1 week total)
| # | Item | Why | Effort |
|---|---|---|---|
| C1 | AsSplitQuery + AsNoTracking on measure query; HTS normalised column; lookup caching |
Row-explosion + repeated lookups dominate page cost | 0.5–1 d |
| C2 | Parallelise + stream detail-page loads | Biggest perceived-speed win | 0.5–1 d |
| C3 | URL state: date/origin/tab in query string; TariffContext cascade |
Shareable links; beats both references; unblocks everything context-dependent | 1–2 d |
| C4 | Quota panel behind order-number links (data already imported) | Dead button today; quota status decides real declarations | 1–2 d |
| C5 | Origin country picker (autocomplete, names+flags, remember per user) + UserPreference table |
Highest-frequency input, currently raw ISO text | 1–2 d |
| C6 | Friendly search errors; remove dead currency filter or wire it to ≈-display currency | Polish that reads as reliability | 0.5 d |
High — the two missing pillars + core workflow — ALL DONE 2026-07-15
| # | Item | Delivered as |
|---|---|---|
| H1 | ~~Browse + tree + heading pager~~ Done | /browse/{src} sections→chapters (note-based grouping), /browse/{src}/{chapter} virtualised indent tree w/ open/close-all + go-to + chapter pager + N-notes; ‹heading› pager on detail page |
| H2 | ~~Search v2~~ Done (ranked LIKE, not FTS) | SearchRankedAsync (tokenised AND-match, phrase/EN/shallow/declarable ranking, mixed text+code), heading-grouped results on Home, debounced typeahead SearchBox (global, keyboard-navigable) |
| H3 | ~~Favorites + recents~~ Done | UserListService (JSON in UserPreferences), star on detail page, auto-recents, landing lists on Home |
| H4 | ~~Validate-declaration tab~~ Done | Third tab; certificate checklist from the code's conditions, per-measure pass/fail + fail reasons |
| H5 | ~~Exclusions, geo popover, source flags~~ Done | Italic exclusion lists per row (grouping-exempt), group-area member popover, union-vs-national layer flags per card |
| H6 | ~~Context/search shell + a11y~~ Done | Global typeahead in navbar (+/ shortcut), dialog focus save/restore, tablist roles + arrow keys, aria-labels/pressed |
| H7 | ~~Print + XLSX export~~ Done | @media print sheet, print button, dependency-free XlsxWriter + /export/measures endpoint |
| H8 | ~~Stale-data banner~~ Done | Requested date vs release last-updated timestamp comparison |
Not carried over from H-tier scope: SQL full-text (docker image lacks FTS — ranked LIKE is the safe equivalent at current data size) and moving the full (flags/date/movement) bar into the layout (deferred until the browse/detail context model settles).
Medium — differentiation (≈3–4 weeks)
| # | Item | Why | Effort |
|---|---|---|---|
| M1 | MeasureChange diffs at import + changes feed + per-code history chip |
Unique: per-code, favorite-filtered change tracking | 4–5 d |
| M2 | Cross-jurisdiction comparison view /compare/{code} |
The unified-model signature feature | 3–4 d |
| M3 | Measure-validity timeline on detail page | Visual point-in-time; reuses M1 data | 2–3 d |
| M4 | MCP server over Core.Services (search/measures/duty-calc/BTI/notes tools) — users bring their own Claude; embedded AI deferred | Attacks classification via the user's own AI seat; zero per-token cost for TaricHive | 3–5 d |
| M5 | Search hub tabs (BTI/ATaR/legislation counts per heading) | Evidence-quantified search | 3–4 d |
| M6 | EORI/VIES/REX validators | Cheap trust wins | 1–2 d |
| M7 | Multi-word/typo tolerance + alias table seeded from alphabetical index | Search maturity | 2–3 d |
| M8 | Reference pages: footnotes, certificates, additional codes, regions | Link targets + lookup value | 2–3 d |
| M9 | Density toggle + type-floor raise + inline-style purge | Readability at scale | 2 d |
Low — later bets
| # | Item | Notes |
|---|---|---|
| L1 | Compliance batch screening (paste/upload list → background job → grid/XLSX) | High effort; do after H-tier proves daily use; CustomsHive integration point |
| L2 | Transaction-value calculator; VAT-highlighting per add-code | Calculator maturity |
| L3 | Correlation tables (HS revisions) | Needs new dataset in taric-opendata |
| L4 | HTS integration into the main detail frame | Or explicitly badge as separate |
| L5 | Customs-offices directory; origin-optimisation view; declaration-ready export; AI digests/Q&A | Originals; sequence by user pull |
Explicit non-goals: case law, CROSS, ECICS, trade statistics, German EZT texts, customs-value plausibility check (dataset-heavy, low internal payoff).
Verification sources: full read of TaricHive.Web components/pages/Program.cs and TaricHive.Core services at commit state 2026-07-15; fresh bundle mining (routes, components, services, API paths) from the public app.20260508.075508.js; live-walkthrough evidence from the two companion docs. No proprietary code was copied; all reference-app material is behavioural inference.