platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,82 @@
package app
import (
"io"
"os"
"strconv"
"strings"
)
// ---------------------------------------------------------------------
// tail helper
// ---------------------------------------------------------------------
const defaultTailMaxBytes = 512 * 1024
func tailFile(path string, maxLines int) []string {
if maxLines <= 0 {
return nil
}
// читаем лимит из env, если задан
maxBytes := defaultTailMaxBytes
if env := os.Getenv("SVPN_TAIL_MAX_BYTES"); env != "" {
if n, err := strconv.Atoi(env); err == nil && n > 0 {
maxBytes = n
}
}
f, err := os.Open(path)
if err != nil {
// файла нет или нет прав — просто ничего не отдаём
return nil
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil
}
size := fi.Size()
if size <= 0 {
return nil
}
// с какого смещения читаем хвост
start := int64(0)
if size > int64(maxBytes) {
start = size - int64(maxBytes)
}
// двигаем указатель в файле
if _, err := f.Seek(start, io.SeekStart); err != nil {
return nil
}
// читаем хвост
data, err := io.ReadAll(f)
if err != nil {
return nil
}
// режем по строкам
lines := strings.Split(string(data), "\n")
// если мы начали читать с середины файла (start > 0),
// первая строка почти наверняка обрезана — выбрасываем её.
if start > 0 && len(lines) > 0 {
lines = lines[1:]
}
// убираем финальную пустую строку, если есть
if n := len(lines); n > 0 && lines[n-1] == "" {
lines = lines[:n-1]
}
// берём только последние maxLines
if len(lines) > maxLines {
lines = lines[len(lines)-maxLines:]
}
return lines
}