mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 07:41:27 +00:00
feat: phase 0 foundation — monorepo scaffold, backend, frontend, CI, docker
Establishes the skeleton every later phase builds on: Go+Gin API with zap logging, /api/healthz, SQLite+goose runner; Vite+React18+TS strict+ Tailwind v4 frontend with bento-mono theme tokens; openapi.yaml driving a generated TS client; oxlint with vendored anti-slop rules; GitHub CI; multi-stage Dockerfile -> 33.6MB distroless image; docker compose up serves the blank board. Generated with [Devin](https://devin.ai) Authored By: TDvorak <[email protected]> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e7f32949e9
commit
b60821350a
46 files changed
+6415
-10
No files matched your search
@@ -0,0 +1,42 @@
|
||||
// Package api wires the Gin router and /api handlers.
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NewRouter builds the HTTP handler: zap access log, recovery, /api routes.
|
||||
func NewRouter(logger *zap.Logger, db *sql.DB) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(accessLog(logger), gin.Recovery())
|
||||
|
||||
v1 := r.Group("/api")
|
||||
v1.GET("/healthz", func(c *gin.Context) {
|
||||
if err := db.PingContext(c.Request.Context()); err != nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "db unreachable"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// accessLog is gin.Logger backed by zap: one structured line per request.
|
||||
func accessLog(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
logger.Info("request",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Duration("latency", time.Since(start)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Package db opens the SQLite database and applies goose migrations.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrations embed.FS
|
||||
|
||||
// Open opens (creating if needed) the SQLite file at path and migrates it
|
||||
// to the latest schema. WAL + busy_timeout keep reads snappy under writes.
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)", path)
|
||||
sqlDB, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(migrations)
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return nil, fmt.Errorf("goose dialect: %w", err)
|
||||
}
|
||||
if err := goose.Up(sqlDB, "migrations"); err != nil {
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
return sqlDB, nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- Key/value store for user prefs (theme, renderer, ...). Values are JSON.
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value JSON NOT NULL
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE settings;
|
||||
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
<!doctype html><meta charset="utf-8"><title>Dash</title><p>Dash API is running. Frontend build is not embedded in this binary.</p>
|
||||
@@ -0,0 +1,41 @@
|
||||
// Package web serves the embedded frontend build (SPA with index.html
|
||||
// fallback). dist/ is populated by the frontend build in Docker/CI; the
|
||||
// committed .keep keeps //go:embed satisfied on a bare clone.
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
var dist embed.FS
|
||||
|
||||
// Register mounts the SPA on the router. Non-/api paths serve static files;
|
||||
// unknown paths fall back to index.html for client-side routing.
|
||||
func Register(r *gin.Engine) error {
|
||||
sub, err := fs.Sub(dist, "dist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileServer := http.FileServer(http.FS(sub))
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
if len(path) >= 4 && path[:4] == "/api" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if f, err := sub.Open(path[1:]); err == nil {
|
||||
_ = f.Close()
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
return
|
||||
}
|
||||
c.Request.URL.Path = "/"
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user