feat: режим индикаторов без цветовой кодировки + переустройство /setup

Новая настройка app_settings.indicator_mode (color | accessible), переключается
на /setup. В режиме accessible сигналы, которые раньше держались только на цвете,
переходят на форму/текст:
- качество цены (0-9) — 5-ступенчатый signal-meter вместо градиентной точки/числа;
  единый модуль web/static/price-quality.js, ветвление по window.QF_INDICATOR_MODE;
- вкладка «Ценообразование»: ведущая колонка-meter вместо заливки строк,
  пометка W вместо амбер-подсветки world-цен, ⚠ вместо красного «загрязнённого» итога.
- GET/PUT /api/settings/ui (без рестарта), регистрируется в обоих наборах роутов.

/setup переустроен: две колонки одной высоты, кнопка «Вернуться в приложение»,
исправлен <title>.

Документация: bible-local 02/03/04 + decisions/2026-08-31-accessible-indicator-mode.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbRvQgPZpM4SJTaX3iLrXk
This commit is contained in:
Mikhail Chusavitin
2026-08-31 18:08:38 +03:00
co-authored by Claude Sonnet 5
parent d97ac447ca
commit 681ec15e2b
15 changed files with 453 additions and 52 deletions
+6
View File
@@ -85,6 +85,12 @@ Controls terminology:
CSV export reads PN вендора, Описание, and LOT from `data-vendor-pn`, `data-desc`, `data-lot` row attributes to bypass the rowspan cell offset problem.
In the colour-optional indicator mode (`app_settings.indicator_mode = "accessible"`, see
[03-database.md](03-database.md) and [decisions/2026-08-31-accessible-indicator-mode.md](decisions/2026-08-31-accessible-indicator-mode.md))
both tables gain a narrow leading price-quality meter column (per sub-row), the row background
tint is removed, `world`-fallback cells show a `W` text marker instead of amber, and the footer
total shows a `⚠` prefix instead of red. CSV output is unaffected.
## Configuration versioning
Configuration revisions are append-only snapshots stored in `local_configuration_versions`.
+4 -2
View File
@@ -17,7 +17,7 @@ Main tables:
| `local_partnumber_book_items` | PN -> LOT catalog payload |
| `pending_changes` | sync queue |
| `connection_settings` | encrypted MariaDB connection settings |
| `app_settings` | local app state |
| `app_settings` | local app state (key→value); includes `indicator_mode` = `color` \| `accessible`, a per-client display preference set on `/setup` |
| `local_schema_migrations` | applied local migration markers |
| `local_qt_settings` | server-pushed configurator settings cache (from `qt_settings`) |
@@ -261,7 +261,9 @@ PK: lot_name
| price | decimal(12,2) NOT NULL | |
| price_quality | tinyint unsigned, nullable | added by migration 033. Set by the external pricelist-building tool, 0-9, based on quote recency/count per its own per-lot pricing-period settings — QF only reads and displays it, never computes it. |
`price_quality` is synced through `LocalPricelistItem` (pricelist detail page) and, separately, through `LocalComponent`/`services.ComponentView` (`/api/components`, used by the configurator's search dropdown and item table) — both read paths go through `pricelistItemRow`/`toLocalComponent()` in `internal/localdb/components.go`. The `/api/components` path reads the component universe (`world` `estimate`, see [decisions/2026-07-24-component-universe-world-union.md](decisions/2026-07-24-component-universe-world-union.md)), where a world-only LOT is forced to `price_quality = 0` — the only place QF writes this field rather than reading it. The color scale (red 0 → yellow 5 → green 9, gradient) lives in **one shared JS module**, `web/static/price-quality.js` (loaded by `base.html` for every page) — `priceQualityColor`/`qualityDotHtml`/`qualityBadgeHtml`/`qualityRowStyle`. Do not reimplement the color scale locally in a template; add a new helper to that module instead. Used in: pricelist detail "Качество" column, configurator search dropdown (colored dot), configurator table (leftmost quality-dot column), pricing tab (row background tint).
`price_quality` is synced through `LocalPricelistItem` (pricelist detail page) and, separately, through `LocalComponent`/`services.ComponentView` (`/api/components`, used by the configurator's search dropdown and item table) — both read paths go through `pricelistItemRow`/`toLocalComponent()` in `internal/localdb/components.go`. The `/api/components` path reads the component universe (`world` `estimate`, see [decisions/2026-07-24-component-universe-world-union.md](decisions/2026-07-24-component-universe-world-union.md)), where a world-only LOT is forced to `price_quality = 0` — the only place QF writes this field rather than reading it. The visual scale lives in **one shared JS module**, `web/static/price-quality.js` (loaded by `base.html` for every page) — `priceQualityColor`/`priceQualityLevel`/`qualityMeterHtml`/`qualityDotHtml`/`qualityBadgeHtml`/`qualityRowStyle`. Do not reimplement the scale locally in a template; add a new helper to that module instead. Used in: pricelist detail "Качество" column, configurator search dropdown, configurator table (leftmost quality column), pricing tab.
The module has **two render modes**, chosen per client by `window.QF_INDICATOR_MODE` (injected by `base.html` from `app_settings.indicator_mode`, default `color`): `color` = hue scale red 0 → yellow 5 → green 9; `accessible` = colour-blind-safe 5-step signal-strength meter (`priceQualityLevel`: `0-1/2-3/4-5/6-7/8-9`), single neutral hue. In `accessible` mode the pricing tab drops the row background tint for a leading meter column, the `world`-fallback amber cell tint becomes a `W` text marker, and the "contaminated total" red becomes a `⚠` prefix — see [decisions/2026-08-31-accessible-indicator-mode.md](decisions/2026-08-31-accessible-indicator-mode.md). Any new colour-only signal must add a branch in this module keyed on `isAccessible()`, not a local one.
The real table also has `price_method`, `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` and `lead_time_weeks` columns, owned and written by the external pricing engine that maintains `qt_pricelist_items`**keep them in the table for backward compatibility with that system; QF must never drop, rename, or write to them.** QF itself does not model, sync, or display any of them (tried once, removed as unused/dead in the client). Do not re-add them to `models.PricelistItem`/`LocalPricelistItem` without a concrete UI need.
+11
View File
@@ -29,6 +29,17 @@
`POST /api/restart` exists only in `debug` mode.
## Settings
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/settings/ui` | read per-client UI display preferences → `{ "indicator_mode": "color" \| "accessible" }` |
| `PUT` | `/api/settings/ui` | set `indicator_mode` (body `{ "indicator_mode": ... }`); `422` on an unknown value |
Stored in `app_settings` (SQLite); no restart. Registered in both the normal and the setup-mode
route sets. Consumed by `web/static/price-quality.js` and the pricing tab via a
`window.QF_INDICATOR_MODE` global that `base.html` renders from the same setting.
## Reference data
| Method | Path | Purpose |
@@ -0,0 +1,63 @@
# Decision: `indicator_mode` — a colour-optional rendering mode for UI signals
**Date:** 2026-08-31
**Status:** active
## Context
Several UI signals were encoded by hue alone and unreadable with a colour-vision deficiency:
- `price_quality` (09) as a red→yellow→green gradient — a dot in the configurator search
dropdown and inline before `lot_name`, a coloured number in the pricelist "Качество" column,
a full-row background tint on the "Ценообразование" tab;
- the amber `world`-fallback price cell tint
([2026-07-10-world-pricelist-fallback.md](2026-07-10-world-pricelist-fallback.md));
- the red "contaminated total" in the pricing footer
([2026-07-24-pricing-total-world-share.md](2026-07-24-pricing-total-world-share.md)).
The bible has no dedicated accessibility contract, but `table-management` §Icon Semantics,
`controls-selection` §Buttons, `go-code-style` §Business Logic Placement and `build-version-display`
say the same thing: meaning is never carried by one ambiguous channel — back it with
shape / number / explicit `title`+`aria-label`, and keep the indicator visually subordinate.
## Decision
One per-client preference, **`app_settings.indicator_mode`** = `color` (default) | `accessible`,
set on `/setup` ("Настройки") via `GET`/`PUT /api/settings/ui` (`internal/handlers/settings.go`)
— no restart. Deliberately named for the concern (how indicators are rendered), not for the
current widget, so future colour-only signals attach to the same switch.
`internal/handlers/web.go` `render()` and `internal/handlers/setup.go` inject the mode for every
page; `base.html` publishes it as `window.QF_INDICATOR_MODE`. `web/static/price-quality.js` is
still the single source of the price-quality scale and branches on the mode internally, so its
call sites do not change.
In `accessible` mode:
- `price_quality` renders as a **5-step signal-strength meter** (`priceQualityLevel` =
`min(4, floor(score/2))``0-1 / 2-3 / 4-5 / 6-7 / 8-9`), five bars in one neutral hue, each
instance carrying `title`/`aria-label` = `Качество цены: N/9`. `qualityDotHtml` and
`qualityBadgeHtml` return the meter;
- `qualityRowStyle` returns `''` — the pricing tab instead shows the meter in a narrow leading
column (conditional `<th>` / `colspan` in `index.html`, per-sub-row `<td>`);
- the `world`-fallback amber class is dropped for a `W` superscript text marker on the price
(text, not a colour class — `applyCustomPrice`'s colour-stripping regex is a non-issue);
- `_setPricingTotal` drops `text-red-600` for a leading `⚠` glyph, on the same trigger as the
red (`worldShare > 0`). The hover popup — including the "prices for N of M positions" coverage
line — is unchanged in both modes; per 2026-07-24 there is still no colour-coded coverage-only
signal, and `accessible` mode does not add one.
`color` mode is byte-for-byte the previous behaviour.
## Consequences
- Default unchanged; nothing moves until a user opts in on `/setup`.
- `web/static/price-quality.js` stays the only place that knows the 09 scale. Any new
colour-coded quality/price signal adds a branch there keyed on `isAccessible()`, never a local
reimplementation, and its `accessible` form must be shape/text, not another hue.
- The pricing-tab tables have a mode-dependent column count (8 / 9). `index.html` guards the
`<th>`, the empty-state `colspan`, the `tfoot` "Итого:" `colspan` and the JS row template on
`ACCESSIBLE_MODE` / `{{ eq .IndicatorMode "accessible" }}`. CSV export is untouched — it reads
`data-*` row attributes, not cell positions.
- New routes `GET`/`PUT /api/settings/ui`, registered in both the normal and setup-mode route
sets in `cmd/qfs/main.go`. `render()` nil-guards `localDB` for tests.