package platform import ( "context" "database/sql" "io/fs" "log/slog" "net/http" "os" "path/filepath" "strings" "time" ) type App struct { config Config database *sql.DB logger *slog.Logger mux *http.ServeMux migrations []Migration startups []func(context.Context) error } func (a *App) RegisterStartup(startup func(context.Context) error) { a.startups = append(a.startups, startup) } func (a *App) Start(ctx context.Context) error { for _, startup := range a.startups { if err := startup(ctx); err != nil { return err } } return nil } func NewApp(cfg Config, database *sql.DB, logger *slog.Logger) *App { app := &App{config: cfg, database: database, logger: logger, mux: http.NewServeMux()} app.registerPlatformRoutes() return app } func (a *App) Config() Config { return a.config } func (a *App) Database() *sql.DB { return a.database } func (a *App) Logger() *slog.Logger { return a.logger } func (a *App) Handle(pattern string, handler http.Handler) { a.mux.Handle(pattern, handler) } func (a *App) HandleFunc(pattern string, handler http.HandlerFunc) { a.mux.HandleFunc(pattern, handler) } func (a *App) Handler() http.Handler { return requestSecurityHeaders(a.mux) } func (a *App) registerPlatformRoutes() { a.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "product": "sense"}) }) a.mux.HandleFunc("GET /readyz", a.ready) a.mux.HandleFunc("GET /api/v1/system/info", func(w http.ResponseWriter, _ *http.Request) { WriteJSON(w, http.StatusOK, map[string]any{ "product": "Sense", "database": a.config.DatabaseMode, "modules": []string{}, }) }) // Deliberately do not register upstream demo/code-generation routes. if a.config.StaticDir != "" { a.registerStaticFiles(a.config.StaticDir) } } func (a *App) ready(w http.ResponseWriter, r *http.Request) { if a.database == nil { WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "memory"}) return } ctx, cancel := timeLimitedContext(r, 2*time.Second) defer cancel() if err := a.database.PingContext(ctx); err != nil { WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready", "database": "unavailable"}) return } WriteJSON(w, http.StatusOK, map[string]string{"status": "ready", "database": "postgres"}) } func (a *App) registerStaticFiles(directory string) { absolute, err := filepath.Abs(directory) if err != nil { a.logger.Warn("invalid UI static directory", "error", err) return } fileSystem := os.DirFS(absolute) a.mux.Handle("GET /assets/", http.StripPrefix("/", http.FileServer(http.FS(fileSystem)))) a.mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(r.URL.Path)), "/") if path == "." || path == "" { path = "index.html" } if _, err := fs.Stat(fileSystem, path); err != nil { path = "index.html" } http.ServeFileFS(w, r, fileSystem, path) }) } func requestSecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "no-referrer") next.ServeHTTP(w, r) }) }