Skip to content

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.

  1. 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.
  2. 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.
  3. 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 (/preference API — this is how "Remember origin" and favorites work).
  4. 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.
  5. Two real engineering defects: the measure query runs four collection Includes without AsSplitQuery() (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:

  1. 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.
  2. Context dimensions belong to the app, not the page. (jurisdiction, movement, date, origin, language) should live in one cascading TariffContext reflected in the URL (?date=2026-07-15&origin=CN) and (for origin/language) in per-user preferences. This is Taric Support's applicationStateService.filter done properly — theirs never reaches the URL either; putting it there beats both apps and makes every screen shareable.
  3. 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) — MeasureCard shows one flag per card from Measures.First().Source, which mislabels mixed cards; derive per row.
  4. 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.
  5. Reference pages are missing as first-class citizens: footnotes, certificates, additional codes, geographical groups (with member lists — the 1011 ERGA OMNES link 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).
  6. 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/E at 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:

  1. 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.
  2. 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: a SearchBox component with CancellationToken-cancelled service calls; no JS needed.
  3. Combined text+code: "2204 sparkling" → prefix-filter by code, text-filter within. Trivial once tokenised: digits-only tokens → code prefix, rest → text.
  4. 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").
  5. 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.
  6. 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.
  7. 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 TariffDetail into MainLayout (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 (currently disabled + 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/c switch tabs, Esc closes 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 _tab a 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:

  1. 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.
  2. Exclusions are invisible. MeasureExcludedGeographicalArea is 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).
  3. 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, from GeographicalAreaMemberships.
  4. Compound duty readability: format 12 % + 30.4 EUR / 100 kg MAX 18 % on one line but with the MIN/MAX clauses visually subordinate (smaller/tinted MAX …), the way Tares verbalises and TS superscripts — DutyFormatter already emits the string; the component should split on term kind rather than print a flat string.
  5. 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 %".
  6. Supplementary units should speak — render NomenclatureUnit rows as words ("Litre + kg net") near the header, not only as a measure row; the calculator already derives inputs from them.
  7. 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.
  8. 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}". TaricSyncTracker has the date; one comparison.
  9. Seasonal measures: validity windows are stored but not shown per row. A small → 31 Dec suffix when ValidityEnd within 12 months, and a 🍂 chip for intra-year windows (CH periods, NO seasonal rates).
  10. 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):

  1. Quota search & panel (§7.1) — /quota page: search by order number/code, table of definitions + balances. Data present.
  2. Favorites + recent codes — TS persists server-side via /preference; we need a UserPreference table (Entra user id → JSON). Star on detail page; landing shows favorites + last 10 visited (with jurisdiction+date context).
  3. Validate-declaration tabMeasureService.ValidateDeclarationAsync exists with zero UI. Third tab; certificates checklist + hard additional-code validation like TS.
  4. Print / export — print stylesheet + window.print(); XLSX export of the measure set via the existing CustomsHive-style static XLSX builder pattern.
  5. Changes feed — unique leverage: taric-opendata deltas are already imported. Compute per-release diffs (new/ended measures per chapter/code) into a MeasureChange table 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.
  6. 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).
  7. 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.
  8. Business-info validators — EORI (EU SOAP/REST), VIES VAT, REX: free public APIs, small forms, big trust win for declarants. Low effort each.
  9. Customs offices directory (COL dataset, public) — nice, later.
  10. Correlation tables (HS 2022→2027 transposition) — needs the WCO/EU correlation dataset in taric-opendata first; valuable each HS revision year.
  11. 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 MeasureChange data, 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.

  1. Introduce TariffContext (source, movement, date, origin, language) as a cascading value owned by MainLayout, synced two-way with the query string via NavigationManager/SupplyParameterFromQuery, persisted (origin, language, density) to UserPreference. Removes the per-page state duplication in TariffDetail, Home, and future browse pages, and is the precondition for URL shareability (§4.2).
  2. Parallelise the detail-page load. OnParametersSetAsync awaits ~9 queries serially (entry, availableSources, hierarchy, measures, stat keys, app settings ×2, BTI count, ATaR count, notes ×2). With IDbContextFactory each 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 (StreamRendering or staged StateHasChanged).
  3. Fix the measure query shape: GetMeasuresAsync has four collection Includes in EF single-query mode → cartesian explosion (a measure with 6 components × 4 conditions × 3 footnotes materialises 72 joined rows). Add .AsSplitQuery() (or configure QuerySplittingBehavior.SplitQuery as the context default) and AsNoTracking().
  4. Cache the immutable: measure types, geo areas + memberships, footnote/certificate/additional-code texts, duty expressions change only at import. IMemoryCache keyed by (source, release) and invalidated by the sync worker on import completion. Eliminates 4 lookup queries per detail view.
  5. Virtualise long lists (<Virtualize>): search results, BTI panel (a heading can carry thousands), future browse tree.
  6. Kill inline styles — move the dozens of style="…" attributes into app.css utility/component classes; they bypass the token system and complicate the dark theme.
  7. Dead code/props cleanup: FilterPanel._currency (never read), MeasuresView.UseUcc (plumbed, unused), quota button (§7.1), _ucc field in TariffDetail.
  8. 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.
  9. 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).
  10. API project: TaricHive.Api exposes one GoodsController; 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, plus search-combined and search-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: favoritesService initialised at boot from preferenceService (/preference API) → server-persisted per user; recent-code-button component alongside. Confirms per-user server storage, not localStorage (§8.2).
  • taric-status widget + 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 FilterPanel should 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; client formatDuty only decorates (superscripts, line breaks). Validates TaricHive's shared DutyFormatter.
  • Origin resolution (hasCountry): region groups expanded client-side from a countryData.regions map minus per-measure excluded_regions — TaricHive now does the same in SQL (GetMeasuresAsync), correctly.
  • No client caching/debounce of note: AngularJS $http without 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

  1. AsSplitQuery on GetMeasuresAsync (§9.3) — the top data-layer fix.
  2. Parallel detail-page loads + streaming (§9.2) — the top perceived-latency fix.
  3. GetAvailableSourcesAsync on every navigation runs LIKE code% + ancestor IN over all sources' nomenclature. Cache per (code, date) for minutes, or maintain a tiny SourceCodeCoverage summary table at import (source, 2-digit chapter, min/max code) to answer availability without touching the big table.
  4. Lookup caching (§9.4).
  5. SearchHtsByCodeAsync does h.HtsNumber.Replace(".", "").StartsWith(...) — unsargable, full scan. Store a normalised HtsNumberDigits computed+indexed column.
  6. Full-text index doubles as the search-perf fix (LIKE '%x%' scans ~300k rows × 4 columns today).
  7. Indexes from the gap analysis §5.8 (covering index on MeasureComponents(MeasureId) INCLUDE(...), Measures(Source, OrderNumber)) — apply and measure.
  8. Virtualize (§9.5) to keep render trees flat on 1,000-row lists.
  9. SignalR payloads: staged rendering means smaller diffs; also gate StateHasChanged storms while typing in filter inputs (@bind:event="oninput" on Home's search re-renders per keystroke — bind on change until 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. Change summarisation — weekly digest of the MeasureChange feed filtered to the user's favorites ("2 of your codes gained anti-dumping duties").
  6. 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.