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
+45
View File
@@ -0,0 +1,45 @@
package localdb
import (
"path/filepath"
"testing"
)
func newModeDB(t *testing.T) *LocalDB {
t.Helper()
local, err := New(filepath.Join(t.TempDir(), "mode.db"))
if err != nil {
t.Fatalf("open localdb: %v", err)
}
t.Cleanup(func() { _ = local.Close() })
return local
}
func TestIndicatorMode(t *testing.T) {
local := newModeDB(t)
if got := local.GetIndicatorMode(); got != IndicatorModeColor {
t.Fatalf("unset: got %q, want %q", got, IndicatorModeColor)
}
if err := local.SetIndicatorMode(IndicatorModeAccessible); err != nil {
t.Fatalf("set accessible: %v", err)
}
if got := local.GetIndicatorMode(); got != IndicatorModeAccessible {
t.Fatalf("after set accessible: got %q, want %q", got, IndicatorModeAccessible)
}
if err := local.SetIndicatorMode(IndicatorModeColor); err != nil {
t.Fatalf("set color: %v", err)
}
if got := local.GetIndicatorMode(); got != IndicatorModeColor {
t.Fatalf("after set color: got %q, want %q", got, IndicatorModeColor)
}
if err := local.SetIndicatorMode("rainbow"); err == nil {
t.Fatal("expected error for invalid mode, got nil")
}
if got := local.GetIndicatorMode(); got != IndicatorModeColor {
t.Fatalf("after rejected set: got %q, want %q", got, IndicatorModeColor)
}
}
+37
View File
@@ -1130,6 +1130,43 @@ func (l *LocalDB) upsertAppSetting(tx *gorm.DB, key, value string, updatedAt tim
`, key, value, updatedAt.Format(time.RFC3339)).Error
}
// Indicator display mode: how the UI renders signals that are otherwise carried
// by colour alone (price-quality scale, world-fallback price cells, incomplete
// pricing totals). "color" (default) keeps the hue coding; "accessible" swaps it
// for shape/text so it stays readable with a colour-vision deficiency. Per-client
// preference stored in app_settings; consumed by web/static/price-quality.js and
// the pricing tab via base.html's window.QF_INDICATOR_MODE.
const (
IndicatorModeColor = "color"
IndicatorModeAccessible = "accessible"
)
// GetIndicatorMode returns the stored indicator display mode, falling back to
// "color" when unset or invalid.
func (l *LocalDB) GetIndicatorMode() string {
value, ok := l.getAppSettingValue("indicator_mode")
if !ok {
return IndicatorModeColor
}
switch strings.TrimSpace(value) {
case IndicatorModeAccessible:
return IndicatorModeAccessible
default:
return IndicatorModeColor
}
}
// SetIndicatorMode stores the indicator display mode. Only the two known
// literals are accepted.
func (l *LocalDB) SetIndicatorMode(mode string) error {
mode = strings.TrimSpace(mode)
if mode != IndicatorModeColor && mode != IndicatorModeAccessible {
return fmt.Errorf("invalid indicator mode %q", mode)
}
now := time.Now()
return l.upsertAppSetting(l.db, "indicator_mode", mode, now)
}
// SetLastSyncTime sets the last sync timestamp
func (l *LocalDB) SetLastSyncTime(t time.Time) error {
// Using raw SQL for upsert since SQLite doesn't have native UPSERT in all versions