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:
co-authored by
Claude Sonnet 5
parent
d97ac447ca
commit
681ec15e2b
@@ -0,0 +1,47 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SettingsHandler serves per-client UI display preferences stored in app_settings.
|
||||
type SettingsHandler struct {
|
||||
localDB *localdb.LocalDB
|
||||
}
|
||||
|
||||
func NewSettingsHandler(localDB *localdb.LocalDB) *SettingsHandler {
|
||||
return &SettingsHandler{localDB: localDB}
|
||||
}
|
||||
|
||||
type uiSettingsResponse struct {
|
||||
IndicatorMode string `json:"indicator_mode"`
|
||||
}
|
||||
|
||||
// GetUI returns the current UI display preferences.
|
||||
func (h *SettingsHandler) GetUI(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, uiSettingsResponse{
|
||||
IndicatorMode: h.localDB.GetIndicatorMode(),
|
||||
})
|
||||
}
|
||||
|
||||
// PutUI updates UI display preferences. Currently only indicator_mode.
|
||||
func (h *SettingsHandler) PutUI(c *gin.Context) {
|
||||
req, ok := BindJSON[struct {
|
||||
IndicatorMode string `json:"indicator_mode"`
|
||||
}](c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.localDB.SetIndicatorMode(req.IndicatorMode); err != nil {
|
||||
RespondError(c, http.StatusUnprocessableEntity, "invalid indicator_mode", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, uiSettingsResponse{
|
||||
IndicatorMode: h.localDB.GetIndicatorMode(),
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
qfassets "git.mchus.pro/mchus/quoteforge"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/appmeta"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/db"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -56,7 +57,9 @@ func (h *SetupHandler) ShowSetup(c *gin.Context) {
|
||||
settings, _ := h.localDB.GetSettings()
|
||||
|
||||
data := gin.H{
|
||||
"Settings": settings,
|
||||
"Settings": settings,
|
||||
"IndicatorMode": h.localDB.GetIndicatorMode(),
|
||||
"AppVersion": appmeta.Version(),
|
||||
}
|
||||
|
||||
tmpl := h.templates["setup.html"]
|
||||
@@ -207,4 +210,3 @@ func buildMySQLDSN(host string, port int, database, user, password string, timeo
|
||||
}
|
||||
return cfg.FormatDSN()
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,10 @@ func NewWebHandler(_ string, localDB *localdb.LocalDB) (*WebHandler, error) {
|
||||
|
||||
func (h *WebHandler) render(c *gin.Context, name string, data gin.H) {
|
||||
data["AppVersion"] = appmeta.Version()
|
||||
data["IndicatorMode"] = localdb.IndicatorModeColor
|
||||
if h.localDB != nil {
|
||||
data["IndicatorMode"] = h.localDB.GetIndicatorMode()
|
||||
}
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl, ok := h.templates[name]
|
||||
if !ok {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user