Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
package main
import (
"context"
"log"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
"github.com/descrybe/descrybe-v2/apps/api/internal/httpapi"
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
)
func main() {
log.SetOutput(logredact.Writer(os.Stderr))
slog.SetDefault(slog.New(logredact.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
if cfg.ShouldWarnRateLimits() {
slog.Warn(cfg.RateLimitWarningMessage(),
"rate_limit_replicas", cfg.RateLimitReplicas,
"rate_limit_multi_replica", cfg.RateLimitMultiReplica,
"rate_limit_backend", cfg.RateLimitBackend,
"rate_limit_backend_requested", cfg.RateLimitBackendRequested,
)
}
ctx := context.Background()
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
MaxConns: int32(cfg.DBMaxConns),
MinConns: int32(cfg.DBMinConns),
MaxConnLifetime: cfg.DBMaxConnLifetime,
MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
MaxConnIdleTime: cfg.DBMaxConnIdleTime,
HealthCheckPeriod: cfg.DBHealthCheckPeriod,
StatementTimeout: cfg.DBStatementTimeout,
})
if err != nil {
log.Fatalf("db: %v", err)
}
defer pool.Close()
sessions := auth.NewSessionManager(pool, cfg.SessionCookieName, cfg.CookieSecure(), cfg.SessionIdleHours)
srv := httpapi.NewServer(cfg, pool, sessions)
runCtx, runCancel := context.WithCancel(context.Background())
defer runCancel()
// Lightweight AI fallback poller so FAQ-miss tickets drain without a separate worker.
if srv.Support != nil {
go srv.Support.RunAutoJobsLoop(runCtx, 2*time.Second, 3)
}
httpServer := newHTTPServer(cfg.HTTPAddr, srv.Router())
ln, err := net.Listen("tcp", cfg.HTTPAddr)
if err != nil {
log.Fatalf("listen: %v", err)
}
slog.Info("api_listening", "addr", cfg.HTTPAddr, "maintenance", cfg.MaintenanceMode, "read_only", cfg.ReadOnlyMode)
go func() {
if err := httpServer.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("serve: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
runCancel()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
}
// newHTTPServer configures net/http timeouts for the API listener.
//
// Tradeoff — WriteTimeout vs long sync/export:
// WriteTimeout bounds the whole ServeHTTP + response write. Feed export
// streams and sync-style handlers can run for many minutes; a short
// WriteTimeout aborts them mid-stream (clients see truncated/hanging
// responses). We use a long WriteTimeout ceiling instead of 0 (unlimited)
// so wedged handlers still release connections eventually. Finer per-route
// deadlines belong on request contexts / middleware for normal JSON APIs.
// Leaving WriteTimeout unset (0) would never reclaim a stuck writer.
func newHTTPServer(addr string, handler http.Handler) *http.Server {
return &http.Server{
Addr: addr,
Handler: handler,
// Headers-only Slowloris guard (independent of ReadTimeout).
ReadHeaderTimeout: 10 * time.Second,
// Full request read (headers + body). Above typical API JSON uploads.
ReadTimeout: 60 * time.Second,
// Long ceiling so streaming exports/sync can finish; see comment above.
WriteTimeout: 15 * time.Minute,
// Close keep-alive connections idle between requests.
IdleTimeout: 120 * time.Second,
}
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"net/http"
"testing"
"time"
)
func TestNewHTTPServerTimeouts(t *testing.T) {
t.Parallel()
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
srv := newHTTPServer(":0", handler)
if srv.Addr != ":0" {
t.Fatalf("Addr = %q, want :0", srv.Addr)
}
if srv.Handler == nil {
t.Fatal("Handler is nil")
}
if got, want := srv.ReadHeaderTimeout, 10*time.Second; got != want {
t.Fatalf("ReadHeaderTimeout = %v, want %v", got, want)
}
if got, want := srv.ReadTimeout, 60*time.Second; got != want {
t.Fatalf("ReadTimeout = %v, want %v", got, want)
}
// Long WriteTimeout preserves streaming feed exports/sync; must stay >> typical JSON handlers.
if got, want := srv.WriteTimeout, 15*time.Minute; got != want {
t.Fatalf("WriteTimeout = %v, want %v", got, want)
}
if got, want := srv.IdleTimeout, 120*time.Second; got != want {
t.Fatalf("IdleTimeout = %v, want %v", got, want)
}
}