// Package ui — контроллер и представления десктопного интерфейса (Fyne). // // Слой обмена с ядром: однонаправленный поток (спец 12.2–12.5). // - Core мутирует состояние; UI только читает снимки и реагирует на события. // - Действия UI = команды (CreateTask/ApproveTask/...), которые зовут Core. // - Подписчик шины (Controller) получает события и перекладывает их в View // (реализация Fyne) через fyne.Do — никаких прямых вызовов Fyne из core. // // Контракты компонентов-виджетов (спец 12.8): интерфейс + Nil-реализация. package ui import "strings" // Канонические уровни записей лога для фильтра панели «Логи» (спец 12.9). const ( LogLevelError = "error" LogLevelWarning = "warning" LogLevelInfo = "info" LogLevelDebug = "debug" ) // LogLevels — уровни в порядке их вывода в фильтре виджета «Логи». var LogLevels = []string{LogLevelError, LogLevelWarning, LogLevelInfo, LogLevelDebug} // NormalizeLogLevel приводит уровень записи лога к одному из канонических // уровней (LogLevels). Синонимы маппятся на ближайший уровень (err/fatal → // error, warn → warning, trace → debug); пустые и неизвестные значения // ("log" от events.LogWriter и всё прочее) считаются информационными. func NormalizeLogLevel(level string) string { switch strings.ToLower(strings.TrimSpace(level)) { case LogLevelError, "err", "fatal": return LogLevelError case LogLevelWarning, "warn": return LogLevelWarning case LogLevelDebug, "trace": return LogLevelDebug default: return LogLevelInfo } } // LogPanel — панель «Логи» (спец 12.9): поток системных логов с уровнями. // // Хранит записи (уровень + строка) в буфере ограниченного размера; при // превышении лимита старые записи отбрасываются. Реализация обязана // поддерживать фильтр по уровням: выпадающий список с чекбоксами // (error/warning/info/debug), по умолчанию отмечены все уровни; показываются // только записи выбранных уровней, изменение набора сразу обновляет ленту. // Контейнер виджета отдаётся окну для встраивания в таб «Логи» — реализация // (Fyne) сама владеет своим виджетом. type LogPanel interface { // Append добавляет запись лога: level — уровень из events.LogLine // (нормализуется через NormalizeLogLevel), text — строка лога. // Реализация обязана перекладывать работу на поток Fyne (fyne.Do / // fyne.DoAndWait), т.к. вызывается из горутин Hub. Append(level, text string) } // NilLogPanel — no-op реализация LogPanel для headless-режима и тестов. type NilLogPanel struct{} // NewNilLogPanel создаёт NilLogPanel. func NewNilLogPanel() *NilLogPanel { return &NilLogPanel{} } func (NilLogPanel) Append(string, string) {} // LogEntry — запись буфера панели «Логи»: канонический уровень + строка. type LogEntry struct { Level string Text string } // LogBuffer — ограниченный буфер записей лога с фильтрацией по уровням // (спец 12.9). Чистая логика без Fyne: используется Fyne-реализацией // LogPanel и тестируется в internal/ui. // // Не синхронизирована: потокобезопасность обеспечивает владелец (все вызовы // на потоке Fyne). type LogBuffer struct { entries []LogEntry maxLen int // лимит суммарного объёма текста записей, символы } // NewLogBuffer создаёт буфер ёмкостью maxLen символов текста записей. func NewLogBuffer(maxLen int) *LogBuffer { return &LogBuffer{maxLen: maxLen} } // Append добавляет запись (уровень нормализуется через NormalizeLogLevel, // хвостовые переводы строк срезаются). При превышении лимита старые записи // вытесняются. Сообщает, было ли вытеснение/обрезка — владелец должен // перерисовать ленту, даже если новая запись не видима. func (b *LogBuffer) Append(level, text string) bool { b.entries = append(b.entries, LogEntry{ Level: NormalizeLogLevel(level), Text: strings.TrimRight(text, "\r\n"), }) return b.trim() } // trim удерживает суммарный объём текста записей в пределах maxLen, // отбрасывая старые записи; одиночная запись длиннее лимита обрезается // до последних maxLen символов. Сообщает, было ли вытеснение/обрезка. func (b *LogBuffer) trim() bool { total := 0 for _, e := range b.entries { total += len(e.Text) + 1 } dropped := false for total > b.maxLen && len(b.entries) > 1 { total -= len(b.entries[0].Text) + 1 b.entries = b.entries[1:] dropped = true } if n := len(b.entries); n > 0 && len(b.entries[n-1].Text) > b.maxLen { b.entries[n-1].Text = b.entries[n-1].Text[len(b.entries[n-1].Text)-b.maxLen:] dropped = true } return dropped } // Entries возвращает копию списка записей буфера. func (b *LogBuffer) Entries() []LogEntry { out := make([]LogEntry, len(b.entries)) copy(out, b.entries) return out } // Render строит текст ленты только из записей уровней, отмеченных в // selected (уровень → показывать). Пустой или nil набор → пустая лента. func (b *LogBuffer) Render(selected map[string]bool) string { var s strings.Builder for _, e := range b.entries { if !selected[e.Level] { continue } s.WriteString(e.Text) s.WriteByte('\n') } return s.String() }