приложение собрано каркасом и вшито в бинарник

- заведён каталог web/ — Vue 3, роутер пятой версии, сборка Vite; собранное
  вшивается через go:embed и раздаётся корневым маршрутом: разметка на
  неизвестном пути вне корней сервиса, отказ контракта внутри корня
- перечень корней сервиса стал единой точкой и порождает регистрацию маршрутов,
  а не описывает её; журнал раздачи пишет исход и длину пути, но не сам путь
- шаг front зовёт Node контейнером docker — Biome, юнит-тесты Vue и сборка
  входят в гейт, а в Dockerfile появилась ступень приложения
This commit is contained in:
av
2026-08-15 18:51:05 +03:00
parent c6ffda9aac
commit 663021f712
46 changed files with 5958 additions and 43 deletions
+43 -18
View File
@@ -23,6 +23,7 @@ import (
"git.vakhrushev.me/av/transcriber/internal/controller/worker"
"git.vakhrushev.me/av/transcriber/internal/metrics"
"git.vakhrushev.me/av/transcriber/internal/service"
"git.vakhrushev.me/av/transcriber/web"
"github.com/joho/godotenv"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
@@ -167,6 +168,15 @@ func main() {
SecureCookie: cfg.Auth.SecureCookie,
}, logger)
// Адресное пространство сервиса объявлено одним перечнем, и он порождает
// регистрацию, а не описывает её: корень, заведённый мимо перечня, не
// получит обработчика вовсе. Отсюда же уровень журнала для адресов
// наблюдения и правило неизвестного пути у раздачи приложения.
mounts := httpcontroller.ServiceMounts(appHandler, authHandler, promhttp.Handler())
dist, appBuilt := web.Dist()
webappHandler := httpcontroller.NewWebappHandler(dist, appBuilt, mounts, logger)
// Сервер приезжает каналом, а не общей переменной: хук исполняется в
// горутине сервера, а читает его горутина остановки, и связи «произошло
// раньше» между ними иначе нет.
@@ -186,17 +196,29 @@ func main() {
err := e.Next()
level := slog.LevelInfo
if e.Request.URL.Path == "/health" || e.Request.URL.Path == "/metrics" {
if httpcontroller.IsObservationAddress(mounts, e.Request.URL.Path) {
// Опрос здоровья и метрик идёт постоянно и полезного не несёт.
level = slog.LevelDebug
}
logger.Log(e.Request.Context(), level, "Incoming request",
attrs := []any{
"http.method", e.Request.Method,
"http.route", journalRoute(e.Request.URL.Path),
"http.route", journalRoute(e.Request.URL.Path, mounts),
"http.status_code", e.Status(),
"duration_ms", time.Since(start).Milliseconds(),
"transport", "http")
"transport", "http",
}
// Путь, отданный приложению, в журнал не идёт — вместо него исход
// и длина: по ним видно, что происходит, а множеством значений
// самого пути распоряжается спрашивающий.
if outcome := httpcontroller.WebappOutcome(e); outcome != "" {
attrs = append(attrs,
"webapp.outcome", outcome,
"http.path_length", len(e.Request.URL.Path))
}
logger.Log(e.Request.Context(), level, "Incoming request", attrs...)
return err
})
@@ -222,20 +244,11 @@ func main() {
return fmt.Errorf("failed to apply app rate limit: %w", err)
}
authHandler.Register(se.Router)
appHandler.Register(se.Router)
httpcontroller.RegisterServiceRoutes(se.Router, mounts)
se.Router.GET("/health", func(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]string{
"status": "ok",
"message": "Transcriber service is running",
})
})
se.Router.GET("/metrics", func(e *core.RequestEvent) error {
promhttp.Handler().ServeHTTP(e.Response, e.Request)
return nil
})
// Раздача приложения вешается последней: она занимает корень, и всё,
// что не совпало ни с одним адресом сервиса, доходит до неё.
webappHandler.Register(se.Router)
return se.Next()
})
@@ -313,6 +326,9 @@ func main() {
// сегмент такого пути и есть имя файла в хранилище.
const filesPathPrefix = "/api/files/"
// webappRoute — чем в журнале обозначается всякий путь, отданный приложению.
const webappRoute = "<приложение>"
// journalRoute готовит путь запроса к записи в журнал.
//
// Инвариант проекта запрещает имени файла в хранилище попадать в журнал: имя —
@@ -323,7 +339,16 @@ const filesPathPrefix = "/api/files/"
//
// Срезается только имя: маршрут остаётся различимым, и наблюдаемость от этого не
// теряется.
func journalRoute(path string) string {
//
// Путь, не принадлежащий сервису, в журнал не идёт вовсе. До появления раздачи
// приложения такой путь ловил отказ маршрутизатора, а теперь получает разметку
// с кодом `200`: множеством его значений распоряжается спрашивающий, и
// дословная строка сделала бы журнал местом, куда аноним пишет свой текст.
func journalRoute(path string, mounts []httpcontroller.Mount) string {
if !httpcontroller.IsServiceAddress(mounts, path) {
return webappRoute
}
if !strings.HasPrefix(path, filesPathPrefix) {
return path
}