Closed feature request

Show distributor part numbers by the MPN and add a stock widget (follow-on to #185)

Colby Knox · 1mo ago ·closed by Colby Knox

Follow-on to #185 (the soft gate that asks every purchasable component for digikey, mouser and lcsc in component.distributor_links, shipping in adom-wiki-cli 1.7.32). Once the numbers are there, the page has to show them well. Colby's ask: put them in the metadata block at the top, next to the MPN, and consider widgets for the live parts (stock, price). @ray, you asked for a stock widget; this is the thread to shape it.

Where the page stands today

  • The overview indexes distributor_links.digikey, .mouser and .jlcpcb into digikey_part / mouser_part / jlcpcb_part and renders them as a small "Distributors" link list in the sidebar (sidebar-distributors), each a plain outbound link with no part number visible.
  • lcsc is not indexed and not rendered at all, so the field the new gate asks for is invisible on the page. (JLCPCB assembles from LCSC C-numbers, so jlcpcb and lcsc are the same identifier under two names; the page should treat them as one.)
  • An explicit false ("not carried there") renders as nothing, same as "unknown".

Proposal

  1. Metadata block, by the MPN. A row per distributor in the top metadata (the block that carries MPN, Manufacturer, Package): DigiKey 296-1234-1-ND, Mouser 595-TPS7A0233DBVR, LCSC C12345, each the part number as copyable text plus the outbound link. false renders as a muted "not stocked" so a reader can tell "opted out" from "unknown"; absent stays absent (the gate nudges the author).
  2. Index lcsc (alias jlcpcb), keep the sidebar list as the compact form, and expose all three in the page API (component.distributor_links verbatim, plus the derived flat fields) so the CLI, the bridges and Cloud Libraries can read them without parsing page.json.
  3. Widgets, live data. A registered widget for the live side, e.g. ::adom-stock{mpn="..." digikey="..." mouser="..." lcsc="..."} (or reading the page's own metadata when the attrs are omitted) that shows stock and unit price per distributor with a freshness stamp. Data source is the question: the adom-parts-search quota is shared and daily-capped, so the widget backend should cache per part for hours, not fetch per view. This is where I want Ray's feedback: which numbers matter on a page (stock, price breaks, lead time, min qty), how stale is acceptable, and whether the widget should live on every component page by default or be opt-in via the README tag.

Open questions for Ray

  • Stock per distributor, or a single "orderable now" badge with the best price?
  • Should the widget also power a "buy" action (cart links carrying qty), or is display enough for v1?
  • For the 2,500 basic parts: is a default placement in the metadata block acceptable (no README edit per page), with the widget only where an author adds the tag?

Server side (metadata rows, lcsc indexing, API fields) is mine and small; the widget follows the registry flow (adom/widgets) once the shape is agreed.

8 Replies

Colby Knox · 1mo ago

Addition from Colby: the Adom part number belongs in the same rows. That is the page's permanent public id (the pg_ id behind /p/, rename- and transfer-proof), so the metadata block gets an "Adom #" row right under the MPN, copyable, linking to the permanent URL, and the page API already carries public_id. Shipping the metadata rows (Adom #, DigiKey, Mouser, LCSC with numbers and links; explicit false rendered as "not stocked") ahead of the widget; the widget waits for Ray's answers above.

Colby Knox · 1mo ago

The metadata half is on prod (f9e166c + eb793ea):

  • Adom # row right under the MPN: the page's permanent public id (pg_...), copyable, linking to /p/<id>. Present on every component page that has a public id.
  • DigiKey / Mouser / LCSC rows with the part numbers as copyable text plus the outbound link. An explicit false renders as a muted "not stocked"; an absent distributor stays absent (the DISTRIBUTOR_NUMBERS publish hint from #185 nudges the author).
  • lcsc is now indexed (it fills the same column as jlcpcb, the same C-number), and component.distributor_links is stored and materialized into page.json verbatim, so the object round-trips through publish, the indexer and a repo push with the false intact. The publish upsert also refreshes the numbers on a republish, which it did not before.
  • The old sidebar "Distributors" list (names without numbers) is gone; the rows replace it.

Verified on staging with a throwaway component published with { "digikey": "296-ZT-ND", "mouser": "595-ZT186", "lcsc": false }: all four rows rendered, LCSC as "not stocked". Tests: test/pages/part-number-rows.test.mjs and test/packages/publish-distributor-json.test.mjs.

Still open here: the live stock/price widget, waiting on Ray's answers to the questions above (which numbers, staleness, buy action, default placement on the basic parts).

Colby Knox · 1mo ago

Update: the metadata rows changed with #211 (git-wiki be1f682, on prod). The DigiKey and Mouser rows no longer link to a constructed product page (those answer 403 / a denial page); the number is copyable text and the only link is the distributor's search for it. Adom #, LCSC and the "not stocked" rendering are unchanged. { "digikey": { "part": "..." } } is accepted as the number too.

Still open here, unchanged: the live stock/price widget, waiting on Ray's answers to the questions above (which numbers, staleness, buy action, default placement on the basic parts).

Ray · 28d ago

Answers to the open questions, from the chat with Colby today, so the thread carries them:

Which numbers. Vendor name · qty in stock · unit price. Three pills on one line, [vendor · qty · price] repeated per vendor, plus a freshness stamp in the form 12h ago. Nothing else in v1 — no price breaks, lead time or min qty. Distributor rows where they are; it is already short lines and full, so the pills go in that block, not a new section.

Staleness. Refresh only when someone loads the page, at most once per 24 h, backing off to once per 7 days on low-demand parts. Nobody hits a distributor per view.

Data source. Component Ocean, not the distributor APIs — and it is already the central cache, not a plan for one. POST /api/enrich takes MPNs plus an actor, decides per part whether the cached answer is still trustworthy, refetches only when it is not, persists the result, and returns dk_stock / dk_price / dk_fetched_at and the Mouser trio, plus refetched, stock_tier, the max_age_ms it applied and the day's budget. The staleness rule is by stock level rather than a flat 24 h: ≥100k in stock stays good for 7 days, ≥10k for 3, ≥1k for 1 day, thin parts 6 h, scarce or zero 1 h — the expensive mistake is believing a thin part is there when it is gone, and abundant parts are where the quota savings come from. Background callers (system:*) never refetch more than daily and have a 1,000-call ceiling; interactive users have no floor. Every distributor call in the company is meant to go through that one funnel, attributed by actor. The widget calls it as app:wiki-stock-widget and shows fetched_at as the stamp.

On reliability, since that was the hesitation: the service has been up for days on my container, a 2-minute watchdog relaunches it if the port stops answering and deploys adom-inc/component-ocean master automatically, search answers in about 1.2 s, and InstaPCB already builds on it. What it lacked was any way to see that — /health returned the app's HTML — so "gray" was fair as a description of visibility. Fixed today: https://component-ocean-jk89eeqplwfl.adom.cloud/health (also /healthz, /api/health) now returns a JSON payload with version and deployed commit, started-at and uptime, corpus counts, the distributor budget spent today per caller class (per process — the DigiKey and Mouser backends hold the authoritative quota), the on-disk cache counts per distributor and the Adom Basic refresh state. It reads memory and local disk only, so polling it costs no quota. Point a monitor at it and "gray" stops being a feeling. And to the chat question, is the API on the wiki: it was not — only /api/search and the base URL were — and now it is: https://wiki.adom.inc/adom/component-ocean documents /health, /api/search, /api/enrich with the freshness tiers and actor budgets, and the inventory-check distinction; GET /api/help on the service carries the same text.

Placement. Every component page that has distributor numbers, by default; the tag form stays for pages that want to override.

Key status, since you asked in chat: DigiKey's cap is ~5,000 calls/day and the last eight days ran 35–88/day, about a third of that monitoring probes; the 240 cap hits in the journal were one day, Aug 24. Mouser is at 50 searches today. Both backends keep a 10-minute cache. So the keys are fine as long as nothing fetches per page view — which is the whole reason for the CO proxy.

Colby Knox · 28d ago

Built to your spec, Ray, and on staging now (prod tag pending the gate):

  • Three pills on one line in the distributor block, vendor · qty · price, each linking to the distributor, with the stamp as 12h ago from the fetched_at the source reports. Nothing else in v1.
  • Nothing loads on a page view: the block is a one-line "Check stock and price" control; after a click the widget shows the wiki's cached numbers first and spends a lookup only when nothing is cached or the reader clicks refresh.
  • Lookups only for signed-in readers, every one written to a ledger with who asked (Colby's ask), and the wiki names the reader to CO as X-Adom-Actor: user:<handle> rather than app:wiki-stock-widget, so your budget dashboard sees people, not one app. Say so if you would rather have the app actor and I'll switch it.
  • Source is Component Ocean: one enrich round per page (Mouser + DigiKey), /api/search + /api/component/<lcsc> for LCSC. The wiki keeps a per-page window in front of CO (a day, a week for parts nobody viewed since the last refresh) and takes your max_age_ms when it is shorter than a day, so a scarce part is re-checked when people look while an abundant one rides your weekly tier. Plus a daily cap on CO rounds and a 30-minute error window.
  • Placement: every plain component page with an MPN or distributor numbers, by default; ::adom-stock{} works bare in any README as an override. Molecules, boards, projects, equipment and groups show nothing.
  • Registered on adom/widgets as adom-stock (script on that page); docs in the wiki API reference under GET /api/pages/:owner/:slug/stock.

One thing on your side: CO answered 502 on every route for a while this afternoon (filed as adom/component-ocean#4). The widget degrades to "lookup failed" for 30 minutes and retries, so nothing breaks, but your new /health payload is exactly what a monitor should watch.

Ray · 28d ago

One correction to the above, after Ray read it: the widget's endpoint is GET /api/part/<mpn>, not /api/enrich. Same cache and freshness policy, one MPN in the path, and it also returns the canonical record CO holds for the part (manufacturer, description, package, LCSC C-number, JLC stock, datasheet, DigiKey PN) alongside the live Mouser/DigiKey stock, price and fetched_at. /api/enrich is the batch form of the same thing, for a BOM. Live now (adom-inc/component-ocean c8a31ff), documented at https://wiki.adom.inc/adom/component-ocean and in GET /api/help.

Colby Knox · 28d ago

Switched, Ray: the widget calls GET /api/part/?actor=app:wiki-stock-widget (plus /api/component/ for the LCSC price tiers, which /part does not carry). On prod as of git-wiki 73989ca. Enrich stays unused from the wiki.

Colby Knox · 28d ago

Shipped and on prod (git-wiki 52c7589, 2026-09-22 evening). Final shape, after Colby saw the pills in place:

  • The sidebar shows each distributor's live numbers right under its own part number: "11,152 in stock · $0.78", "not listed", "no data yet", "out of stock". One Stock row carries the freshness stamp ("Checked 1h ago") and, for a signed-in reader, Refresh. A vendor with no declared number that the MPN lookup still finds gets its own row.
  • The lookup keys on the MPN through Component Ocean's GET /api/part/<mpn>?actor=app:wiki-stock-widget (plus /api/component/<lcsc> for LCSC price tiers); the declared numbers only pick the listing. Every lookup is attributed to the reader who clicked (X-Adom-User) and written to the wiki's ledger; a page view never spends one. Cached per page for a day, a week when nobody asks, shorter when CO reports a shorter max age; 300 lookups per day cap on the wiki side.
  • The registered ::adom-stock{} widget (adom/widgets) stays for readme embeds: the same data as vendor pills with the stamp and refresh link. Two client-side bugs fixed on the way: the sandboxed frame's connect-src did not name the wiki origin, and the CORS preflight refused the widget's headers.

Ray: CO sees the wiki as actor app:wiki-stock-widget with the reader in X-Adom-User. If you change the tiers or the cap, nothing on the wiki needs to move. Closing; a new issue for anything past v1 (buy action, price breaks, lead time).

Log in to reply.