506 Commits

Author SHA1 Message Date
Rene Fichtmueller
8d11a4d24d feat(monitoring): Fleet-Crash-Loop-Watchdog — Pi-Heartbeat (fleet_health) + Erik-Detector (v_fleet_health) schließt den Self-Healing-Blindspot (Crash-Loop tarnt sich als online) 2026-07-07 23:02:20 +02:00
Rene Fichtmueller
4806cdfd1c feat(scraper): 100G+ zuerst in Ziel-Auswahl (fs-com Detail/Discovery + verify-product-page-prices + resolve-price-availability) — Lupo-Fokus 2026-07-07 22:10:01 +02:00
Rene Fichtmueller
86d1531d52 fix(security): scrub hardcoded DB credentials, untrack training data, add scan allowlist
- Replace plaintext tip (transceiver_db) and llm_gateway DB passwords with
  env-var references guarded by fail-fast checks, across 15 scripts + sql runbook.
  Covers quoted exports, unquoted psql calls, python env dicts and
  env-or-hardcoded-default fallbacks.
- Untrack training-data/*.jsonl (~48MB, kept on disk) and gitignore *.jsonl.
- Add .security-scan-allowlist for legit false positives: vendor name flexoptix,
  own homelab infra, sibling-project codenames in the sync journal, env-resolved
  DB URIs, example placeholders, ML tokenizer tokens.
- Pre-push leak scanner goes from 397 to 0 findings, green without --no-verify.

Follow-up (not in this commit): rotate the leaked values (llm_gateway first,
tip fleet-wide coordinated); git history still holds the old passwords.
2026-07-07 12:06:07 +02:00
root
3e0750eb3c feat(dqo): deploy tip-dqo self-heal orchestrator
- escalation-sink: add NTFY_TOKEN Bearer auth header support
- self-heal-guards: pass max_load from GuardConfig to isLoadAcceptable
- control-loop: include MAX_LOAD in guardConfig so DQO_MAX_LOAD env is respected
2026-07-07 09:47:47 +00:00
201f5d3a47 fix(dqo): route vendor pricing/stock to pi-fetch (residential Pi cluster)
The Adapter (sql/129) seeded all vendor sources as erik-safe, but TIP data is
fetched by the 3 residential Pi 5 workers (tashi/portia/ester), all now
Playwright-capable — not Erik (datacenter IP, blocked by FS.com/ATGBICS).
14 sources -> pi-fetch (fixes gbics/sfpcables never-dispatching under erik-safe).
Config only; loop stays in Plan-Mode. 10gtek stays proxmox-heavy.
2026-07-06 19:45:13 +00:00
Rene Fichtmueller
332319975a Merge branch 'main' of http://192.168.178.196:3000/rene/transceiver-db 2026-07-06 11:50:04 +02:00
Rene Fichtmueller
60cffb3585 feat(tip): autonomous data-quality orchestrator (tip-dqo, plan-mode)
Close the missing feedback loop. Detection (v_scraper_health, v_data_quality_alerts,
check_switch_quality) was accurate but inert; dispatch was blind fixed-cadence
(boss.send() is never called from any health path). tip-dqo is a separate, read-mostly
control plane that closes DETECT -> PRIORITIZE -> DISPATCH -> VERIFY -> RECHECK against
observation-table freshness (never pg-boss job state), with DB-enforced guardrails.

- sql/129: source_registry (signal->queue map + per-source SLA + guardrail contract +
  global kill-switch), self_heal_dispatch append-only ledger, scraper_run_log,
  acknowledged_exceptions, v_dqo_targets ranked view.
- orchestrator/: control-loop, self-heal-guards (pure, unit-tested), escalation-sink
  (ntfy + no-op fallback), index entrypoint. Reuses the verification-robots dispatch
  idiom (extracted shared dispatchQueues) and the quality views as-is.
- Guardrails: kill-switch, plan-mode default, per-source cooldown/budget/backoff/
  circuit-breaker, load-guard, erik-safe profile cap, delta-gated escalation. Root-cause
  routing escalates code_fix/blocked (e.g. ATGBICS silent stock-write break) to a human
  instead of looping retries; only staleness is auto-dispatched.
- security: remove hardcoded 'tip_dev_2026' DB password fallback across 6 sites ->
  fail-closed requireDbPassword() / dbConnectionString().

SELF_HEAL_PLAN_ONLY defaults true (dispatches nothing; writes plan rows for review).
Verified against the live DB: detects ATGBICS-stock dead/54d -> escalate, Flexoptix-stock
stale/30d -> plan-dispatch; guards + ledger correct; 8/8 guard unit tests pass.
2026-07-06 11:38:19 +02:00
root
6e53cd4568 feat(data-quality): fix malformed URLs + datasheet-enrichment worklist
- fn_strip_double_url_prefix + one-time fix for 436 rows whose scraped URLs had a
  duplicated host prefix (https://host.comhttps://host.com/x) -> unfetchable.
  Ingest guard trg_transceiver_url_normalize prevents recurrence.
- v_datasheet_enrichment_worklist: pipeline input listing every switch/transceiver
  needing a field filled from source, with its best source URL + type.
2026-07-06 09:27:26 +00:00
Rene Fichtmueller
d9c37af642 fix(security): remove hardcoded DB password from run-atgbics-mac.sh
The script hardcoded the pre-rotation POSTGRES_PASSWORD (retired 2026-06-04,
already invalid). Source the current password from ~/.tip/.env (git-ignored,
exported) instead — same pattern as the FS.com residential runner.
2026-07-06 10:16:27 +02:00
root
b45027aa57 fix(news): guard against future-dated published_at
One article (The Next Platform) had published_at 2026-07-15 while scraped
2026-06-19 -- an article cannot be published after it was scraped. One-time
clamp to the scrape date + BEFORE INSERT/UPDATE trigger to prevent recurrence.
2026-07-06 07:47:00 +00:00
root
41e98ca5df fix(procurement): exclude speed=0 signals + consistent speed formatting
Reconciles Stock/Health working-tree WIP uncommitted since 2026-07-04:
- supply-squeeze: exclude speed_gbps=0 / unclassified speed_tier=0 from
  price-momentum and AI-cluster-demand signals
- dashboard: render transceiver speed via fmtSpd() (1600 -> 1.6T) in
  reorder-top, switch-compat, supply-squeeze, dead-stock-revival tables
2026-07-06 07:44:34 +00:00
root
479a0dbe56 feat(switch-db): durable guard + validator for switch/compat quality
- trg_compat_speed_guard: BEFORE INSERT/UPDATE on compatibility forces
  algorithmic (spec_match/community) rows whose transceiver is faster than the
  switch max port speed to status=incompatible. Curated rows never touched.
  Makes the "no physically-impossible compatibility" invariant structural, so
  it survives a scraper regression.
- check_switch_quality(): on-demand proof; CI gate is
  `select bool_and(pass) from check_switch_quality() where enforced` (must be
  true). Data-source gaps (coding coverage, speed<=0, datasheet) tracked as
  enforced=false so they do not fail the gate before the data exists.
2026-07-06 06:58:25 +00:00
root
98d0946975 fix(switch-db): eliminate physically-impossible compatibility rows + validation layer
Root cause: the form-factor fallback in flexoptix-compat matched transceivers by
form factor only, ignoring speed, so a 400G OSFP port matched every OSFP module
incl. 1.6T (5686+ impossible rows across 266 switches).

- flexoptix-compat: fallback now filters transceiver speed <= switch max port
  speed (derived from ports_config + stored max_speed_gbps). Prevents regeneration.
- sql/125: evidence-based effective-ceiling (stored/ports/curated) views;
  flag 5811 impossible spec_match rows status=incompatible (curated rows never
  touched, understated switches never false-flagged); backfill max_speed_gbps for
  327 switches from ports_config; re-derive speed for 306 speed<=0 transceivers
  (form-factor capped, high-confidence only). v_switch_quality_alerts is the task
  list; v_switch_understated_ports surfaces switches needing datasheet enrichment.
2026-07-06 06:48:16 +00:00
Rene Fichtmueller
414ea65fee fix(db): rekey stock_extraction_empty alert to universal quantity signal (124)
Reading packages/scraper showed QSFPTEK/ATGBICS populate quantity_available
100%; they simply lack FS.com-only units_sold/warehouse-split. The 123 alert
keyed on those FS.com fields and false-positived QSFPTEK. Rekey to
quantity_available OR warehouse_qty so the alert list drains to empty; surface
pct_qty_available/pct_vendor_ts in v_stock_completeness.
2026-07-06 05:49:39 +00:00
Rene Fichtmueller
0a586e0384 feat(db): add data-quality monitoring views (123)
Read-only monitoring layer from the 2026-07-05 data-quality audit:
- v_scraper_health: freshness + breadth per (source vendor, obs table)
- v_stock_completeness: pct of stock fields actually extracted
- v_coverage: catalog composition + reach_label completeness
- v_data_quality_alerts: actionable open-issue list (drains to empty)
2026-07-06 05:43:17 +00:00
Rene Fichtmueller
4299e24e13 fix(scraper): parse FS.com delivery dates + lead_time; fix units_sold K/M inflation
FS.com renders per-warehouse availability as a bare word-date on each line
("2 Stk. im DE-Lager, 8. Juli 2026 Lieferbar"). The old regex only matched a
"Lieferung:"/"erwartet:" prefix or numeric dd.mm.yyyy, so warehouse_de/global
delivery_date + backorder_estimated_date were 0% populated across 85k rows, and
lead_time_days was never written at all.

- new fs-com-parse.ts: pure, unit-tested parsers (13 node:test cases).
- extractDeliveryDate finds the bare word/numeric date per warehouse, bounded to
  its own line so a date never bleeds to a neighbouring warehouse.
- computeLeadTimeDays = min(delivery_date - today), floored at 0; threaded into
  upsertStockObservation (now writes lead_time_days) and upsertPriceObservation.
- parseGermanQty: K/M abbreviations now treat '.' as a decimal ("4.8K"=4800,
  "1.4M"=1.4M) instead of stripping it (was "1.4M"->14,000,000 — the outlier).
- units_sold sanity gate at ingest (FS_MAX_UNITS_SOLD, default 2,000,000).
- sql/122: purge impossible historical units_sold, re-document columns as collected.
2026-07-04 22:37:05 +02:00
root
dd2c917f05 feat(data-quality): product_type classification + form_factor recovery + lead_time docs
sql/119: product_type enum (module|dac|aoc|aec|switch|nic|accessory) computed by
trigger trg_transceivers_biu from part_number/category/reach on every insert/update.
Backfilled 41008 rows (36284 module, 2791 dac, 1835 aoc, 29 aec, 27 accessory,
26 switch, 16 nic). Cables and switches/NICs no longer counted as modules;
BASE-T/RJ45 copper modules protected from cable demotion; AOC/DAC keyword beats
the optical-token guard. Consumers filter product_type=module.

sql/120: conservative form_factor recovery. Corrects only provably-wrong rows
(form_factors.max_speed < speed_gbps) via an explicit title token; 87 fixed
(e.g. 400G cables mislabelled SFP+ to QSFP-DD), 947 opaque rows left untouched
(never guessed). Scraped original preserved in new column form_factor_raw.

sql/121: document lead_time_days as not-collected (0 percent populated on all
time-series tables) so no consumer fabricates a default.

scraper/utils/db.ts: note that product_type/form_factor are trigger-maintained.
2026-07-04 16:05:28 +00:00
Rene Fichtmueller
7c9bd10941 feat(scraper): worker-only entry for horizontal scaling (Raspberry Pi nodes) 2026-06-11 15:28:11 +00:00
Rene Fichtmueller
1ba598e7ae feat(dashboard): availability-by-speed panel in Supply Squeeze (class + price trend + why per tier) 2026-06-11 10:35:37 +00:00
Rene Fichtmueller
3cbfabbb38 feat(procurement): availability why-attribution + per-SKU price momentum (1.6T: 2 suppliers, +13.7% same-SKU, 0% stock) 2026-06-11 10:22:30 +00:00
Rene Fichtmueller
c6b5bcf844 feat(procurement): transceiver search box — find a part, click for prices/suppliers/availability/price-chart 2026-06-11 10:12:01 +00:00
Rene Fichtmueller
5d69f0160c feat(procurement): data-grounded supply-availability signal per speed tier (1.6T scarce/800G moderate/400G abundant from real supplier+stock data) 2026-06-11 09:19:57 +00:00
Rene Fichtmueller
de40c65b01 feat(switch-compat): hard verification gate — only manufacturer-verified switches show compatibility
A switch presents transceiver compatibility ONLY if ports_verified=true (its port
config confirmed against the manufacturer datasheet). Unverified switches return
NO suggestions rather than potentially-wrong data. New columns: ports_verified,
ports_verified_source, ports_verified_at. Verified so far against datasheets:
HPE Aruba CX 10000-48Y6C, NVIDIA SN3700 (corrected 100G->200G QSFP56), SN4700,
SN5400, SN5600. 262 switches still pending datasheet verification.
2026-06-11 08:59:01 +00:00
Rene Fichtmueller
00a5ac8e8b docs: changelog — switch compat requires real coding compatibility 2026-06-11 06:58:15 +00:00
Rene Fichtmueller
2dc21ee0aa fix(switch-compat): parse embedded port speed for chassis switches
Chassis switches use ports_config keys like 'max_per_slot_100G_QSFP28' (speed in
the middle), not just '100G_QSFP28'. The '^[0-9.]+G' anchor failed -> port_speed
NULL -> 0 suggestions (e.g. Cisco 8818 router). Now extracts the speed token
wherever it sits before the cage. Cisco 8818: 0 -> 55, all Cisco-codeable, max 400G.
2026-06-11 06:57:42 +00:00
Rene Fichtmueller
3bec30bad3 fix(switch-compat): require Flexoptix coding for the switch vendor (actually runs)
Physical fit alone is not real compatibility — a module that seats in the cage
still won't run unless Flexoptix can code it for the switch's platform. Added a
coding requirement: each suggestion must have an fx_compatibilities entry for the
switch's mapped vendor (HPE/Aruba->aruba, Cisco->cisco, NVIDIA->mellanox, etc.;
whitebox/unmapped -> MSA Standard fallback).

CX 10000-48Y6C: 629 physically-fitting -> 283 actually-codeable-for-Aruba. All
283 verified to carry an Aruba coding. Every suggestion is now catalog-confirmed,
datasheet-accurate, physically fitting AND guaranteed codeable to run in the switch.
2026-06-11 06:55:48 +00:00
Rene Fichtmueller
eeaeda697a docs: changelog — Flexoptix 100% datasheet audit + switch data gap assessment 2026-06-11 06:51:29 +00:00
Rene Fichtmueller
bd9d72d1bc fix(flexoptix-enricher): handle Terabit Ethernet rates in speed extraction
nominalSpeedFromSpecs missed '1.6T Ethernet' protocol entries (only matched 'G'),
returning 800 for twin-port 1.6T parts instead of 1600. Added Terabit handling +
guarded against 'Nx 800G' aggregate partial-matches. After re-deriving all 805
catalog-confirmed FX parts from their stored datasheet specs, DB now matches the
Flexoptix datasheet 100%: Form Factor 802/802, Speed 801/801, 0 deviations.
2026-06-11 06:49:49 +00:00
Rene Fichtmueller
df798d4430 docs: changelog — authoritative source data + switch-compat physical validation 2026-06-11 05:37:16 +00:00
Rene Fichtmueller
7f13144c53 fix(switch-compat): only suggest catalog-confirmed Flexoptix parts (fx_specifications)
~107 parts were misattributed to the Flexoptix vendor but do not exist in the
live Flexoptix catalog (FX uses dot-SKUs like S.1606.28.KD; these use dash-SKUs
like FOT-AX-200G). They polluted 'BEI FLEXOPTIX BESTELLEN' with non-orderable
phantom parts carrying name-guessed specs. Now require fx_specifications IS NOT
NULL — every suggestion is confirmed in Flexoptix's API with authoritative
datasheet specs. CX 10000-48Y6C: 801 -> 630 suggestions, all catalog-confirmed,
0 physically impossible, 0 phantoms.
2026-06-11 05:36:44 +00:00
Rene Fichtmueller
436f62bc2b fix(flexoptix-enricher): source form_factor + speed_gbps from authoritative API specs
Root cause of unreliable speeds: the bulk Flexoptix products API returns NO speed
or form-factor field, so the catalog sync guessed them by parsing the product
NAME (e.g. 'FO-109010-CWDM' -> 100000G). Cryptic FX codes like 'O.164HG2.2.C:Sx'
are unparseable, producing garbage.

The detail enricher already pulls per-SKU specifications=1 (rate-limited) but only
wrote secondary fields. Now it also derives:
  form_factor <- structured 'Form Factor' spec (authoritative datasheet value)
  speed_gbps  <- highest Ethernet rate in 'Supported Protocols', fallback to the
                 'Bandwidth' line-rate upper bound mapped to nominal
Both OVERWRITE the corrupt bulk values (COALESCE(spec, existing)). Never derived
from the product name. Verified: 100/100 freshly-enriched FX parts now have
physically-consistent form_factor/speed (0 contradictions), incl. uncrackable
codes correctly resolved to OSFP/800G, QSFP-DD800/800G etc.
2026-06-11 05:19:48 +00:00
Rene Fichtmueller
2b7d5c7037 fix(switch-compat): physically validate Flexoptix suggestions by port speed + cage mechanics
getFlexoptixSuggestions matched ONLY by form factor, discarding the speed encoded
in each ports_config key (e.g. '100G_QSFP28'). Corrupt transceiver speed_gbps
values (400G/200G/128G/100000G mislabeled as QSFP28) leaked through, so a 100G
switch showed impossible '400G QSFP28' / '100T QSFP28' suggestions.

Now parses (speed, form_factor) from each port key and requires every suggested
module to (a) mechanically seat in the cage — precise port-FF -> accepted-module-FF
map, a QSFP28 cage takes QSFP+/28/56 but never QSFP-DD — and (b) have
speed_gbps <= the port's speed. CX 10000-48Y6C (25G SFP28 + 100G QSFP28) now
returns only valid <=25G SFP / <=100G QSFP modules; 0 physically impossible
entries (was 4 garbage groups). Belt-and-suspenders: even with corrupt speed data,
nothing oversized can reach a customer-facing suggestion.
2026-06-10 22:39:24 +00:00
Rene Fichtmueller
53518fa15a docs: changelog — reconciliation healed 4 dead jobs, full pipeline live 2026-06-07 05:58:34 +00:00
Rene Fichtmueller
c66ffc92cb docs: changelog — Erik/Gitea reconciliation complete 2026-06-07 05:02:46 +00:00
Rene Fichtmueller
8c6df1028d merge: reconcile Erik-deployed fixes with Gitea feature branch (origin/main)
Merges 28 Gitea commits (Transceiver Academy, dashboard features A-P, equivalence
matchers OPN/spec, Abverkauf velocity engine, Flexoptix detail sync, SQL 111-118,
MCP equivalences tool, training data) with 22 Erik-local commits (price chart,
fmtSpd, supply-squeeze per-SKU fix, research-robot actions, NADDOD warehouse,
FS competitor stock, reorder bloat fix, flexoptix empty-token fix, vendor
reliability, blog WireGuard failover).

Only 2 files changed on both sides; ort auto-merged both cleanly. Manually
resolved a resulting duplicate GET /api/hype-cycle/market-signals: kept the
Erik handler (technologies+marketSignalScore+drivers+globalContext, matches the
merged dashboard), removed the Gitea Multi-source variant (preserved in history).

All 3 packages build with 0 TS errors. Safety: branch erik-pre-reconcile-20260607,
bundle _RECON_full-backup-20260607.bundle (also on Fearghas), bulk-price WIP in
stash@{0}, untracked SQL backed up to _RECON_untracked_sql_20260607/.
2026-06-07 05:01:32 +00:00
Rene Fichtmueller
121477810a docs: changelog 2026-06-06 — data refresh + reorder bloat fix + pulse 404 fix + health audit 2026-06-06 21:43:20 +00:00
Rene Fichtmueller
87cd17808f fix(dashboard): Procurement-Pulse Buy-Signals card hit 404 endpoint
loadProcurementPulse called /api/procurement/reorder (does not exist) -> caught
-> always showed 0 Buy Signals. Correct route is /api/procurement/reorder-top,
which returns summary.buy_now. Fixed endpoint + count mapping (now shows 314).
2026-06-06 21:42:21 +00:00
Rene Fichtmueller
bb4046bb1e fix(reorder-signals): delete-before-insert prevents unbounded table growth
reorder_signals grew to 4.49M rows / 1.19GB — the compute job INSERTed a fresh
row per transceiver every 4h run but never deleted old ones (24h TTL filtered
them at read time via DISTINCT ON + expires_at, but they were never purged).
4.37M rows were already expired dead weight.

Fix: DELETE existing rows for a transceiver before inserting its new signal, so
the table holds exactly one (latest) row per transceiver. Cleaned up to 18,175
rows / 4.5MB (99.6% reclaimed, VACUUM FULL). Backup: reorder_signals_keep_bak_20260606.
Verified: re-running compute:reorder-signals keeps count stable at 18,175.
2026-06-06 21:39:57 +00:00
Rene Fichtmueller
c67fef1862 docs: changelog — flexoptix sync 401 root cause (empty-token short-circuit) 2026-06-06 17:09:07 +00:00
Rene Fichtmueller
2432c0ddfc fix(flexoptix-sync): empty FLEXOPTIX_API_TOKEN string short-circuited bearer login
Root cause of the persistent sync:flexoptix-catalog HTTP 401: line 397 used
'?? null' which only coerces null/undefined. With FLEXOPTIX_API_TOKEN='' (empty
string set in .env), token stayed '' and line 485's 'token ?? getBearerToken()'
returned '' instead of performing the username/password login — sending an empty
'Bearer ' header that the products endpoint rejected with 401.

Fix: '|| null' coerces empty string to null so the bearer-login fallback fires.
Verified: sync now completes (username/password -> customer token -> products 200,
3 products/price/stock writes on limit=50). Credentials were correct all along.
2026-06-06 17:06:22 +00:00
Rene Fichtmueller
82934d3e0a docs: changelog 2026-06-06 — supply-squeeze fix, robot actions, competitor stock 2026-06-06 16:54:13 +00:00
Rene Fichtmueller
a9f95fc552 feat(research-robot): actionable recommendations + dispatch/pause/resume buttons
The Research Robot panel showed only an LLM assessment (info, no action). Now:

API (research-robot.ts):
- GET enriches response with recommendations[] computed from live pgboss state:
  classifies each persistently-failing job (auth/401, network, no-handler, other)
  into severity + concrete advice + offered actions.
- POST /action {action,job}: dispatch (enqueue one run), pause (remove from
  schedule with backup to research_robot_paused_schedules), resume (restore).
  All validated against the pgboss.queue whitelist.

Dashboard:
- Renders each recommendation as a card with severity colour, cause, last error,
  and action buttons (Jetzt auslösen / Pausieren / Fortsetzen / Token-Anleitung).
- Verified: sync:flexoptix-catalog -> critical auth (HTTP 401), offers
  token-help + pause. Dispatch/pause/resume roundtrip tested green.
2026-06-06 16:53:54 +00:00
Rene Fichtmueller
0cf607040f fix(supply-squeeze): per-SKU paired price comparison eliminates catalog-composition bias
The 30d-vs-60d price momentum aggregated AVG/median across whatever SKUs
happened to be in a speed/form-factor bucket each period. New expensive SKUs
entering the catalog (NVIDIA switches at 30k USD, AOC cables) faked huge jumps
— 400G OSFP showed +151% when matched-SKU reality was 0%.

Now: compute per-transceiver median price in each period, keep only SKUs present
in BOTH periods (>=2 obs each), report the median of per-SKU pct deltas. Also
excludes non-transceiver form factors, AOC/DAC cables, switch SKUs, price>15k,
and anomalous observations. Result: 400G OSFP +151%->0%, signals 21->8, and the
ones that remain (NVIDIA MFA7U10 +84% same-SKU) are genuine price moves.
2026-06-06 16:48:15 +00:00
Rene Fichtmueller
03fdfa7d51 feat(naddod-scraper): extract per-warehouse stock breakdown + write warehouse_global_qty
Adds parseWarehouseStock() to decode the HTML-entity-encoded warehouse_stock JSON
(us/nl/sg/cn per-region array). When the static page has warehouse data, writes:
  warehouse_de_qty   ← nl (EU-closest warehouse)
  warehouse_global_qty ← sum(us+nl+sg+cn), or falls back to quantity_available
  stock_confidence   ← 3 (L3) when warehouse breakdown available, else 2

Note: per-warehouse quantities require JS execution to populate (API-loaded);
static HTML has [0,0] placeholders. The fallback ensures NADDOD global totals
appear in the competitor-by-tech dashboard comparison.
2026-06-06 16:34:02 +00:00
Rene Fichtmueller
9bf7da3fda feat(stock): FS.COM competitor stock by technology in Sales Velocity table
Adds /api/stock/competitor-by-tech endpoint aggregating warehouse_de_qty +
warehouse_global_qty from stock_observations for public competitors (FS.COM
etc.) per technology class. Dashboard velocity table gets two new columns
FS.COM DE + FS.COM Global with traffic-light coloring vs. monthly demand.
2026-06-05 23:06:13 +00:00
Rene Fichtmueller
261135c544 fix(detail-panel): show Flexoptix price in competitor comparison + fix 800.00G->800G 2026-06-05 22:55:15 +00:00
Rene Fichtmueller
0d3edd6ea9 feat+fix(dashboard): price chart + fmtSpd speed display (stable) 2026-06-05 22:49:09 +00:00
Rene Fichtmueller
8b232d942d feat+fix(dashboard): price chart on signal-card click + fmtSpd speed display cleanup 2026-06-05 22:48:13 +00:00
Rene Fichtmueller
8432a44574 fix(dashboard): fmtSpd() all speed displays -- 1.00G->1G, 400.00G->400G, 1600->1.6T 2026-06-05 22:38:52 +00:00
Rene Fichtmueller
f2b26e3286 fix(dashboard): clean speed display via fmtSpd() — 1.00G→1G, 400.00G→400G, 1600→1.6T 2026-06-05 22:38:13 +00:00