diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/.gitignore b/PROJECTS/advanced/monitor-the-situation-dashboard/.gitignore
new file mode 100644
index 00000000..48cf6544
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/.gitignore
@@ -0,0 +1,18 @@
+# ©AngelaMos | 2026
+# .gitignore
+
+.env
+.env.local
+.env.*.local
+
+backend/tmp/
+backend/bin/
+backend/coverage.out
+backend/coverage.html
+backend/keys/
+
+frontend/dist/
+frontend/node_modules/
+frontend/.vite/
+
+docs/
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.air.toml b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.air.toml
new file mode 100644
index 00000000..5f03fe95
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.air.toml
@@ -0,0 +1,29 @@
+# AngelaMos | 2026
+# .air.toml - Hot reload configuration
+
+root = "."
+tmp_dir = "tmp"
+
+[build]
+cmd = "go build -o ./tmp/main ./cmd/api"
+bin = "tmp/main"
+full_bin = "./tmp/main"
+include_ext = ["go", "yaml", "yml"]
+exclude_dir = ["tmp", "vendor", "bin", "keys", "migrations"]
+exclude_regex = ["_test\\.go"]
+delay = 1000
+stop_on_error = true
+send_interrupt = true
+kill_delay = 500
+
+[log]
+time = false
+
+[color]
+main = "cyan"
+watcher = "magenta"
+build = "yellow"
+runner = "green"
+
+[misc]
+clean_on_exit = true
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.env.example b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.env.example
new file mode 100644
index 00000000..f542823a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.env.example
@@ -0,0 +1,33 @@
+# AngelaMos | 2026
+# .env.example
+
+# Environment: development, staging, production
+ENVIRONMENT=development
+
+# Server
+HOST=0.0.0.0
+PORT=8080
+
+# Database
+DATABASE_URL=postgres://postgres:postgres@localhost:5432/app?sslmode=disable
+
+# Redis
+REDIS_URL=redis://localhost:6379/0
+
+# JWT
+JWT_PRIVATE_KEY_PATH=keys/private.pem
+JWT_PUBLIC_KEY_PATH=keys/public.pem
+ACCESS_TOKEN_EXPIRE_MINUTES=15
+REFRESH_TOKEN_EXPIRE_DAYS=7
+
+# Rate Limiting
+RATE_LIMIT_REQUESTS=100
+RATE_LIMIT_WINDOW=1m
+
+# OpenTelemetry
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+OTEL_SERVICE_NAME=go-backend
+
+# Logging
+LOG_LEVEL=debug
+LOG_FORMAT=text
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.gitignore b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.gitignore
new file mode 100644
index 00000000..3688c87b
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.gitignore
@@ -0,0 +1,44 @@
+# AngelaMos | 2026
+# .gitignore
+
+# Binaries
+bin/
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test
+*.test
+coverage.out
+coverage.html
+
+# Build
+tmp/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# Keys (sensitive)
+keys/*.pem
+keys/*.key
+
+# Vendor (if using)
+vendor/
+
+# Debug
+__debug_bin*
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.golangci.yml b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.golangci.yml
new file mode 100644
index 00000000..679c3e2c
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/.golangci.yml
@@ -0,0 +1,113 @@
+# AngelaMos | 2026
+# .golangci.yml
+
+version: "2"
+
+linters:
+ default: none
+ enable:
+ - errcheck
+ - govet
+ - gosec
+ - bodyclose
+ - nilerr
+ - errorlint
+ - exhaustive
+ - gocritic
+ - funlen
+ - gocognit
+ - dupl
+ - goconst
+ - ineffassign
+ - unused
+ - unconvert
+ - unparam
+ - testifylint
+ - fatcontext
+
+ settings:
+ errcheck:
+ check-type-assertions: true
+ check-blank: true
+
+ funlen:
+ lines: 100
+ statements: 50
+
+ gocognit:
+ min-complexity: 20
+
+ govet:
+ enable-all: true
+ disable:
+ - fieldalignment
+
+ revive:
+ rules:
+ - name: blank-imports
+ - name: context-as-argument
+ - name: context-keys-type
+ - name: error-return
+ - name: error-strings
+ - name: error-naming
+ - name: exported
+ - name: increment-decrement
+ - name: var-declaration
+ - name: package-comments
+ disabled: true
+ - name: range
+ - name: receiver-naming
+ - name: time-naming
+ - name: unexported-return
+ - name: indent-error-flow
+ - name: errorf
+ - name: empty-block
+ - name: superfluous-else
+ - name: unreachable-code
+
+ staticcheck:
+ checks:
+ - all
+
+ gosec:
+ excludes:
+ - G104
+
+ sloglint:
+ no-mixed-args: true
+ kv-only: true
+ context: all
+
+issues:
+ max-same-issues: 50
+ exclude-dirs:
+ - vendor
+ - testdata
+ exclude-rules:
+ - path: _test\.go
+ linters:
+ - funlen
+ - dupl
+ - goconst
+
+
+formatters:
+ enable:
+ - gci # Groups imports
+ - gofumpt # Whitespace
+ - golines # Vertical wrap
+ settings:
+ golines:
+ max-len: 80
+ reformat-tags: true
+ goimports:
+ local-prefixes:
+ - github.com/carterperez-dev/templates/go-backend
+ gci:
+ sections:
+ - standard
+ - default
+ - prefix(github.com/carterperez-dev)
+ custom-order: true
+ gofumpt:
+ extra-rules: true
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/cmd/api/main.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/cmd/api/main.go
new file mode 100644
index 00000000..150d2b52
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/cmd/api/main.go
@@ -0,0 +1,221 @@
+// AngelaMos | 2026
+// main.go
+
+package main
+
+import (
+ "context"
+ "flag"
+ "log/slog"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/go-chi/chi/v5"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/admin"
+ "github.com/carterperez-dev/templates/go-backend/internal/auth"
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+ "github.com/carterperez-dev/templates/go-backend/internal/health"
+ "github.com/carterperez-dev/templates/go-backend/internal/middleware"
+ "github.com/carterperez-dev/templates/go-backend/internal/server"
+ "github.com/carterperez-dev/templates/go-backend/internal/user"
+)
+
+const (
+ drainDelay = 5 * time.Second
+)
+
+func main() {
+ configPath := flag.String("config", "config.yaml", "path to config file")
+ flag.Parse()
+
+ if err := run(*configPath); err != nil {
+ slog.Error("application error", "error", err)
+ os.Exit(1)
+ }
+}
+
+//nolint:funlen // bootstrap code is inherently verbose
+func run(configPath string) error {
+ ctx, stop := signal.NotifyContext(
+ context.Background(),
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ )
+ defer stop()
+
+ cfg, err := config.Load(configPath)
+ if err != nil {
+ return err
+ }
+
+ logger := setupLogger(cfg.Log)
+ slog.SetDefault(logger)
+
+ logger.Info("starting application",
+ "name", cfg.App.Name,
+ "version", cfg.App.Version,
+ "environment", cfg.App.Environment,
+ )
+
+ var telemetry *core.Telemetry
+ if cfg.Otel.Enabled {
+ tel, telErr := core.NewTelemetry(ctx, cfg.Otel, cfg.App)
+ if telErr != nil {
+ logger.Warn("failed to initialize telemetry", "error", telErr)
+ } else {
+ telemetry = tel
+ logger.Info("OpenTelemetry tracer initialized",
+ "endpoint", cfg.Otel.Endpoint,
+ )
+ }
+ }
+
+ db, err := core.NewDatabase(ctx, cfg.Database)
+ if err != nil {
+ return err
+ }
+ logger.Info("database connected",
+ "max_open_conns", cfg.Database.MaxOpenConns,
+ "max_idle_conns", cfg.Database.MaxIdleConns,
+ )
+
+ redis, err := core.NewRedis(ctx, cfg.Redis)
+ if err != nil {
+ return err
+ }
+ logger.Info("redis connected",
+ "pool_size", cfg.Redis.PoolSize,
+ )
+
+ jwtManager, err := auth.NewJWTManager(cfg.JWT)
+ if err != nil {
+ return err
+ }
+ logger.Info("JWT manager initialized",
+ "algorithm", "ES256",
+ "key_id", jwtManager.GetKeyID(),
+ )
+
+ userRepo := user.NewRepository(db.DB)
+ userSvc := user.NewService(userRepo)
+ userHandler := user.NewHandler(userSvc)
+
+ authRepo := auth.NewRepository(db.DB)
+ authSvc := auth.NewService(authRepo, jwtManager, userSvc, redis.Client)
+ authHandler := auth.NewHandler(authSvc)
+
+ healthHandler := health.NewHandler(db, redis)
+
+ adminHandler := admin.NewHandler(admin.HandlerConfig{
+ DBStats: db.Stats,
+ RedisStats: redis.PoolStats,
+ DBPing: db.Ping,
+ RedisPing: redis.Ping,
+ })
+
+ srv := server.New(server.Config{
+ ServerConfig: cfg.Server,
+ HealthHandler: healthHandler,
+ Logger: logger,
+ })
+
+ router := srv.Router()
+
+ router.Use(middleware.RequestID)
+ router.Use(middleware.Logger(logger))
+ router.Use(
+ middleware.NewRateLimiter(redis.Client, middleware.RateLimitConfig{
+ Limit: middleware.PerMinute(
+ cfg.RateLimit.Requests,
+ cfg.RateLimit.Burst,
+ ),
+ FailOpen: true,
+ }).Handler,
+ )
+ router.Use(middleware.SecurityHeaders(cfg.App.Environment == "production"))
+ router.Use(middleware.CORS(cfg.CORS))
+
+ healthHandler.RegisterRoutes(router)
+
+ router.Get("/.well-known/jwks.json", jwtManager.GetJWKSHandler())
+
+ authenticator := middleware.Authenticator(jwtManager)
+ adminOnly := middleware.RequireAdmin
+
+ router.Route("/v1", func(r chi.Router) {
+ authHandler.RegisterRoutes(r, authenticator)
+
+ r.Post("/users", authHandler.Register)
+
+ userHandler.RegisterRoutes(r, authenticator)
+ userHandler.RegisterAdminRoutes(r, authenticator, adminOnly)
+ adminHandler.RegisterRoutes(r, authenticator, adminOnly)
+ })
+
+ errChan := make(chan error, 1)
+ go func() {
+ errChan <- srv.Start()
+ }()
+
+ select {
+ case err := <-errChan:
+ return err
+ case <-ctx.Done():
+ logger.Info("shutdown signal received")
+ }
+
+ shutdownCtx, cancel := context.WithTimeout(
+ context.Background(),
+ cfg.Server.ShutdownTimeout+drainDelay+5*time.Second,
+ )
+ defer cancel()
+
+ if err := srv.Shutdown(shutdownCtx, drainDelay); err != nil {
+ logger.Error("server shutdown error", "error", err)
+ }
+
+ if telemetry != nil {
+ if err := telemetry.Shutdown(shutdownCtx); err != nil {
+ logger.Error("telemetry shutdown error", "error", err)
+ }
+ }
+
+ if err := redis.Close(); err != nil {
+ logger.Error("redis close error", "error", err)
+ }
+
+ if err := db.Close(); err != nil {
+ logger.Error("database close error", "error", err)
+ }
+
+ logger.Info("application stopped")
+ return nil
+}
+
+func setupLogger(cfg config.LogConfig) *slog.Logger {
+ var handler slog.Handler
+
+ level := slog.LevelInfo
+ switch cfg.Level {
+ case "debug":
+ level = slog.LevelDebug
+ case "warn":
+ level = slog.LevelWarn
+ case "error":
+ level = slog.LevelError
+ }
+
+ opts := &slog.HandlerOptions{Level: level}
+
+ if cfg.Format == "json" {
+ handler = slog.NewJSONHandler(os.Stdout, opts)
+ } else {
+ handler = slog.NewTextHandler(os.Stdout, opts)
+ }
+
+ return slog.New(handler)
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/config.yaml b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/config.yaml
new file mode 100644
index 00000000..01aa28f0
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/config.yaml
@@ -0,0 +1,53 @@
+# AngelaMos | 2026
+# config.yaml - Default configuration
+
+app:
+ name: "Go Backend Template"
+ version: "1.0.0"
+
+server:
+ host: "0.0.0.0"
+ port: 8080
+ read_timeout: 30s
+ write_timeout: 30s
+ idle_timeout: 120s
+ shutdown_timeout: 15s
+
+database:
+ max_open_conns: 25
+ max_idle_conns: 5
+ conn_max_lifetime: 1h
+ conn_max_idle_time: 30m
+
+redis:
+ pool_size: 10
+ min_idle_conns: 5
+
+jwt:
+ access_token_expire: 15m
+ refresh_token_expire: 168h
+ issuer: "go-backend"
+
+rate_limit:
+ requests: 100
+ window: 1m
+ burst: 20
+
+cors:
+ allowed_origins:
+ - "http://localhost:3000"
+ - "http://localhost:3420"
+ allowed_methods:
+ - "GET"
+ - "POST"
+ - "PUT"
+ - "PATCH"
+ - "DELETE"
+ - "OPTIONS"
+ allowed_headers:
+ - "Accept"
+ - "Authorization"
+ - "Content-Type"
+ - "X-Request-ID"
+ allow_credentials: true
+ max_age: 300
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod
new file mode 100644
index 00000000..e5981e90
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod
@@ -0,0 +1,69 @@
+module github.com/carterperez-dev/templates/go-backend
+
+go 1.25.0
+
+require (
+ github.com/go-chi/chi/v5 v5.2.3
+ github.com/go-playground/validator/v10 v10.23.0
+ github.com/go-redis/redis_rate/v10 v10.0.1
+ github.com/google/uuid v1.6.0
+ github.com/jackc/pgx/v5 v5.7.2
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/knadh/koanf/parsers/yaml v1.1.0
+ github.com/knadh/koanf/providers/env v1.1.0
+ github.com/knadh/koanf/providers/file v1.2.1
+ github.com/knadh/koanf/v2 v2.1.2
+ github.com/lestrrat-go/jwx/v3 v3.0.12
+ github.com/redis/go-redis/v9 v9.7.0
+ go.opentelemetry.io/otel v1.33.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
+ go.opentelemetry.io/otel/sdk v1.33.0
+ go.opentelemetry.io/otel/trace v1.33.0
+ golang.org/x/crypto v0.43.0
+ golang.org/x/time v0.14.0
+ google.golang.org/grpc v1.68.1
+)
+
+require (
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/knadh/koanf/maps v0.1.2 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+ github.com/lestrrat-go/dsig v1.0.0 // indirect
+ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
+ github.com/lestrrat-go/httpcc v1.0.1 // indirect
+ github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
+ github.com/lestrrat-go/option v1.0.1 // indirect
+ github.com/lestrrat-go/option/v2 v2.0.0 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/segmentio/asm v1.2.1 // indirect
+ github.com/valyala/fastjson v1.6.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
+ go.opentelemetry.io/otel/metric v1.33.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.4.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.3 // indirect
+ golang.org/x/net v0.45.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+ google.golang.org/protobuf v1.35.2 // indirect
+)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum
new file mode 100644
index 00000000..0a9a9619
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum
@@ -0,0 +1,165 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
+github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo=
+github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
+github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
+github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4=
+github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
+github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc=
+github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY=
+github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
+github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
+github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
+github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
+github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
+github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
+github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
+github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
+github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
+github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
+go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
+go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
+go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
+go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
+go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
+go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
+go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
+go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
+go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
+golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
+google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
+google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/admin/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/admin/handler.go
new file mode 100644
index 00000000..4f75ab22
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/admin/handler.go
@@ -0,0 +1,208 @@
+// AngelaMos | 2026
+// handler.go
+
+package admin
+
+import (
+ "context"
+ "database/sql"
+ "net/http"
+ "runtime"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/redis/go-redis/v9"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+type AuthService interface {
+ InvalidateAllSessions(ctx context.Context) error
+}
+
+type Handler struct {
+ dbStats func() sql.DBStats
+ redisStats func() *redis.PoolStats
+ redisPing func(ctx context.Context) error
+ dbPing func(ctx context.Context) error
+ authSvc AuthService
+}
+
+type HandlerConfig struct {
+ DBStats func() sql.DBStats
+ RedisStats func() *redis.PoolStats
+ RedisPing func(ctx context.Context) error
+ DBPing func(ctx context.Context) error
+ AuthSvc AuthService
+}
+
+func NewHandler(cfg HandlerConfig) *Handler {
+ return &Handler{
+ dbStats: cfg.DBStats,
+ redisStats: cfg.RedisStats,
+ redisPing: cfg.RedisPing,
+ dbPing: cfg.DBPing,
+ authSvc: cfg.AuthSvc,
+ }
+}
+
+func (h *Handler) RegisterRoutes(
+ r chi.Router,
+ authenticator, adminOnly func(http.Handler) http.Handler,
+) {
+ r.Route("/admin", func(r chi.Router) {
+ r.Use(authenticator)
+ r.Use(adminOnly)
+
+ r.Get("/stats", h.GetSystemStats)
+ r.Get("/stats/db", h.GetDatabaseStats)
+ r.Get("/stats/redis", h.GetRedisStats)
+ r.Get("/stats/runtime", h.GetRuntimeStats)
+ })
+}
+
+func (h *Handler) GetSystemStats(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ dbHealthy := true
+ if h.dbPing != nil {
+ if err := h.dbPing(ctx); err != nil {
+ dbHealthy = false
+ }
+ }
+
+ redisHealthy := true
+ if h.redisPing != nil {
+ if err := h.redisPing(ctx); err != nil {
+ redisHealthy = false
+ }
+ }
+
+ var memStats runtime.MemStats
+ runtime.ReadMemStats(&memStats)
+
+ response := SystemStatsResponse{
+ Database: DatabaseStatus{
+ Healthy: dbHealthy,
+ Stats: h.getDBStats(),
+ },
+ Redis: RedisStatus{
+ Healthy: redisHealthy,
+ Stats: h.getRedisStats(),
+ },
+ Runtime: RuntimeStats{
+ GoVersion: runtime.Version(),
+ NumGoroutine: runtime.NumGoroutine(),
+ NumCPU: runtime.NumCPU(),
+ MemAlloc: memStats.Alloc,
+ MemSys: memStats.Sys,
+ NumGC: memStats.NumGC,
+ },
+ }
+
+ core.OK(w, response)
+}
+
+func (h *Handler) GetDatabaseStats(w http.ResponseWriter, r *http.Request) {
+ core.OK(w, h.getDBStats())
+}
+
+func (h *Handler) GetRedisStats(w http.ResponseWriter, r *http.Request) {
+ core.OK(w, h.getRedisStats())
+}
+
+func (h *Handler) GetRuntimeStats(w http.ResponseWriter, r *http.Request) {
+ var memStats runtime.MemStats
+ runtime.ReadMemStats(&memStats)
+
+ response := RuntimeStats{
+ GoVersion: runtime.Version(),
+ NumGoroutine: runtime.NumGoroutine(),
+ NumCPU: runtime.NumCPU(),
+ MemAlloc: memStats.Alloc,
+ MemSys: memStats.Sys,
+ NumGC: memStats.NumGC,
+ }
+
+ core.OK(w, response)
+}
+
+func (h *Handler) getDBStats() *DBPoolStats {
+ if h.dbStats == nil {
+ return nil
+ }
+
+ stats := h.dbStats()
+ return &DBPoolStats{
+ MaxOpenConnections: stats.MaxOpenConnections,
+ OpenConnections: stats.OpenConnections,
+ InUse: stats.InUse,
+ Idle: stats.Idle,
+ WaitCount: stats.WaitCount,
+ WaitDuration: stats.WaitDuration.String(),
+ MaxIdleClosed: stats.MaxIdleClosed,
+ MaxIdleTimeClosed: stats.MaxIdleTimeClosed,
+ MaxLifetimeClosed: stats.MaxLifetimeClosed,
+ }
+}
+
+func (h *Handler) getRedisStats() *RedisPoolStats {
+ if h.redisStats == nil {
+ return nil
+ }
+
+ stats := h.redisStats()
+ return &RedisPoolStats{
+ Hits: stats.Hits,
+ Misses: stats.Misses,
+ Timeouts: stats.Timeouts,
+ TotalConns: stats.TotalConns,
+ IdleConns: stats.IdleConns,
+ StaleConns: stats.StaleConns,
+ }
+}
+
+type SystemStatsResponse struct {
+ Database DatabaseStatus `json:"database"`
+ Redis RedisStatus `json:"redis"`
+ Runtime RuntimeStats `json:"runtime"`
+}
+
+type DatabaseStatus struct {
+ Healthy bool `json:"healthy"`
+ Stats *DBPoolStats `json:"stats,omitempty"`
+}
+
+type RedisStatus struct {
+ Healthy bool `json:"healthy"`
+ Stats *RedisPoolStats `json:"stats,omitempty"`
+}
+
+type DBPoolStats struct {
+ MaxOpenConnections int `json:"max_open_connections"`
+ OpenConnections int `json:"open_connections"`
+ InUse int `json:"in_use"`
+ Idle int `json:"idle"`
+ WaitCount int64 `json:"wait_count"`
+ WaitDuration string `json:"wait_duration"`
+ MaxIdleClosed int64 `json:"max_idle_closed"`
+ MaxIdleTimeClosed int64 `json:"max_idle_time_closed"`
+ MaxLifetimeClosed int64 `json:"max_lifetime_closed"`
+}
+
+type RedisPoolStats struct {
+ Hits uint32 `json:"hits"`
+ Misses uint32 `json:"misses"`
+ Timeouts uint32 `json:"timeouts"`
+ TotalConns uint32 `json:"total_conns"`
+ IdleConns uint32 `json:"idle_conns"`
+ StaleConns uint32 `json:"stale_conns"`
+}
+
+type RuntimeStats struct {
+ GoVersion string `json:"go_version"`
+ NumGoroutine int `json:"num_goroutine"`
+ NumCPU int `json:"num_cpu"`
+ MemAlloc uint64 `json:"mem_alloc_bytes"`
+ MemSys uint64 `json:"mem_sys_bytes"`
+ NumGC uint32 `json:"num_gc"`
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/dto.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/dto.go
new file mode 100644
index 00000000..9fc1cfa1
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/dto.go
@@ -0,0 +1,62 @@
+// AngelaMos | 2026
+// dto.go
+
+package auth
+
+import (
+ "time"
+)
+
+type LoginRequest struct {
+ Email string `json:"email" validate:"required,email,max=255"`
+ Password string `json:"password" validate:"required,min=8,max=128"`
+}
+
+type RegisterRequest struct {
+ Email string `json:"email" validate:"required,email,max=255"`
+ Password string `json:"password" validate:"required,min=8,max=128"`
+ Name string `json:"name" validate:"required,min=1,max=100"`
+}
+
+type RefreshRequest struct {
+ RefreshToken string `json:"refresh_token" validate:"required"`
+}
+
+type TokenResponse struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int `json:"expires_in"`
+ ExpiresAt time.Time `json:"expires_at"`
+}
+
+type UserResponse struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Role string `json:"role"`
+ Tier string `json:"tier"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type AuthResponse struct {
+ User UserResponse `json:"user"`
+ Tokens TokenResponse `json:"tokens"`
+}
+
+type SessionInfo struct {
+ ID string `json:"id"`
+ UserAgent string `json:"user_agent"`
+ IPAddress string `json:"ip_address"`
+ CreatedAt time.Time `json:"created_at"`
+ ExpiresAt time.Time `json:"expires_at"`
+}
+
+type SessionsResponse struct {
+ Sessions []SessionInfo `json:"sessions"`
+}
+
+type ChangePasswordRequest struct {
+ CurrentPassword string `json:"current_password" validate:"required"`
+ NewPassword string `json:"new_password" validate:"required,min=8,max=128"`
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/entity.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/entity.go
new file mode 100644
index 00000000..8ce8a4e5
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/entity.go
@@ -0,0 +1,47 @@
+// AngelaMos | 2026
+// entity.go
+
+package auth
+
+import (
+ "time"
+)
+
+type RefreshToken struct {
+ ID string `db:"id"`
+ UserID string `db:"user_id"`
+ TokenHash string `db:"token_hash"`
+ FamilyID string `db:"family_id"`
+ ExpiresAt time.Time `db:"expires_at"`
+ CreatedAt time.Time `db:"created_at"`
+ IsUsed bool `db:"is_used"`
+ UsedAt *time.Time `db:"used_at"`
+ RevokedAt *time.Time `db:"revoked_at"`
+ ReplacedByID *string `db:"replaced_by_id"`
+ UserAgent string `db:"user_agent"`
+ IPAddress string `db:"ip_address"`
+}
+
+func (t *RefreshToken) IsExpired() bool {
+ return time.Now().After(t.ExpiresAt)
+}
+
+func (t *RefreshToken) IsRevoked() bool {
+ return t.RevokedAt != nil
+}
+
+func (t *RefreshToken) IsValid() bool {
+ return !t.IsExpired() && !t.IsRevoked() && !t.IsUsed
+}
+
+func (t *RefreshToken) MarkAsUsed(replacedByID string) {
+ now := time.Now()
+ t.IsUsed = true
+ t.UsedAt = &now
+ t.ReplacedByID = &replacedByID
+}
+
+func (t *RefreshToken) Revoke() {
+ now := time.Now()
+ t.RevokedAt = &now
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/handler.go
new file mode 100644
index 00000000..16b3b6bd
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/handler.go
@@ -0,0 +1,316 @@
+// AngelaMos | 2026
+// handler.go
+
+package auth
+
+import (
+ "encoding/json"
+ "errors"
+ "net"
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-playground/validator/v10"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+ "github.com/carterperez-dev/templates/go-backend/internal/middleware"
+)
+
+type Handler struct {
+ service *Service
+ validator *validator.Validate
+}
+
+func NewHandler(service *Service) *Handler {
+ return &Handler{
+ service: service,
+ validator: validator.New(validator.WithRequiredStructEnabled()),
+ }
+}
+
+func (h *Handler) RegisterRoutes(
+ r chi.Router,
+ authenticator func(http.Handler) http.Handler,
+) {
+ r.Route("/auth", func(r chi.Router) {
+ r.Post("/login", h.Login)
+ r.Post("/register", h.Register)
+ r.Post("/refresh", h.Refresh)
+
+ r.Group(func(r chi.Router) {
+ r.Use(authenticator)
+ r.Get("/me", h.GetMe)
+ r.Post("/logout", h.Logout)
+ r.Post("/logout-all", h.LogoutAll)
+ r.Get("/sessions", h.GetSessions)
+ r.Delete("/sessions/{sessionID}", h.RevokeSession)
+ r.Post("/change-password", h.ChangePassword)
+ })
+ })
+}
+
+func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
+ var req LoginRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ userAgent := r.UserAgent()
+ ipAddress := extractIPAddress(r)
+
+ resp, err := h.service.Login(r.Context(), req, userAgent, ipAddress)
+ if err != nil {
+ if errors.Is(err, ErrInvalidCredentials) {
+ core.JSONError(
+ w,
+ core.UnauthorizedError("invalid email or password"),
+ )
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, resp)
+}
+
+func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
+ var req RegisterRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ userAgent := r.UserAgent()
+ ipAddress := extractIPAddress(r)
+
+ resp, err := h.service.Register(r.Context(), req, userAgent, ipAddress)
+ if err != nil {
+ if errors.Is(err, ErrEmailExists) {
+ core.JSONError(w, core.DuplicateError("email"))
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.Created(w, resp)
+}
+
+func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
+ var req RefreshRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ userAgent := r.UserAgent()
+ ipAddress := extractIPAddress(r)
+
+ resp, err := h.service.Refresh(
+ r.Context(),
+ req.RefreshToken,
+ userAgent,
+ ipAddress,
+ )
+ if err != nil {
+ if errors.Is(err, ErrTokenReuse) {
+ core.JSONError(w, core.NewAppError(
+ core.ErrTokenRevoked,
+ "security alert: token reuse detected, all sessions revoked",
+ http.StatusUnauthorized,
+ "TOKEN_REUSE_DETECTED",
+ ))
+ return
+ }
+ if errors.Is(err, core.ErrTokenExpired) {
+ core.JSONError(w, core.TokenExpiredError())
+ return
+ }
+ if errors.Is(err, core.ErrTokenRevoked) {
+ core.JSONError(w, core.TokenRevokedError())
+ return
+ }
+ if errors.Is(err, core.ErrTokenInvalid) {
+ core.JSONError(w, core.TokenInvalidError())
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, resp)
+}
+
+func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ var req RefreshRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.service.Logout(r.Context(), req.RefreshToken, userID); err != nil {
+ if errors.Is(err, core.ErrForbidden) {
+ core.Forbidden(w, "cannot revoke another user's token")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+func (h *Handler) LogoutAll(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ if err := h.service.LogoutAll(r.Context(), userID); err != nil {
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+func (h *Handler) GetSessions(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ sessions, err := h.service.GetActiveSessions(r.Context(), userID)
+ if err != nil {
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, SessionsResponse{Sessions: sessions})
+}
+
+func (h *Handler) RevokeSession(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ sessionID := chi.URLParam(r, "sessionID")
+ if sessionID == "" {
+ core.BadRequest(w, "session ID required")
+ return
+ }
+
+ if err := h.service.RevokeSession(r.Context(), userID, sessionID); err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "session")
+ return
+ }
+ if errors.Is(err, core.ErrForbidden) {
+ core.Forbidden(w, "cannot revoke another user's session")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ var req ChangePasswordRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ if err := h.service.ChangePassword(r.Context(), userID, req.CurrentPassword, req.NewPassword); err != nil {
+ if errors.Is(err, ErrInvalidCredentials) {
+ core.JSONError(
+ w,
+ core.UnauthorizedError("current password is incorrect"),
+ )
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+ if userID == "" {
+ core.Unauthorized(w, "")
+ return
+ }
+
+ user, err := h.service.GetCurrentUser(r.Context(), userID)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, user)
+}
+
+func extractIPAddress(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ ips := strings.Split(xff, ",")
+ return strings.TrimSpace(ips[len(ips)-1])
+ }
+
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return r.RemoteAddr
+ }
+
+ return ip
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/jwt.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/jwt.go
new file mode 100644
index 00000000..026cf056
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/jwt.go
@@ -0,0 +1,295 @@
+// AngelaMos | 2026
+// jwt.go
+
+package auth
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk"
+ "github.com/lestrrat-go/jwx/v3/jwt"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+ "github.com/carterperez-dev/templates/go-backend/internal/middleware"
+)
+
+type JWTManager struct {
+ privateKey jwk.Key
+ publicKey jwk.Key
+ publicJWKS jwk.Set
+ config config.JWTConfig
+}
+
+func NewJWTManager(cfg config.JWTConfig) (*JWTManager, error) {
+ privateKeyPEM, err := os.ReadFile(cfg.PrivateKeyPath)
+ if err != nil {
+ return nil, fmt.Errorf("read private key: %w", err)
+ }
+
+ privateKey, err := jwk.ParseKey(privateKeyPEM, jwk.WithPEM(true))
+ if err != nil {
+ return nil, fmt.Errorf("parse private key: %w", err)
+ }
+
+ if setErr := privateKey.Set(jwk.AlgorithmKey, jwa.ES256()); setErr != nil {
+ return nil, fmt.Errorf("set algorithm: %w", setErr)
+ }
+
+ keyID := uuid.New().String()[:8]
+ if setErr := privateKey.Set(jwk.KeyIDKey, keyID); setErr != nil {
+ return nil, fmt.Errorf("set key id: %w", setErr)
+ }
+
+ publicKey, err := privateKey.PublicKey()
+ if err != nil {
+ return nil, fmt.Errorf("derive public key: %w", err)
+ }
+
+ if setErr := publicKey.Set(jwk.KeyUsageKey, "sig"); setErr != nil {
+ return nil, fmt.Errorf("set key usage: %w", setErr)
+ }
+
+ publicJWKS := jwk.NewSet()
+ if addErr := publicJWKS.AddKey(publicKey); addErr != nil {
+ return nil, fmt.Errorf("add key to set: %w", addErr)
+ }
+
+ return &JWTManager{
+ privateKey: privateKey,
+ publicKey: publicKey,
+ publicJWKS: publicJWKS,
+ config: cfg,
+ }, nil
+}
+
+func GenerateKeyPair(privateKeyPath, publicKeyPath string) error {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return fmt.Errorf("generate key: %w", err)
+ }
+
+ jwkPrivate, err := jwk.Import(privateKey)
+ if err != nil {
+ return fmt.Errorf("import private key: %w", err)
+ }
+
+ keyID := uuid.New().String()[:8]
+ if setErr := jwkPrivate.Set(jwk.KeyIDKey, keyID); setErr != nil {
+ return fmt.Errorf("set key id: %w", setErr)
+ }
+ if setErr := jwkPrivate.Set(jwk.AlgorithmKey, jwa.ES256()); setErr != nil {
+ return fmt.Errorf("set algorithm: %w", setErr)
+ }
+
+ privatePEM, err := jwk.Pem(jwkPrivate)
+ if err != nil {
+ return fmt.Errorf("encode private key: %w", err)
+ }
+
+ if writeErr := os.WriteFile(privateKeyPath, privatePEM, 0o600); writeErr != nil {
+ return fmt.Errorf("write private key: %w", writeErr)
+ }
+
+ jwkPublic, err := jwkPrivate.PublicKey()
+ if err != nil {
+ return fmt.Errorf("derive public key: %w", err)
+ }
+
+ publicPEM, err := jwk.Pem(jwkPublic)
+ if err != nil {
+ return fmt.Errorf("encode public key: %w", err)
+ }
+
+ //nolint:gosec // G306: public key is intentionally world-readable
+ if writeErr := os.WriteFile(publicKeyPath, publicPEM, 0o644); writeErr != nil {
+ return fmt.Errorf("write public key: %w", writeErr)
+ }
+
+ return nil
+}
+
+type AccessTokenClaims struct {
+ UserID string `json:"sub"`
+ Role string `json:"role"`
+ Tier string `json:"tier"`
+ TokenVersion int `json:"token_version"`
+}
+
+func (m *JWTManager) CreateAccessToken(
+ claims AccessTokenClaims,
+) (string, error) {
+ now := time.Now()
+
+ token, err := jwt.NewBuilder().
+ JwtID(uuid.New().String()).
+ Issuer(m.config.Issuer).
+ Audience([]string{m.config.Audience}).
+ Subject(claims.UserID).
+ IssuedAt(now).
+ Expiration(now.Add(m.config.AccessTokenExpire)).
+ NotBefore(now).
+ Claim("role", claims.Role).
+ Claim("tier", claims.Tier).
+ Claim("token_version", claims.TokenVersion).
+ Claim("type", "access").
+ Build()
+ if err != nil {
+ return "", fmt.Errorf("build token: %w", err)
+ }
+
+ signed, err := jwt.Sign(token, jwt.WithKey(jwa.ES256(), m.privateKey))
+ if err != nil {
+ return "", fmt.Errorf("sign token: %w", err)
+ }
+
+ return string(signed), nil
+}
+
+func (m *JWTManager) VerifyAccessToken(
+ ctx context.Context,
+ tokenString string,
+) (*middleware.AccessTokenClaims, error) {
+ token, err := jwt.Parse(
+ []byte(tokenString),
+ jwt.WithKey(jwa.ES256(), m.publicKey),
+ jwt.WithValidate(true),
+ jwt.WithIssuer(m.config.Issuer),
+ jwt.WithAudience(m.config.Audience),
+ )
+ if err != nil {
+ if isTokenExpiredError(err) {
+ return nil, fmt.Errorf("verify token: %w", core.ErrTokenExpired)
+ }
+ return nil, fmt.Errorf("verify token: %w", core.ErrTokenInvalid)
+ }
+
+ var tokenType string
+ if err := token.Get("type", &tokenType); err != nil ||
+ tokenType != "access" {
+ return nil, fmt.Errorf(
+ "verify token: invalid token type: %w",
+ core.ErrTokenInvalid,
+ )
+ }
+
+ subject, ok := token.Subject()
+ if !ok || subject == "" {
+ return nil, fmt.Errorf(
+ "verify token: missing subject: %w",
+ core.ErrTokenInvalid,
+ )
+ }
+
+ var roleStr string
+ if err := token.Get("role", &roleStr); err != nil {
+ return nil, fmt.Errorf(
+ "verify token: missing role claim: %w",
+ core.ErrTokenInvalid,
+ )
+ }
+
+ var tierStr string
+ if err := token.Get("tier", &tierStr); err != nil {
+ return nil, fmt.Errorf(
+ "verify token: missing tier claim: %w",
+ core.ErrTokenInvalid,
+ )
+ }
+
+ var versionFloat float64
+ if err := token.Get("token_version", &versionFloat); err != nil {
+ return nil, fmt.Errorf(
+ "verify token: missing token_version claim: %w",
+ core.ErrTokenInvalid,
+ )
+ }
+
+ return &middleware.AccessTokenClaims{
+ UserID: subject,
+ Role: roleStr,
+ Tier: tierStr,
+ TokenVersion: int(versionFloat),
+ }, nil
+}
+
+func isTokenExpiredError(err error) bool {
+ if err == nil {
+ return false
+ }
+ errStr := err.Error()
+ return strings.Contains(errStr, "exp") &&
+ strings.Contains(errStr, "not satisfied")
+}
+
+func (m *JWTManager) GetJWKSHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+
+ if err := json.NewEncoder(w).Encode(m.publicJWKS); err != nil {
+ http.Error(
+ w,
+ "Internal Server Error",
+ http.StatusInternalServerError,
+ )
+ return
+ }
+ }
+}
+
+func (m *JWTManager) GetPublicKey() jwk.Key {
+ return m.publicKey
+}
+
+func (m *JWTManager) GetKeyID() string {
+ var kid string
+ //nolint:errcheck // key ID always set during NewJWTManager init
+ _ = m.privateKey.Get(jwk.KeyIDKey, &kid)
+ return kid
+}
+
+type RefreshTokenData struct {
+ Token string
+ Hash string
+ ExpiresAt time.Time
+ FamilyID string
+}
+
+func (m *JWTManager) CreateRefreshToken(
+ userID, familyID string,
+) (*RefreshTokenData, error) {
+ token, err := core.GenerateRefreshToken()
+ if err != nil {
+ return nil, fmt.Errorf("generate refresh token: %w", err)
+ }
+
+ hash := core.HashToken(token)
+ expiresAt := time.Now().Add(m.config.RefreshTokenExpire)
+
+ if familyID == "" {
+ familyID = uuid.New().String()
+ }
+
+ return &RefreshTokenData{
+ Token: token,
+ Hash: hash,
+ ExpiresAt: expiresAt,
+ FamilyID: familyID,
+ }, nil
+}
+
+func (m *JWTManager) VerifyRefreshTokenHash(token, storedHash string) bool {
+ return core.CompareTokenHash(token, storedHash)
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/repository.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/repository.go
new file mode 100644
index 00000000..2abb0360
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/repository.go
@@ -0,0 +1,236 @@
+// AngelaMos | 2026
+// repository.go
+
+package auth
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+type Repository interface {
+ Create(ctx context.Context, token *RefreshToken) error
+ FindByHash(ctx context.Context, tokenHash string) (*RefreshToken, error)
+ FindByID(ctx context.Context, id string) (*RefreshToken, error)
+ MarkAsUsed(ctx context.Context, id, replacedByID string) error
+ RevokeByID(ctx context.Context, id string) error
+ RevokeByFamilyID(ctx context.Context, familyID string) error
+ RevokeAllForUser(ctx context.Context, userID string) error
+ GetActiveSessionsForUser(
+ ctx context.Context,
+ userID string,
+ ) ([]RefreshToken, error)
+ DeleteExpired(ctx context.Context) (int64, error)
+}
+
+type repository struct {
+ db core.DBTX
+}
+
+func NewRepository(db core.DBTX) Repository {
+ return &repository{db: db}
+}
+
+func (r *repository) Create(ctx context.Context, token *RefreshToken) error {
+ query := `
+ INSERT INTO refresh_tokens (
+ id, user_id, token_hash, family_id, expires_at,
+ user_agent, ip_address
+ ) VALUES (
+ $1, $2, $3, $4, $5, $6, $7
+ )
+ RETURNING created_at`
+
+ err := r.db.GetContext(ctx, &token.CreatedAt, query,
+ token.ID,
+ token.UserID,
+ token.TokenHash,
+ token.FamilyID,
+ token.ExpiresAt,
+ token.UserAgent,
+ token.IPAddress,
+ )
+ if err != nil {
+ return fmt.Errorf("create refresh token: %w", err)
+ }
+
+ return nil
+}
+
+func (r *repository) FindByHash(
+ ctx context.Context,
+ tokenHash string,
+) (*RefreshToken, error) {
+ query := `
+ SELECT
+ id, user_id, token_hash, family_id, expires_at, created_at,
+ is_used, used_at, revoked_at, replaced_by_id, user_agent, ip_address
+ FROM refresh_tokens
+ WHERE token_hash = $1`
+
+ var token RefreshToken
+ err := r.db.GetContext(ctx, &token, query, tokenHash)
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, fmt.Errorf("find refresh token: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("find refresh token: %w", err)
+ }
+
+ return &token, nil
+}
+
+func (r *repository) FindByID(
+ ctx context.Context,
+ id string,
+) (*RefreshToken, error) {
+ query := `
+ SELECT
+ id, user_id, token_hash, family_id, expires_at, created_at,
+ is_used, used_at, revoked_at, replaced_by_id, user_agent, ip_address
+ FROM refresh_tokens
+ WHERE id = $1`
+
+ var token RefreshToken
+ err := r.db.GetContext(ctx, &token, query, id)
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, fmt.Errorf("find refresh token: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("find refresh token: %w", err)
+ }
+
+ return &token, nil
+}
+
+func (r *repository) MarkAsUsed(
+ ctx context.Context,
+ id, replacedByID string,
+) error {
+ query := `
+ UPDATE refresh_tokens
+ SET is_used = true, used_at = NOW(), replaced_by_id = $2
+ WHERE id = $1 AND is_used = false`
+
+ result, err := r.db.ExecContext(ctx, query, id, replacedByID)
+ if err != nil {
+ return fmt.Errorf("mark refresh token as used: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("mark refresh token as used: %w", err)
+ }
+
+ if rows == 0 {
+ return fmt.Errorf("mark refresh token as used: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+
+func (r *repository) RevokeByID(ctx context.Context, id string) error {
+ query := `
+ UPDATE refresh_tokens
+ SET revoked_at = NOW()
+ WHERE id = $1 AND revoked_at IS NULL`
+
+ result, err := r.db.ExecContext(ctx, query, id)
+ if err != nil {
+ return fmt.Errorf("revoke refresh token: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("revoke refresh token: %w", err)
+ }
+
+ if rows == 0 {
+ return fmt.Errorf("revoke refresh token: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+
+func (r *repository) RevokeByFamilyID(
+ ctx context.Context,
+ familyID string,
+) error {
+ query := `
+ UPDATE refresh_tokens
+ SET revoked_at = NOW()
+ WHERE family_id = $1 AND revoked_at IS NULL`
+
+ _, err := r.db.ExecContext(ctx, query, familyID)
+ if err != nil {
+ return fmt.Errorf("revoke token family: %w", err)
+ }
+
+ return nil
+}
+
+func (r *repository) RevokeAllForUser(
+ ctx context.Context,
+ userID string,
+) error {
+ query := `
+ UPDATE refresh_tokens
+ SET revoked_at = NOW()
+ WHERE user_id = $1 AND revoked_at IS NULL`
+
+ _, err := r.db.ExecContext(ctx, query, userID)
+ if err != nil {
+ return fmt.Errorf("revoke all user tokens: %w", err)
+ }
+
+ return nil
+}
+
+func (r *repository) GetActiveSessionsForUser(
+ ctx context.Context,
+ userID string,
+) ([]RefreshToken, error) {
+ query := `
+ SELECT
+ id, user_id, token_hash, family_id, expires_at, created_at,
+ is_used, used_at, revoked_at, replaced_by_id, user_agent, ip_address
+ FROM refresh_tokens
+ WHERE user_id = $1
+ AND revoked_at IS NULL
+ AND is_used = false
+ AND expires_at > NOW()
+ ORDER BY created_at DESC`
+
+ var tokens []RefreshToken
+ err := r.db.SelectContext(ctx, &tokens, query, userID)
+ if err != nil {
+ return nil, fmt.Errorf("get active sessions: %w", err)
+ }
+
+ return tokens, nil
+}
+
+func (r *repository) DeleteExpired(ctx context.Context) (int64, error) {
+ query := `
+ DELETE FROM refresh_tokens
+ WHERE expires_at < $1`
+
+ cutoff := time.Now().Add(-24 * time.Hour)
+
+ result, err := r.db.ExecContext(ctx, query, cutoff)
+ if err != nil {
+ return 0, fmt.Errorf("delete expired tokens: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return 0, fmt.Errorf("delete expired tokens: %w", err)
+ }
+
+ return rows, nil
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/service.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/service.go
new file mode 100644
index 00000000..3629051c
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/auth/service.go
@@ -0,0 +1,411 @@
+// AngelaMos | 2026
+// service.go
+
+package auth
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/redis/go-redis/v9"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+var (
+ ErrInvalidCredentials = errors.New("invalid credentials")
+ ErrTokenReuse = errors.New("token reuse detected")
+ ErrEmailExists = errors.New("email already exists")
+)
+
+type UserInfo struct {
+ ID string
+ Email string
+ Name string
+ PasswordHash string
+ Role string
+ Tier string
+ TokenVersion int
+}
+
+type UserProvider interface {
+ GetByEmail(ctx context.Context, email string) (*UserInfo, error)
+ GetByID(ctx context.Context, id string) (*UserInfo, error)
+ Create(
+ ctx context.Context,
+ email, passwordHash, name string,
+ ) (*UserInfo, error)
+ IncrementTokenVersion(ctx context.Context, userID string) error
+ UpdatePassword(ctx context.Context, userID, passwordHash string) error
+}
+
+type Service struct {
+ repo Repository
+ jwt *JWTManager
+ userProvider UserProvider
+ redis *redis.Client
+ blacklistTTL time.Duration
+}
+
+func NewService(
+ repo Repository,
+ jwt *JWTManager,
+ userProvider UserProvider,
+ redisClient *redis.Client,
+) *Service {
+ return &Service{
+ repo: repo,
+ jwt: jwt,
+ userProvider: userProvider,
+ redis: redisClient,
+ blacklistTTL: 15 * time.Minute,
+ }
+}
+
+func (s *Service) Login(
+ ctx context.Context,
+ req LoginRequest,
+ userAgent, ipAddress string,
+) (*AuthResponse, error) {
+ user, err := s.userProvider.GetByEmail(ctx, req.Email)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ //nolint:errcheck // timing attack prevention - always verify to prevent enumeration
+ _, _, _ = core.VerifyPasswordTimingSafe(req.Password, nil)
+ return nil, ErrInvalidCredentials
+ }
+ return nil, fmt.Errorf("get user: %w", err)
+ }
+
+ valid, newHash, err := core.VerifyPasswordTimingSafe(
+ req.Password,
+ &user.PasswordHash,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("verify password: %w", err)
+ }
+
+ if !valid {
+ return nil, ErrInvalidCredentials
+ }
+
+ if newHash != "" {
+ //nolint:errcheck // best-effort rehash upgrade
+ _ = s.userProvider.UpdatePassword(ctx, user.ID, newHash)
+ }
+
+ return s.createAuthResponse(ctx, user, userAgent, ipAddress, "", nil)
+}
+
+func (s *Service) Register(
+ ctx context.Context,
+ req RegisterRequest,
+ userAgent, ipAddress string,
+) (*AuthResponse, error) {
+ passwordHash, err := core.HashPassword(req.Password)
+ if err != nil {
+ return nil, fmt.Errorf("hash password: %w", err)
+ }
+
+ user, err := s.userProvider.Create(ctx, req.Email, passwordHash, req.Name)
+ if err != nil {
+ if errors.Is(err, core.ErrDuplicateKey) {
+ return nil, ErrEmailExists
+ }
+ return nil, fmt.Errorf("create user: %w", err)
+ }
+
+ return s.createAuthResponse(ctx, user, userAgent, ipAddress, "", nil)
+}
+
+func (s *Service) Refresh(
+ ctx context.Context,
+ refreshToken, userAgent, ipAddress string,
+) (*AuthResponse, error) {
+ tokenHash := core.HashToken(refreshToken)
+
+ storedToken, err := s.repo.FindByHash(ctx, tokenHash)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ return nil, fmt.Errorf("refresh: %w", core.ErrTokenInvalid)
+ }
+ return nil, fmt.Errorf("find token: %w", err)
+ }
+
+ if storedToken.IsUsed {
+ //nolint:errcheck // security revocation continues regardless
+ _ = s.repo.RevokeByFamilyID(ctx, storedToken.FamilyID)
+ return nil, ErrTokenReuse
+ }
+
+ if !storedToken.IsValid() {
+ if storedToken.IsRevoked() {
+ return nil, fmt.Errorf("refresh: %w", core.ErrTokenRevoked)
+ }
+ return nil, fmt.Errorf("refresh: %w", core.ErrTokenExpired)
+ }
+
+ user, err := s.userProvider.GetByID(ctx, storedToken.UserID)
+ if err != nil {
+ return nil, fmt.Errorf("get user: %w", err)
+ }
+
+ return s.createAuthResponse(
+ ctx,
+ user,
+ userAgent,
+ ipAddress,
+ storedToken.FamilyID,
+ &storedToken.ID,
+ )
+}
+
+func (s *Service) Logout(
+ ctx context.Context,
+ refreshToken, userID string,
+) error {
+ tokenHash := core.HashToken(refreshToken)
+
+ storedToken, err := s.repo.FindByHash(ctx, tokenHash)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ return nil
+ }
+ return fmt.Errorf("find token: %w", err)
+ }
+
+ if storedToken.UserID != userID {
+ return fmt.Errorf("logout: %w", core.ErrForbidden)
+ }
+
+ if err := s.repo.RevokeByID(ctx, storedToken.ID); err != nil &&
+ !errors.Is(err, core.ErrNotFound) {
+ return fmt.Errorf("revoke token: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Service) LogoutAll(ctx context.Context, userID string) error {
+ if err := s.repo.RevokeAllForUser(ctx, userID); err != nil {
+ return fmt.Errorf("revoke all tokens: %w", err)
+ }
+
+ if err := s.userProvider.IncrementTokenVersion(ctx, userID); err != nil {
+ return fmt.Errorf("increment token version: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Service) RevokeAccessToken(
+ ctx context.Context,
+ jti string,
+ expiresAt time.Time,
+) error {
+ key := "blacklist:" + jti
+ ttl := time.Until(expiresAt)
+
+ if ttl <= 0 {
+ return nil
+ }
+
+ if err := s.redis.Set(ctx, key, "1", ttl).Err(); err != nil {
+ return fmt.Errorf("blacklist token: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Service) IsAccessTokenBlacklisted(
+ ctx context.Context,
+ jti string,
+) (bool, error) {
+ key := "blacklist:" + jti
+
+ exists, err := s.redis.Exists(ctx, key).Result()
+ if err != nil {
+ return false, fmt.Errorf("check blacklist: %w", err)
+ }
+
+ return exists > 0, nil
+}
+
+func (s *Service) GetActiveSessions(
+ ctx context.Context,
+ userID string,
+) ([]SessionInfo, error) {
+ tokens, err := s.repo.GetActiveSessionsForUser(ctx, userID)
+ if err != nil {
+ return nil, fmt.Errorf("get sessions: %w", err)
+ }
+
+ sessions := make([]SessionInfo, 0, len(tokens))
+ for _, t := range tokens {
+ sessions = append(sessions, SessionInfo{
+ ID: t.ID,
+ UserAgent: t.UserAgent,
+ IPAddress: t.IPAddress,
+ CreatedAt: t.CreatedAt,
+ ExpiresAt: t.ExpiresAt,
+ })
+ }
+
+ return sessions, nil
+}
+
+func (s *Service) RevokeSession(
+ ctx context.Context,
+ userID, sessionID string,
+) error {
+ token, err := s.repo.FindByID(ctx, sessionID)
+ if err != nil {
+ return fmt.Errorf("find session: %w", err)
+ }
+
+ if token.UserID != userID {
+ return fmt.Errorf("revoke session: %w", core.ErrForbidden)
+ }
+
+ if err := s.repo.RevokeByID(ctx, sessionID); err != nil {
+ return fmt.Errorf("revoke session: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Service) ChangePassword(
+ ctx context.Context,
+ userID, currentPassword, newPassword string,
+) error {
+ user, err := s.userProvider.GetByID(ctx, userID)
+ if err != nil {
+ return fmt.Errorf("get user: %w", err)
+ }
+
+ valid, _, err := core.VerifyPasswordWithRehash(
+ currentPassword,
+ user.PasswordHash,
+ )
+ if err != nil {
+ return fmt.Errorf("verify password: %w", err)
+ }
+
+ if !valid {
+ return ErrInvalidCredentials
+ }
+
+ newHash, err := core.HashPassword(newPassword)
+ if err != nil {
+ return fmt.Errorf("hash password: %w", err)
+ }
+
+ if err := s.userProvider.UpdatePassword(ctx, userID, newHash); err != nil {
+ return fmt.Errorf("update password: %w", err)
+ }
+
+ if err := s.LogoutAll(ctx, userID); err != nil {
+ return fmt.Errorf("logout all: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Service) ValidateTokenVersion(
+ ctx context.Context,
+ userID string,
+ tokenVersion int,
+) error {
+ user, err := s.userProvider.GetByID(ctx, userID)
+ if err != nil {
+ return fmt.Errorf("get user: %w", err)
+ }
+
+ if tokenVersion < user.TokenVersion {
+ return fmt.Errorf("validate token version: %w", core.ErrTokenRevoked)
+ }
+
+ return nil
+}
+
+func (s *Service) GetCurrentUser(
+ ctx context.Context,
+ userID string,
+) (*UserResponse, error) {
+ user, err := s.userProvider.GetByID(ctx, userID)
+ if err != nil {
+ return nil, err
+ }
+
+ return &UserResponse{
+ ID: user.ID,
+ Email: user.Email,
+ Name: user.Name,
+ Role: user.Role,
+ Tier: user.Tier,
+ }, nil
+}
+
+func (s *Service) createAuthResponse(
+ ctx context.Context,
+ user *UserInfo,
+ userAgent, ipAddress, familyID string,
+ oldTokenID *string,
+) (*AuthResponse, error) {
+ accessToken, err := s.jwt.CreateAccessToken(AccessTokenClaims{
+ UserID: user.ID,
+ Role: user.Role,
+ Tier: user.Tier,
+ TokenVersion: user.TokenVersion,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("create access token: %w", err)
+ }
+
+ refreshData, err := s.jwt.CreateRefreshToken(user.ID, familyID)
+ if err != nil {
+ return nil, fmt.Errorf("create refresh token: %w", err)
+ }
+
+ newTokenID := uuid.New().String()
+
+ refreshTokenEntity := &RefreshToken{
+ ID: newTokenID,
+ UserID: user.ID,
+ TokenHash: refreshData.Hash,
+ FamilyID: refreshData.FamilyID,
+ ExpiresAt: refreshData.ExpiresAt,
+ UserAgent: userAgent,
+ IPAddress: ipAddress,
+ }
+
+ if err := s.repo.Create(ctx, refreshTokenEntity); err != nil {
+ return nil, fmt.Errorf("store refresh token: %w", err)
+ }
+
+ if oldTokenID != nil {
+ //nolint:errcheck // best-effort token chain tracking
+ _ = s.repo.MarkAsUsed(ctx, *oldTokenID, newTokenID)
+ }
+
+ return &AuthResponse{
+ User: UserResponse{
+ ID: user.ID,
+ Email: user.Email,
+ Name: user.Name,
+ Role: user.Role,
+ Tier: user.Tier,
+ CreatedAt: time.Now(),
+ },
+ Tokens: TokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshData.Token,
+ TokenType: "Bearer",
+ ExpiresIn: int(15 * time.Minute / time.Second),
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ },
+ }, nil
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/config/config.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/config/config.go
new file mode 100644
index 00000000..ff266e50
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/config/config.go
@@ -0,0 +1,302 @@
+// AngelaMos | 2026
+// config.go
+
+package config
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/env"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+)
+
+type Config struct {
+ App AppConfig `koanf:"app"`
+ Server ServerConfig `koanf:"server"`
+ Database DatabaseConfig `koanf:"database"`
+ Redis RedisConfig `koanf:"redis"`
+ JWT JWTConfig `koanf:"jwt"`
+ RateLimit RateLimitConfig `koanf:"rate_limit"`
+ CORS CORSConfig `koanf:"cors"`
+ Log LogConfig `koanf:"log"`
+ Otel OtelConfig `koanf:"otel"`
+}
+
+type AppConfig struct {
+ Name string `koanf:"name"`
+ Version string `koanf:"version"`
+ Environment string `koanf:"environment"`
+}
+
+type ServerConfig struct {
+ Host string `koanf:"host"`
+ Port int `koanf:"port"`
+ ReadTimeout time.Duration `koanf:"read_timeout"`
+ WriteTimeout time.Duration `koanf:"write_timeout"`
+ IdleTimeout time.Duration `koanf:"idle_timeout"`
+ ShutdownTimeout time.Duration `koanf:"shutdown_timeout"`
+}
+
+type DatabaseConfig struct {
+ URL string `koanf:"url"`
+ MaxOpenConns int `koanf:"max_open_conns"`
+ MaxIdleConns int `koanf:"max_idle_conns"`
+ ConnMaxLifetime time.Duration `koanf:"conn_max_lifetime"`
+ ConnMaxIdleTime time.Duration `koanf:"conn_max_idle_time"`
+}
+
+type RedisConfig struct {
+ URL string `koanf:"url"`
+ PoolSize int `koanf:"pool_size"`
+ MinIdleConns int `koanf:"min_idle_conns"`
+}
+
+type JWTConfig struct {
+ PrivateKeyPath string `koanf:"private_key_path"`
+ PublicKeyPath string `koanf:"public_key_path"`
+ AccessTokenExpire time.Duration `koanf:"access_token_expire"`
+ RefreshTokenExpire time.Duration `koanf:"refresh_token_expire"`
+ Issuer string `koanf:"issuer"`
+ Audience string `koanf:"audience"`
+}
+
+type RateLimitConfig struct {
+ Requests int `koanf:"requests"`
+ Window time.Duration `koanf:"window"`
+ Burst int `koanf:"burst"`
+}
+
+type CORSConfig struct {
+ AllowedOrigins []string `koanf:"allowed_origins"`
+ AllowedMethods []string `koanf:"allowed_methods"`
+ AllowedHeaders []string `koanf:"allowed_headers"`
+ AllowCredentials bool `koanf:"allow_credentials"`
+ MaxAge int `koanf:"max_age"`
+}
+
+type LogConfig struct {
+ Level string `koanf:"level"`
+ Format string `koanf:"format"`
+}
+
+type OtelConfig struct {
+ Endpoint string `koanf:"endpoint"`
+ ServiceName string `koanf:"service_name"`
+ Enabled bool `koanf:"enabled"`
+ Insecure bool `koanf:"insecure"`
+ SampleRate float64 `koanf:"sample_rate"`
+}
+
+var (
+ cfg *Config
+ once sync.Once
+)
+
+func Load(configPath string) (*Config, error) {
+ var loadErr error
+
+ once.Do(func() {
+ k := koanf.New(".")
+
+ if err := loadDefaults(k); err != nil {
+ loadErr = fmt.Errorf("load defaults: %w", err)
+ return
+ }
+
+ if configPath != "" {
+ if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil {
+ loadErr = fmt.Errorf("load config file: %w", err)
+ return
+ }
+ }
+
+ if err := k.Load(env.Provider("", ".", envKeyReplacer), nil); err != nil {
+ loadErr = fmt.Errorf("load env vars: %w", err)
+ return
+ }
+
+ cfg = &Config{}
+ if err := k.Unmarshal("", cfg); err != nil {
+ loadErr = fmt.Errorf("unmarshal config: %w", err)
+ return
+ }
+
+ if err := validate(cfg); err != nil {
+ loadErr = fmt.Errorf("validate config: %w", err)
+ return
+ }
+ })
+
+ if loadErr != nil {
+ return nil, loadErr
+ }
+
+ return cfg, nil
+}
+
+func Get() *Config {
+ if cfg == nil {
+ panic("config not loaded: call Load() first")
+ }
+ return cfg
+}
+
+func loadDefaults(k *koanf.Koanf) error {
+ defaults := map[string]any{
+ "app.name": "Go Backend",
+ "app.version": "1.0.0",
+ "app.environment": "development",
+
+ "server.host": "0.0.0.0",
+ "server.port": 8080,
+ "server.read_timeout": "30s",
+ "server.write_timeout": "30s",
+ "server.idle_timeout": "120s",
+ "server.shutdown_timeout": "15s",
+
+ "database.max_open_conns": 25,
+ "database.max_idle_conns": 5,
+ "database.conn_max_lifetime": "1h",
+ "database.conn_max_idle_time": "30m",
+
+ "redis.pool_size": 10,
+ "redis.min_idle_conns": 5,
+
+ "jwt.access_token_expire": "15m",
+ "jwt.refresh_token_expire": "168h",
+ "jwt.issuer": "go-backend",
+ "jwt.audience": "go-backend-api",
+ "jwt.private_key_path": "keys/private.pem",
+ "jwt.public_key_path": "keys/public.pem",
+
+ "rate_limit.requests": 100,
+ "rate_limit.window": "1m",
+ "rate_limit.burst": 20,
+
+ "cors.allowed_origins": []string{"http://localhost:3000"},
+ "cors.allowed_methods": []string{
+ "GET",
+ "POST",
+ "PUT",
+ "PATCH",
+ "DELETE",
+ "OPTIONS",
+ },
+ "cors.allowed_headers": []string{
+ "Accept",
+ "Authorization",
+ "Content-Type",
+ "X-Request-ID",
+ },
+ "cors.allow_credentials": true,
+ "cors.max_age": 300,
+
+ "log.level": "info",
+ "log.format": "json",
+
+ "otel.enabled": false,
+ "otel.insecure": true,
+ "otel.sample_rate": 0.1,
+ "otel.service_name": "go-backend",
+ }
+
+ for key, value := range defaults {
+ if err := k.Set(key, value); err != nil {
+ return fmt.Errorf("set default %s: %w", key, err)
+ }
+ }
+
+ return nil
+}
+
+var envKeyMap = map[string]string{
+ "DATABASE_URL": "database.url",
+ "REDIS_URL": "redis.url",
+ "ENVIRONMENT": "app.environment",
+ "HOST": "server.host",
+ "PORT": "server.port",
+ "LOG_LEVEL": "log.level",
+ "LOG_FORMAT": "log.format",
+ "JWT_PRIVATE_KEY_PATH": "jwt.private_key_path",
+ "JWT_PUBLIC_KEY_PATH": "jwt.public_key_path",
+ "JWT_ACCESS_TOKEN_EXPIRE": "jwt.access_token_expire",
+ "JWT_REFRESH_TOKEN_EXPIRE": "jwt.refresh_token_expire",
+ "JWT_ISSUER": "jwt.issuer",
+ "JWT_AUDIENCE": "jwt.audience",
+ "RATE_LIMIT_REQUESTS": "rate_limit.requests",
+ "RATE_LIMIT_WINDOW": "rate_limit.window",
+ "RATE_LIMIT_BURST": "rate_limit.burst",
+ "OTEL_ENDPOINT": "otel.endpoint",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "otel.endpoint",
+ "OTEL_SERVICE_NAME": "otel.service_name",
+ "OTEL_ENABLED": "otel.enabled",
+ "OTEL_INSECURE": "otel.insecure",
+ "OTEL_SAMPLE_RATE": "otel.sample_rate",
+}
+
+func envKeyReplacer(s string) string {
+ if mapped, ok := envKeyMap[s]; ok {
+ return mapped
+ }
+ return ""
+}
+
+func validate(c *Config) error {
+ if c.Database.URL == "" {
+ return fmt.Errorf("DATABASE_URL is required")
+ }
+
+ if c.Redis.URL == "" {
+ return fmt.Errorf("REDIS_URL is required")
+ }
+
+ if c.JWT.PrivateKeyPath == "" {
+ return fmt.Errorf("JWT_PRIVATE_KEY_PATH is required")
+ }
+
+ if c.JWT.PublicKeyPath == "" {
+ return fmt.Errorf("JWT_PUBLIC_KEY_PATH is required")
+ }
+
+ if c.CORS.AllowCredentials {
+ for _, origin := range c.CORS.AllowedOrigins {
+ if origin == "*" {
+ return fmt.Errorf(
+ "CORS wildcard '*' cannot be used with AllowCredentials",
+ )
+ }
+ }
+ }
+
+ if c.App.Environment == "production" {
+ if c.Otel.Enabled && c.Otel.Insecure {
+ return fmt.Errorf("OTEL_INSECURE must be false in production")
+ }
+ }
+
+ if c.Server.ReadTimeout <= 0 {
+ return fmt.Errorf("server.read_timeout must be positive")
+ }
+
+ if c.Server.WriteTimeout <= 0 {
+ return fmt.Errorf("server.write_timeout must be positive")
+ }
+
+ return nil
+}
+
+func (c *Config) IsProduction() bool {
+ return c.App.Environment == "production"
+}
+
+func (c *Config) IsDevelopment() bool {
+ return c.App.Environment == "development"
+}
+
+func (s *ServerConfig) Address() string {
+ return fmt.Sprintf("%s:%d", s.Host, s.Port)
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/database.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/database.go
new file mode 100644
index 00000000..766af7f8
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/database.go
@@ -0,0 +1,145 @@
+// AngelaMos | 2026
+// database.go
+
+package core
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "math/rand/v2"
+ "time"
+
+ _ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/jmoiron/sqlx"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+)
+
+type Database struct {
+ DB *sqlx.DB
+}
+
+func NewDatabase(
+ ctx context.Context,
+ cfg config.DatabaseConfig,
+) (*Database, error) {
+ db, err := sqlx.ConnectContext(ctx, "pgx", cfg.URL)
+ if err != nil {
+ return nil, fmt.Errorf("connect to database: %w", err)
+ }
+
+ db.SetMaxOpenConns(cfg.MaxOpenConns)
+ db.SetMaxIdleConns(cfg.MaxIdleConns)
+ db.SetConnMaxLifetime(jitteredDuration(cfg.ConnMaxLifetime))
+ db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
+
+ pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ if err := db.PingContext(pingCtx); err != nil {
+ _ = db.Close() //nolint:errcheck // cleanup on connection failure
+ return nil, fmt.Errorf("ping database: %w", err)
+ }
+
+ return &Database{DB: db}, nil
+}
+
+func (d *Database) Close() error {
+ if d.DB != nil {
+ return d.DB.Close()
+ }
+ return nil
+}
+
+func (d *Database) Ping(ctx context.Context) error {
+ pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ if err := d.DB.PingContext(pingCtx); err != nil {
+ return fmt.Errorf("database ping failed: %w", err)
+ }
+
+ return nil
+}
+
+func (d *Database) Stats() sql.DBStats {
+ return d.DB.Stats()
+}
+
+type DBTX interface {
+ sqlx.ExtContext
+ sqlx.ExecerContext
+ GetContext(ctx context.Context, dest any, query string, args ...any) error
+ SelectContext(
+ ctx context.Context,
+ dest any,
+ query string,
+ args ...any,
+ ) error
+}
+
+func InTx(ctx context.Context, db *sqlx.DB, fn func(tx *sqlx.Tx) error) error {
+ tx, err := db.BeginTxx(ctx, nil)
+ if err != nil {
+ return fmt.Errorf("begin transaction: %w", err)
+ }
+
+ defer func() {
+ if p := recover(); p != nil {
+ _ = tx.Rollback() //nolint:errcheck // best-effort rollback on panic
+ panic(p)
+ }
+ }()
+
+ if err := fn(tx); err != nil {
+ if rbErr := tx.Rollback(); rbErr != nil {
+ return fmt.Errorf("rollback failed: %w (original: %w)", rbErr, err)
+ }
+ return err
+ }
+
+ if err := tx.Commit(); err != nil {
+ return fmt.Errorf("commit transaction: %w", err)
+ }
+
+ return nil
+}
+
+func InTxWithOptions(
+ ctx context.Context,
+ db *sqlx.DB,
+ opts *sql.TxOptions,
+ fn func(tx *sqlx.Tx) error,
+) error {
+ tx, err := db.BeginTxx(ctx, opts)
+ if err != nil {
+ return fmt.Errorf("begin transaction: %w", err)
+ }
+
+ defer func() {
+ if p := recover(); p != nil {
+ _ = tx.Rollback() //nolint:errcheck // best-effort rollback on panic
+ panic(p)
+ }
+ }()
+
+ if err := fn(tx); err != nil {
+ if rbErr := tx.Rollback(); rbErr != nil {
+ return fmt.Errorf("rollback failed: %w (original: %w)", rbErr, err)
+ }
+ return err
+ }
+
+ if err := tx.Commit(); err != nil {
+ return fmt.Errorf("commit transaction: %w", err)
+ }
+
+ return nil
+}
+
+func jitteredDuration(base time.Duration) time.Duration {
+ //nolint:gosec // G404: non-security-sensitive jitter for connection pool
+ jitter := time.Duration(rand.Int64N(int64(base / 7)))
+ return base + jitter
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/errors.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/errors.go
new file mode 100644
index 00000000..b478a6ee
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/errors.go
@@ -0,0 +1,169 @@
+// AngelaMos | 2026
+// errors.go
+
+package core
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+)
+
+var (
+ ErrNotFound = errors.New("resource not found")
+ ErrDuplicateKey = errors.New("duplicate key violation")
+ ErrForeignKey = errors.New("foreign key violation")
+ ErrInvalidInput = errors.New("invalid input")
+ ErrUnauthorized = errors.New("unauthorized")
+ ErrForbidden = errors.New("forbidden")
+ ErrInternal = errors.New("internal server error")
+ ErrConflict = errors.New("resource conflict")
+ ErrRateLimited = errors.New("rate limit exceeded")
+ ErrTokenExpired = errors.New("token expired")
+ ErrTokenInvalid = errors.New("token invalid")
+ ErrTokenRevoked = errors.New("token revoked")
+)
+
+type AppError struct {
+ Err error `json:"-"`
+ Message string `json:"message"`
+ StatusCode int `json:"-"`
+ Code string `json:"code"`
+}
+
+func (e *AppError) Error() string {
+ if e.Message != "" {
+ return e.Message
+ }
+ if e.Err != nil {
+ return e.Err.Error()
+ }
+ return "unknown error"
+}
+
+func (e *AppError) Unwrap() error {
+ return e.Err
+}
+
+func NewAppError(
+ err error,
+ message string,
+ statusCode int,
+ code string,
+) *AppError {
+ return &AppError{
+ Err: err,
+ Message: message,
+ StatusCode: statusCode,
+ Code: code,
+ }
+}
+
+func NotFoundError(resource string) *AppError {
+ return &AppError{
+ Err: ErrNotFound,
+ Message: fmt.Sprintf("%s not found", resource),
+ StatusCode: http.StatusNotFound,
+ Code: "NOT_FOUND",
+ }
+}
+
+func DuplicateError(field string) *AppError {
+ return &AppError{
+ Err: ErrDuplicateKey,
+ Message: fmt.Sprintf("%s already exists", field),
+ StatusCode: http.StatusConflict,
+ Code: "DUPLICATE",
+ }
+}
+
+func ValidationError(message string) *AppError {
+ return &AppError{
+ Err: ErrInvalidInput,
+ Message: message,
+ StatusCode: http.StatusBadRequest,
+ Code: "VALIDATION_ERROR",
+ }
+}
+
+func UnauthorizedError(message string) *AppError {
+ if message == "" {
+ message = "authentication required"
+ }
+ return &AppError{
+ Err: ErrUnauthorized,
+ Message: message,
+ StatusCode: http.StatusUnauthorized,
+ Code: "UNAUTHORIZED",
+ }
+}
+
+func ForbiddenError(message string) *AppError {
+ if message == "" {
+ message = "access denied"
+ }
+ return &AppError{
+ Err: ErrForbidden,
+ Message: message,
+ StatusCode: http.StatusForbidden,
+ Code: "FORBIDDEN",
+ }
+}
+
+func InternalError(err error) *AppError {
+ return &AppError{
+ Err: err,
+ Message: "internal server error",
+ StatusCode: http.StatusInternalServerError,
+ Code: "INTERNAL_ERROR",
+ }
+}
+
+func RateLimitError() *AppError {
+ return &AppError{
+ Err: ErrRateLimited,
+ Message: "too many requests",
+ StatusCode: http.StatusTooManyRequests,
+ Code: "RATE_LIMITED",
+ }
+}
+
+func TokenExpiredError() *AppError {
+ return &AppError{
+ Err: ErrTokenExpired,
+ Message: "token has expired",
+ StatusCode: http.StatusUnauthorized,
+ Code: "TOKEN_EXPIRED",
+ }
+}
+
+func TokenInvalidError() *AppError {
+ return &AppError{
+ Err: ErrTokenInvalid,
+ Message: "invalid token",
+ StatusCode: http.StatusUnauthorized,
+ Code: "TOKEN_INVALID",
+ }
+}
+
+func TokenRevokedError() *AppError {
+ return &AppError{
+ Err: ErrTokenRevoked,
+ Message: "token has been revoked",
+ StatusCode: http.StatusUnauthorized,
+ Code: "TOKEN_REVOKED",
+ }
+}
+
+func IsAppError(err error) bool {
+ var appErr *AppError
+ return errors.As(err, &appErr)
+}
+
+func GetAppError(err error) *AppError {
+ var appErr *AppError
+ if errors.As(err, &appErr) {
+ return appErr
+ }
+ return InternalError(err)
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/redis.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/redis.go
new file mode 100644
index 00000000..822b6cc9
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/redis.go
@@ -0,0 +1,63 @@
+// AngelaMos | 2026
+// redis.go
+
+package core
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/redis/go-redis/v9"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+)
+
+type Redis struct {
+ Client *redis.Client
+}
+
+func NewRedis(ctx context.Context, cfg config.RedisConfig) (*Redis, error) {
+ opts, err := redis.ParseURL(cfg.URL)
+ if err != nil {
+ return nil, fmt.Errorf("parse redis url: %w", err)
+ }
+
+ opts.PoolSize = cfg.PoolSize
+ opts.MinIdleConns = cfg.MinIdleConns
+ opts.PoolTimeout = 30 * time.Second
+ opts.ConnMaxIdleTime = 5 * time.Minute
+
+ client := redis.NewClient(opts)
+
+ pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ if err := client.Ping(pingCtx).Err(); err != nil {
+ return nil, fmt.Errorf("ping redis: %w", err)
+ }
+
+ return &Redis{Client: client}, nil
+}
+
+func (r *Redis) Close() error {
+ if r.Client != nil {
+ return r.Client.Close()
+ }
+ return nil
+}
+
+func (r *Redis) Ping(ctx context.Context) error {
+ pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ if err := r.Client.Ping(pingCtx).Err(); err != nil {
+ return fmt.Errorf("redis ping failed: %w", err)
+ }
+
+ return nil
+}
+
+func (r *Redis) PoolStats() *redis.PoolStats {
+ return r.Client.PoolStats()
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/response.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/response.go
new file mode 100644
index 00000000..e37ac737
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/response.go
@@ -0,0 +1,120 @@
+// AngelaMos | 2026
+// response.go
+
+package core
+
+import (
+ "encoding/json"
+ "log/slog"
+ "net/http"
+)
+
+type Response struct {
+ Success bool `json:"success"`
+ Data any `json:"data,omitempty"`
+ Error *Error `json:"error,omitempty"`
+ Meta *Meta `json:"meta,omitempty"`
+}
+
+type Error struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+type Meta struct {
+ Page int `json:"page,omitempty"`
+ PageSize int `json:"page_size,omitempty"`
+ Total int `json:"total,omitempty"`
+ TotalPages int `json:"total_pages,omitempty"`
+}
+
+func JSON(w http.ResponseWriter, status int, data any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+
+ response := Response{
+ Success: status >= 200 && status < 300,
+ Data: data,
+ }
+
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ slog.Error("failed to encode response", "error", err)
+ }
+}
+
+func JSONWithMeta(w http.ResponseWriter, status int, data any, meta *Meta) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+
+ response := Response{
+ Success: true,
+ Data: data,
+ Meta: meta,
+ }
+
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ slog.Error("failed to encode response", "error", err)
+ }
+}
+
+func JSONError(w http.ResponseWriter, err error) {
+ appErr := GetAppError(err)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(appErr.StatusCode)
+
+ response := Response{
+ Success: false,
+ Error: &Error{
+ Code: appErr.Code,
+ Message: appErr.Message,
+ },
+ }
+
+ if encErr := json.NewEncoder(w).Encode(response); encErr != nil {
+ slog.Error("failed to encode error response", "error", encErr)
+ }
+}
+
+func Created(w http.ResponseWriter, data any) {
+ JSON(w, http.StatusCreated, data)
+}
+
+func OK(w http.ResponseWriter, data any) {
+ JSON(w, http.StatusOK, data)
+}
+
+func NoContent(w http.ResponseWriter) {
+ w.WriteHeader(http.StatusNoContent)
+}
+
+func BadRequest(w http.ResponseWriter, message string) {
+ JSONError(w, ValidationError(message))
+}
+
+func NotFound(w http.ResponseWriter, resource string) {
+ JSONError(w, NotFoundError(resource))
+}
+
+func Unauthorized(w http.ResponseWriter, message string) {
+ JSONError(w, UnauthorizedError(message))
+}
+
+func Forbidden(w http.ResponseWriter, message string) {
+ JSONError(w, ForbiddenError(message))
+}
+
+func InternalServerError(w http.ResponseWriter, err error) {
+ slog.Error("internal server error", "error", err)
+ JSONError(w, InternalError(err))
+}
+
+func Paginated(w http.ResponseWriter, data any, page, pageSize, total int) {
+ totalPages := (total + pageSize - 1) / pageSize
+ JSONWithMeta(w, http.StatusOK, data, &Meta{
+ Page: page,
+ PageSize: pageSize,
+ Total: total,
+ TotalPages: totalPages,
+ })
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/security.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/security.go
new file mode 100644
index 00000000..05065e1a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/security.go
@@ -0,0 +1,218 @@
+// AngelaMos | 2026
+// security.go
+
+package core
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "strings"
+
+ "golang.org/x/crypto/argon2"
+)
+
+const (
+ argonTime = 1
+ argonMemory = 64 * 1024
+ argonThreads = 4
+ argonKeyLen = 32
+ saltLength = 16
+)
+
+func HashPassword(password string) (string, error) {
+ salt := make([]byte, saltLength)
+ if _, err := rand.Read(salt); err != nil {
+ return "", fmt.Errorf("generate salt: %w", err)
+ }
+
+ hash := argon2.IDKey(
+ []byte(password),
+ salt,
+ argonTime,
+ argonMemory,
+ argonThreads,
+ argonKeyLen,
+ )
+
+ b64Salt := base64.RawStdEncoding.EncodeToString(salt)
+ b64Hash := base64.RawStdEncoding.EncodeToString(hash)
+
+ encoded := fmt.Sprintf(
+ "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
+ argon2.Version,
+ argonMemory,
+ argonTime,
+ argonThreads,
+ b64Salt,
+ b64Hash,
+ )
+
+ return encoded, nil
+}
+
+func VerifyPassword(password, encodedHash string) (bool, error) {
+ params, salt, hash, err := decodeHash(encodedHash)
+ if err != nil {
+ return false, err
+ }
+
+ otherHash := argon2.IDKey(
+ []byte(password),
+ salt,
+ params.time,
+ params.memory,
+ params.threads,
+ params.keyLen,
+ )
+
+ if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+func VerifyPasswordWithRehash(
+ password, encodedHash string,
+) (bool, string, error) {
+ valid, err := VerifyPassword(password, encodedHash)
+ if err != nil {
+ return false, "", err
+ }
+
+ if !valid {
+ return false, "", nil
+ }
+
+ if needsRehash(encodedHash) {
+ newHash, hashErr := HashPassword(password)
+ if hashErr != nil {
+ //nolint:nilerr // password verified successfully; rehash failure is non-critical
+ return true, "", nil
+ }
+ return true, newHash, nil
+ }
+
+ return true, "", nil
+}
+
+var dummyHash string
+
+func init() {
+ hash, err := HashPassword("dummy_password_for_timing_attack_prevention")
+ if err != nil {
+ panic(fmt.Sprintf("security: failed to generate dummy hash: %v", err))
+ }
+ dummyHash = hash
+}
+
+func VerifyPasswordTimingSafe(
+ password string,
+ encodedHash *string,
+) (bool, string, error) {
+ hashToVerify := dummyHash
+ if encodedHash != nil && *encodedHash != "" {
+ hashToVerify = *encodedHash
+ }
+
+ valid, newHash, err := VerifyPasswordWithRehash(password, hashToVerify)
+
+ if encodedHash == nil || *encodedHash == "" {
+ return false, "", nil
+ }
+
+ return valid, newHash, err
+}
+
+type argonParams struct {
+ memory uint32
+ time uint32
+ threads uint8
+ keyLen uint32
+}
+
+func decodeHash(encodedHash string) (*argonParams, []byte, []byte, error) {
+ parts := strings.Split(encodedHash, "$")
+ if len(parts) != 6 {
+ return nil, nil, nil, fmt.Errorf("invalid hash format")
+ }
+
+ if parts[1] != "argon2id" {
+ return nil, nil, nil, fmt.Errorf("unsupported algorithm: %s", parts[1])
+ }
+
+ var version int
+ _, err := fmt.Sscanf(parts[2], "v=%d", &version)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("invalid version: %w", err)
+ }
+
+ if version != argon2.Version {
+ return nil, nil, nil, fmt.Errorf("incompatible version: %d", version)
+ }
+
+ params := &argonParams{}
+ _, err = fmt.Sscanf(
+ parts[3],
+ "m=%d,t=%d,p=%d",
+ ¶ms.memory,
+ ¶ms.time,
+ ¶ms.threads,
+ )
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("invalid params: %w", err)
+ }
+
+ salt, err := base64.RawStdEncoding.DecodeString(parts[4])
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("decode salt: %w", err)
+ }
+
+ hash, err := base64.RawStdEncoding.DecodeString(parts[5])
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("decode hash: %w", err)
+ }
+
+ //nolint:gosec // G115: hash length is always small (32 bytes for Argon2id)
+ params.keyLen = uint32(len(hash))
+
+ return params, salt, hash, nil
+}
+
+func needsRehash(encodedHash string) bool {
+ params, _, _, err := decodeHash(encodedHash)
+ if err != nil {
+ return true
+ }
+
+ return params.memory != argonMemory ||
+ params.time != argonTime ||
+ params.threads != argonThreads ||
+ params.keyLen != argonKeyLen
+}
+
+func GenerateSecureToken(length int) (string, error) {
+ bytes := make([]byte, length)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", fmt.Errorf("generate random bytes: %w", err)
+ }
+ return base64.URLEncoding.EncodeToString(bytes), nil
+}
+
+func GenerateRefreshToken() (string, error) {
+ return GenerateSecureToken(32)
+}
+
+func HashToken(token string) string {
+ hash := sha256.Sum256([]byte(token))
+ return hex.EncodeToString(hash[:])
+}
+
+func CompareTokenHash(token, hash string) bool {
+ tokenHash := HashToken(token)
+ return subtle.ConstantTimeCompare([]byte(tokenHash), []byte(hash)) == 1
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/telemetry.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/telemetry.go
new file mode 100644
index 00000000..950bba50
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/telemetry.go
@@ -0,0 +1,142 @@
+// AngelaMos | 2026
+// telemetry.go
+
+package core
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "go.opentelemetry.io/otel/propagation"
+ "go.opentelemetry.io/otel/sdk/resource"
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
+ semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
+ "go.opentelemetry.io/otel/trace"
+ "google.golang.org/grpc/credentials"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+)
+
+type Telemetry struct {
+ TracerProvider *sdktrace.TracerProvider
+ Tracer trace.Tracer
+}
+
+func NewTelemetry(
+ ctx context.Context,
+ otelCfg config.OtelConfig,
+ appCfg config.AppConfig,
+) (*Telemetry, error) {
+ if !otelCfg.Enabled || otelCfg.Endpoint == "" {
+ noopProvider := sdktrace.NewTracerProvider()
+ return &Telemetry{
+ TracerProvider: noopProvider,
+ Tracer: noopProvider.Tracer(otelCfg.ServiceName),
+ }, nil
+ }
+
+ opts := []otlptracegrpc.Option{
+ otlptracegrpc.WithEndpoint(otelCfg.Endpoint),
+ otlptracegrpc.WithTimeout(5 * time.Second),
+ }
+
+ if otelCfg.Insecure {
+ opts = append(
+ opts,
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()),
+ )
+ } else {
+ opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")))
+ }
+
+ exporter, err := otlptracegrpc.New(ctx, opts...)
+ if err != nil {
+ return nil, fmt.Errorf("create otlp exporter: %w", err)
+ }
+
+ res, err := resource.New(ctx,
+ resource.WithAttributes(
+ semconv.ServiceName(otelCfg.ServiceName),
+ semconv.ServiceVersion(appCfg.Version),
+ attribute.String("environment", appCfg.Environment),
+ ),
+ resource.WithHost(),
+ resource.WithProcess(),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create resource: %w", err)
+ }
+
+ sampleRate := otelCfg.SampleRate
+ if sampleRate <= 0 || sampleRate > 1 {
+ sampleRate = 0.1
+ }
+
+ tp := sdktrace.NewTracerProvider(
+ sdktrace.WithBatcher(exporter,
+ sdktrace.WithBatchTimeout(5*time.Second),
+ sdktrace.WithMaxExportBatchSize(512),
+ ),
+ sdktrace.WithResource(res),
+ sdktrace.WithSampler(sdktrace.ParentBased(
+ sdktrace.TraceIDRatioBased(sampleRate),
+ )),
+ )
+
+ otel.SetTracerProvider(tp)
+ otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
+ propagation.TraceContext{},
+ propagation.Baggage{},
+ ))
+
+ return &Telemetry{
+ TracerProvider: tp,
+ Tracer: tp.Tracer(otelCfg.ServiceName),
+ }, nil
+}
+
+func (t *Telemetry) Shutdown(ctx context.Context) error {
+ if t.TracerProvider == nil {
+ return nil
+ }
+
+ shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ if err := t.TracerProvider.Shutdown(shutdownCtx); err != nil {
+ return fmt.Errorf("shutdown tracer provider: %w", err)
+ }
+
+ return nil
+}
+
+func SpanFromContext(ctx context.Context) trace.Span {
+ return trace.SpanFromContext(ctx)
+}
+
+func TraceIDFromContext(ctx context.Context) string {
+ span := trace.SpanFromContext(ctx)
+ if span.SpanContext().IsValid() {
+ return span.SpanContext().TraceID().String()
+ }
+ return ""
+}
+
+func AddSpanEvent(
+ ctx context.Context,
+ name string,
+ attrs ...attribute.KeyValue,
+) {
+ span := trace.SpanFromContext(ctx)
+ span.AddEvent(name, trace.WithAttributes(attrs...))
+}
+
+func SetSpanError(ctx context.Context, err error) {
+ span := trace.SpanFromContext(ctx)
+ span.RecordError(err)
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/validation.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/validation.go
new file mode 100644
index 00000000..f77cbd96
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/core/validation.go
@@ -0,0 +1,42 @@
+// AngelaMos | 2026
+// validation.go
+
+package core
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/go-playground/validator/v10"
+)
+
+func FormatValidationError(err error) string {
+ var ve validator.ValidationErrors
+ if errors.As(err, &ve) {
+ messages := make([]string, 0, len(ve))
+ for _, fe := range ve {
+ messages = append(messages, FormatFieldError(fe))
+ }
+ return strings.Join(messages, "; ")
+ }
+ return "validation failed"
+}
+
+func FormatFieldError(fe validator.FieldError) string {
+ field := strings.ToLower(fe.Field())
+
+ switch fe.Tag() {
+ case "required":
+ return field + " is required"
+ case "email":
+ return field + " must be a valid email"
+ case "min":
+ return field + " must be at least " + fe.Param() + " characters"
+ case "max":
+ return field + " must be at most " + fe.Param() + " characters"
+ case "oneof":
+ return field + " must be one of: " + fe.Param()
+ default:
+ return field + " is invalid"
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/health/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/health/handler.go
new file mode 100644
index 00000000..e7003c6a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/health/handler.go
@@ -0,0 +1,195 @@
+// AngelaMos | 2026
+// handler.go
+
+package health
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/go-chi/chi/v5"
+)
+
+type Checker interface {
+ Ping(ctx context.Context) error
+}
+
+type Handler struct {
+ db Checker
+ redis Checker
+ ready atomic.Bool
+ shutdown atomic.Bool
+}
+
+func NewHandler(db, redis Checker) *Handler {
+ h := &Handler{
+ db: db,
+ redis: redis,
+ }
+ h.ready.Store(true)
+ return h
+}
+
+func (h *Handler) RegisterRoutes(r chi.Router) {
+ r.Get("/healthz", h.Liveness)
+ r.Get("/livez", h.Liveness)
+ r.Get("/readyz", h.Readiness)
+}
+
+func (h *Handler) Liveness(w http.ResponseWriter, r *http.Request) {
+ if h.shutdown.Load() {
+ h.writeStatus(w, http.StatusServiceUnavailable, StatusResponse{
+ Status: "shutting_down",
+ })
+ return
+ }
+
+ h.writeStatus(w, http.StatusOK, StatusResponse{
+ Status: "ok",
+ })
+}
+
+func (h *Handler) Readiness(w http.ResponseWriter, r *http.Request) {
+ if h.shutdown.Load() {
+ h.writeStatus(w, http.StatusServiceUnavailable, StatusResponse{
+ Status: "shutting_down",
+ })
+ return
+ }
+
+ if !h.ready.Load() {
+ h.writeStatus(w, http.StatusServiceUnavailable, StatusResponse{
+ Status: "not_ready",
+ })
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
+ defer cancel()
+
+ checks := h.runHealthChecks(ctx)
+
+ allHealthy := true
+ for _, check := range checks {
+ if !check.Healthy {
+ allHealthy = false
+ break
+ }
+ }
+
+ status := "ok"
+ statusCode := http.StatusOK
+ if !allHealthy {
+ status = "degraded"
+ statusCode = http.StatusServiceUnavailable
+ }
+
+ h.writeStatus(w, statusCode, ReadinessResponse{
+ Status: status,
+ Checks: checks,
+ })
+}
+
+func (h *Handler) runHealthChecks(ctx context.Context) []HealthCheck {
+ var wg sync.WaitGroup
+ checks := make([]HealthCheck, 2)
+
+ wg.Add(2)
+
+ go func() {
+ defer wg.Done()
+ checks[0] = h.checkDatabase(ctx)
+ }()
+
+ go func() {
+ defer wg.Done()
+ checks[1] = h.checkRedis(ctx)
+ }()
+
+ wg.Wait()
+ return checks
+}
+
+func (h *Handler) checkDatabase(ctx context.Context) HealthCheck {
+ check := HealthCheck{
+ Name: "database",
+ Healthy: true,
+ }
+
+ if h.db == nil {
+ check.Healthy = false
+ check.Message = "database checker not configured"
+ return check
+ }
+
+ start := time.Now()
+ err := h.db.Ping(ctx)
+ check.Latency = time.Since(start).String()
+
+ if err != nil {
+ check.Healthy = false
+ check.Message = "ping failed"
+ }
+
+ return check
+}
+
+func (h *Handler) checkRedis(ctx context.Context) HealthCheck {
+ check := HealthCheck{
+ Name: "redis",
+ Healthy: true,
+ }
+
+ if h.redis == nil {
+ check.Healthy = false
+ check.Message = "redis checker not configured"
+ return check
+ }
+
+ start := time.Now()
+ err := h.redis.Ping(ctx)
+ check.Latency = time.Since(start).String()
+
+ if err != nil {
+ check.Healthy = false
+ check.Message = "ping failed"
+ }
+
+ return check
+}
+
+func (h *Handler) SetReady(ready bool) {
+ h.ready.Store(ready)
+}
+
+func (h *Handler) SetShutdown(shutdown bool) {
+ h.shutdown.Store(shutdown)
+}
+
+func (h *Handler) writeStatus(w http.ResponseWriter, status int, data any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ w.WriteHeader(status)
+ //nolint:errcheck // best-effort response
+ _ = json.NewEncoder(w).Encode(data)
+}
+
+type StatusResponse struct {
+ Status string `json:"status"`
+}
+
+type ReadinessResponse struct {
+ Status string `json:"status"`
+ Checks []HealthCheck `json:"checks"`
+}
+
+type HealthCheck struct {
+ Name string `json:"name"`
+ Healthy bool `json:"healthy"`
+ Latency string `json:"latency,omitempty"`
+ Message string `json:"message,omitempty"`
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/auth.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/auth.go
new file mode 100644
index 00000000..0f9fb5f2
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/auth.go
@@ -0,0 +1,189 @@
+// AngelaMos | 2026
+// auth.go
+
+package middleware
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+const (
+ UserIDKey contextKey = "user_id"
+ UserRoleKey contextKey = "user_role"
+ UserTierKey contextKey = "user_tier"
+ ClaimsKey contextKey = "jwt_claims"
+)
+
+type TokenVerifier interface {
+ VerifyAccessToken(
+ ctx context.Context,
+ token string,
+ ) (*AccessTokenClaims, error)
+}
+
+type AccessTokenClaims struct {
+ UserID string
+ Role string
+ Tier string
+ TokenVersion int
+}
+
+func Authenticator(verifier TokenVerifier) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ token := ExtractToken(r)
+
+ if token == "" {
+ core.JSONError(
+ w,
+ core.UnauthorizedError("missing authorization token"),
+ )
+ return
+ }
+
+ claims, err := verifier.VerifyAccessToken(r.Context(), token)
+ if err != nil {
+ handleAuthError(w, err)
+ return
+ }
+
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
+ ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
+ ctx = context.WithValue(ctx, UserTierKey, claims.Tier)
+ ctx = context.WithValue(ctx, ClaimsKey, claims)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+ }
+}
+
+func OptionalAuth(verifier TokenVerifier) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ token := ExtractToken(r)
+
+ if token != "" {
+ claims, err := verifier.VerifyAccessToken(r.Context(), token)
+ if err == nil {
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
+ ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
+ ctx = context.WithValue(ctx, UserTierKey, claims.Tier)
+ ctx = context.WithValue(ctx, ClaimsKey, claims)
+ r = r.WithContext(ctx)
+ }
+ }
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func RequireRole(roles ...string) func(http.Handler) http.Handler {
+ roleSet := make(map[string]struct{}, len(roles))
+ for _, role := range roles {
+ roleSet[role] = struct{}{}
+ }
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ userRole := GetUserRole(r.Context())
+
+ if userRole == "" {
+ core.JSONError(
+ w,
+ core.UnauthorizedError("authentication required"),
+ )
+ return
+ }
+
+ if _, ok := roleSet[userRole]; !ok {
+ core.JSONError(
+ w,
+ core.ForbiddenError("insufficient permissions"),
+ )
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func RequireAdmin(next http.Handler) http.Handler {
+ return RequireRole("admin")(next)
+}
+
+func ExtractToken(r *http.Request) string {
+ authHeader := r.Header.Get("Authorization")
+ if authHeader == "" {
+ return ""
+ }
+
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
+ return ""
+ }
+
+ return strings.TrimSpace(parts[1])
+}
+
+func handleAuthError(w http.ResponseWriter, err error) {
+ if core.IsAppError(err) {
+ core.JSONError(w, err)
+ return
+ }
+
+ switch {
+ case errors.Is(err, core.ErrTokenExpired):
+ core.JSONError(w, core.TokenExpiredError())
+ case errors.Is(err, core.ErrTokenRevoked):
+ core.JSONError(w, core.TokenRevokedError())
+ case errors.Is(err, core.ErrTokenInvalid):
+ core.JSONError(w, core.TokenInvalidError())
+ default:
+ core.JSONError(w, core.TokenInvalidError())
+ }
+}
+
+func GetUserID(ctx context.Context) string {
+ if id, ok := ctx.Value(UserIDKey).(string); ok {
+ return id
+ }
+ return ""
+}
+
+func GetUserRole(ctx context.Context) string {
+ if role, ok := ctx.Value(UserRoleKey).(string); ok {
+ return role
+ }
+ return ""
+}
+
+func GetUserTier(ctx context.Context) string {
+ if tier, ok := ctx.Value(UserTierKey).(string); ok {
+ return tier
+ }
+ return ""
+}
+
+func GetClaims(ctx context.Context) *AccessTokenClaims {
+ if claims, ok := ctx.Value(ClaimsKey).(*AccessTokenClaims); ok {
+ return claims
+ }
+ return nil
+}
+
+func IsAuthenticated(ctx context.Context) bool {
+ return GetUserID(ctx) != ""
+}
+
+func IsAdmin(ctx context.Context) bool {
+ return GetUserRole(ctx) == "admin"
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/headers.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/headers.go
new file mode 100644
index 00000000..332d48f3
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/headers.go
@@ -0,0 +1,103 @@
+// AngelaMos | 2026
+// headers.go
+
+package middleware
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+)
+
+func SecurityHeaders(isProduction bool) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ h := w.Header()
+
+ h.Set("X-Content-Type-Options", "nosniff")
+ h.Set("X-Frame-Options", "DENY")
+ h.Set("X-XSS-Protection", "1; mode=block")
+ h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
+ h.Set(
+ "Permissions-Policy",
+ "geolocation=(), microphone=(), camera=()",
+ )
+
+ if isProduction {
+ h.Set(
+ "Strict-Transport-Security",
+ "max-age=31536000; includeSubDomains; preload",
+ )
+ }
+
+ h.Set("Content-Security-Policy", buildCSP(isProduction))
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func buildCSP(isProduction bool) string {
+ directives := []string{
+ "default-src 'self'",
+ "script-src 'self'",
+ "style-src 'self' 'unsafe-inline'",
+ "img-src 'self' data: https:",
+ "font-src 'self'",
+ "connect-src 'self'",
+ "frame-ancestors 'none'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ }
+
+ if !isProduction {
+ directives[1] = "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
+ }
+
+ return strings.Join(directives, "; ")
+}
+
+func CORS(cfg config.CORSConfig) func(http.Handler) http.Handler {
+ allowedOrigins := make(map[string]struct{}, len(cfg.AllowedOrigins))
+ for _, origin := range cfg.AllowedOrigins {
+ allowedOrigins[origin] = struct{}{}
+ }
+
+ methodsStr := strings.Join(cfg.AllowedMethods, ", ")
+ headersStr := strings.Join(cfg.AllowedHeaders, ", ")
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ origin := r.Header.Get("Origin")
+
+ if origin != "" {
+ if _, ok := allowedOrigins[origin]; ok {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Vary", "Origin")
+
+ if cfg.AllowCredentials {
+ w.Header().
+ Set("Access-Control-Allow-Credentials", "true")
+ }
+ }
+ }
+
+ if r.Method == http.MethodOptions {
+ w.Header().Set("Access-Control-Allow-Methods", methodsStr)
+ w.Header().Set("Access-Control-Allow-Headers", headersStr)
+
+ if cfg.MaxAge > 0 {
+ w.Header().
+ Set("Access-Control-Max-Age", strconv.Itoa(cfg.MaxAge))
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/logging.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/logging.go
new file mode 100644
index 00000000..602ed4c7
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/logging.go
@@ -0,0 +1,99 @@
+// AngelaMos | 2026
+// logging.go
+
+package middleware
+
+import (
+ "context"
+ "log/slog"
+ "net/http"
+ "time"
+
+ "go.opentelemetry.io/otel/trace"
+)
+
+type loggerKey struct{}
+
+func Logger(baseLogger *slog.Logger) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ requestID := GetRequestID(r.Context())
+
+ reqLogger := baseLogger.With(
+ slog.String("request_id", requestID),
+ slog.String("method", r.Method),
+ slog.String("path", r.URL.Path),
+ slog.String("remote_addr", r.RemoteAddr),
+ )
+
+ if span := trace.SpanFromContext(r.Context()); span.SpanContext().
+ IsValid() {
+ reqLogger = reqLogger.With(
+ slog.String(
+ "trace_id",
+ span.SpanContext().TraceID().String(),
+ ),
+ slog.String(
+ "span_id",
+ span.SpanContext().SpanID().String(),
+ ),
+ )
+ }
+
+ ctx := context.WithValue(r.Context(), loggerKey{}, reqLogger)
+
+ ww := &responseWriter{
+ ResponseWriter: w,
+ status: http.StatusOK,
+ }
+
+ next.ServeHTTP(ww, r.WithContext(ctx))
+
+ latency := time.Since(start)
+
+ logLevel := slog.LevelInfo
+ if ww.status >= 500 {
+ logLevel = slog.LevelError
+ } else if ww.status >= 400 {
+ logLevel = slog.LevelWarn
+ }
+
+ reqLogger.Log(r.Context(), logLevel, "request completed",
+ slog.Int("status", ww.status),
+ slog.Int("bytes", ww.bytes),
+ slog.Duration("latency", latency),
+ slog.String("user_agent", r.UserAgent()),
+ )
+ })
+ }
+}
+
+func GetLogger(ctx context.Context) *slog.Logger {
+ if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
+ return logger
+ }
+ return slog.Default()
+}
+
+type responseWriter struct {
+ http.ResponseWriter
+ status int
+ bytes int
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ rw.status = code
+ rw.ResponseWriter.WriteHeader(code)
+}
+
+func (rw *responseWriter) Write(b []byte) (int, error) {
+ n, err := rw.ResponseWriter.Write(b)
+ rw.bytes += n
+ return n, err
+}
+
+func (rw *responseWriter) Unwrap() http.ResponseWriter {
+ return rw.ResponseWriter
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/ratelimit.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/ratelimit.go
new file mode 100644
index 00000000..c85519a1
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/ratelimit.go
@@ -0,0 +1,375 @@
+// AngelaMos | 2026
+// ratelimit.go
+
+package middleware
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ redis_rate "github.com/go-redis/redis_rate/v10"
+ "github.com/redis/go-redis/v9"
+ "golang.org/x/time/rate"
+)
+
+type RateLimitConfig struct {
+ Limit redis_rate.Limit
+ KeyFunc func(*http.Request) string
+ FailOpen bool
+ BypassFunc func(*http.Request) bool
+ OnLimited func(http.ResponseWriter, *http.Request, *redis_rate.Result)
+}
+
+type RateLimiter struct {
+ limiter *redis_rate.Limiter
+ fallback *localLimiter
+ config RateLimitConfig
+}
+
+func NewRateLimiter(rdb *redis.Client, cfg RateLimitConfig) *RateLimiter {
+ if cfg.KeyFunc == nil {
+ cfg.KeyFunc = KeyByIP
+ }
+
+ return &RateLimiter{
+ limiter: redis_rate.NewLimiter(rdb),
+ fallback: newLocalLimiter(),
+ config: cfg,
+ }
+}
+
+func (rl *RateLimiter) Handler(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if rl.config.BypassFunc != nil && rl.config.BypassFunc(r) {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ key := rl.config.KeyFunc(r)
+ res, err := rl.allow(r.Context(), key)
+ if err != nil {
+ if rl.config.FailOpen {
+ slog.Warn("rate limiter error, failing open",
+ "error", err,
+ "key", key,
+ )
+ next.ServeHTTP(w, r)
+ return
+ }
+ http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
+ return
+ }
+
+ setRateLimitHeaders(w, res, rl.config.Limit)
+
+ if res.Allowed == 0 {
+ if rl.config.OnLimited != nil {
+ rl.config.OnLimited(w, r, res)
+ return
+ }
+ writeRateLimitExceeded(w, res)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+func (rl *RateLimiter) allow(
+ ctx context.Context,
+ key string,
+) (*redis_rate.Result, error) {
+ res, err := rl.limiter.Allow(ctx, key, rl.config.Limit)
+ if err != nil {
+ return rl.fallback.allow(key, rl.config.Limit)
+ }
+ return res, nil
+}
+
+func KeyByIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ ips := strings.Split(xff, ",")
+ ip := strings.TrimSpace(ips[len(ips)-1])
+ return "ratelimit:ip:" + ip
+ }
+
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return "ratelimit:ip:" + xri
+ }
+
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ ip = r.RemoteAddr
+ }
+
+ return "ratelimit:ip:" + ip
+}
+
+func KeyByUser(r *http.Request) string {
+ if userID := GetUserID(r.Context()); userID != "" {
+ return "ratelimit:user:" + userID
+ }
+ return KeyByIP(r)
+}
+
+func KeyByUserAndEndpoint(r *http.Request) string {
+ userKey := KeyByUser(r)
+ endpoint := normalizeEndpoint(r.URL.Path)
+ return fmt.Sprintf("%s:endpoint:%s", userKey, endpoint)
+}
+
+func normalizeEndpoint(path string) string {
+ parts := strings.Split(strings.Trim(path, "/"), "/")
+ normalized := make([]string, 0, len(parts))
+
+ for _, part := range parts {
+ if isUUID(part) || isNumeric(part) {
+ normalized = append(normalized, "{id}")
+ } else {
+ normalized = append(normalized, part)
+ }
+ }
+
+ return "/" + strings.Join(normalized, "/")
+}
+
+func isUUID(s string) bool {
+ if len(s) != 36 {
+ return false
+ }
+ return s[8] == '-' && s[13] == '-' && s[18] == '-' && s[23] == '-'
+}
+
+func isNumeric(s string) bool {
+ for _, c := range s {
+ if c < '0' || c > '9' {
+ return false
+ }
+ }
+ return len(s) > 0
+}
+
+func setRateLimitHeaders(
+ w http.ResponseWriter,
+ res *redis_rate.Result,
+ limit redis_rate.Limit,
+) {
+ h := w.Header()
+
+ h.Set("X-RateLimit-Limit", strconv.Itoa(limit.Rate))
+ h.Set("X-RateLimit-Remaining", strconv.Itoa(res.Remaining))
+ h.Set("X-RateLimit-Reset", strconv.FormatInt(
+ time.Now().Add(res.ResetAfter).Unix(), 10))
+
+ windowSecs := int(limit.Period.Seconds())
+ h.Set("RateLimit-Policy", fmt.Sprintf(`%d;w=%d`, limit.Rate, windowSecs))
+ h.Set(
+ "RateLimit",
+ fmt.Sprintf(`%d;t=%d`, res.Remaining, int(res.ResetAfter.Seconds())),
+ )
+}
+
+func writeRateLimitExceeded(w http.ResponseWriter, res *redis_rate.Result) {
+ retryAfter := int(res.RetryAfter.Seconds())
+ if retryAfter < 1 {
+ retryAfter = 1
+ }
+
+ w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusTooManyRequests)
+
+ response := map[string]any{
+ "success": false,
+ "error": map[string]any{
+ "code": "RATE_LIMITED",
+ "message": fmt.Sprintf(
+ "Rate limit exceeded. Retry after %d seconds.",
+ retryAfter,
+ ),
+ },
+ }
+
+ //nolint:errcheck // best-effort response write
+ _ = json.NewEncoder(w).Encode(response)
+}
+
+type limiterEntry struct {
+ limiter *rate.Limiter
+ lastAccess int64
+}
+
+type localLimiter struct {
+ limiters sync.Map
+}
+
+const (
+ cleanupInterval = 5 * time.Minute
+ entryTTL = 10 * time.Minute
+)
+
+func newLocalLimiter() *localLimiter {
+ l := &localLimiter{}
+ go l.cleanup()
+ return l
+}
+
+func (l *localLimiter) cleanup() {
+ ticker := time.NewTicker(cleanupInterval)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ cutoff := time.Now().Add(-entryTTL).Unix()
+ l.limiters.Range(func(key, value any) bool {
+ entry, ok := value.(*limiterEntry)
+ if ok && entry.lastAccess < cutoff {
+ l.limiters.Delete(key)
+ }
+ return true
+ })
+ }
+}
+
+func (l *localLimiter) allow(
+ key string,
+ limit redis_rate.Limit,
+) (*redis_rate.Result, error) {
+ ratePerSec := float64(limit.Rate) / limit.Period.Seconds()
+ now := time.Now().Unix()
+
+ entryI, loaded := l.limiters.Load(key)
+ if !loaded {
+ newEntry := &limiterEntry{
+ limiter: rate.NewLimiter(
+ rate.Limit(ratePerSec),
+ limit.Burst,
+ ),
+ lastAccess: now,
+ }
+ entryI, _ = l.limiters.LoadOrStore(key, newEntry)
+ }
+
+ entry, ok := entryI.(*limiterEntry)
+ if !ok {
+ return nil, fmt.Errorf("invalid limiter entry type")
+ }
+ entry.lastAccess = now
+
+ allowed := entry.limiter.Allow()
+
+ remaining := int(entry.limiter.Tokens())
+ if remaining < 0 {
+ remaining = 0
+ }
+
+ var retryAfter time.Duration
+ if !allowed {
+ retryAfter = time.Duration(float64(time.Second) / ratePerSec)
+ } else {
+ retryAfter = -1
+ }
+
+ allowedInt := 0
+ if allowed {
+ allowedInt = 1
+ }
+
+ return &redis_rate.Result{
+ Limit: limit,
+ Allowed: allowedInt,
+ Remaining: remaining,
+ RetryAfter: retryAfter,
+ ResetAfter: time.Duration(float64(time.Second) / ratePerSec),
+ }, nil
+}
+
+type TierConfig struct {
+ RequestsPerMinute int
+ BurstSize int
+}
+
+var DefaultTiers = map[string]TierConfig{
+ "free": {RequestsPerMinute: 60, BurstSize: 10},
+ "pro": {RequestsPerMinute: 600, BurstSize: 100},
+ "enterprise": {RequestsPerMinute: 6000, BurstSize: 1000},
+}
+
+func TieredRateLimiter(
+ rdb *redis.Client,
+ tiers map[string]TierConfig,
+) func(http.Handler) http.Handler {
+ limiter := redis_rate.NewLimiter(rdb)
+ fallback := newLocalLimiter()
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ userID := GetUserID(r.Context())
+ tier := GetUserTier(r.Context())
+
+ if tier == "" {
+ tier = "free"
+ }
+
+ config, ok := tiers[tier]
+ if !ok {
+ config = tiers["free"]
+ }
+
+ limit := redis_rate.Limit{
+ Rate: config.RequestsPerMinute,
+ Burst: config.BurstSize,
+ Period: time.Minute,
+ }
+
+ key := fmt.Sprintf("ratelimit:user:%s", userID)
+
+ res, err := limiter.Allow(r.Context(), key, limit)
+ if err != nil {
+ //nolint:errcheck // fallback never fails
+ res, _ = fallback.allow(key, limit)
+ }
+
+ w.Header().Set("X-RateLimit-Tier", tier)
+ setRateLimitHeaders(w, res, limit)
+
+ if res.Allowed == 0 {
+ writeRateLimitExceeded(w, res)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func PerMinute(rate, burst int) redis_rate.Limit {
+ return redis_rate.Limit{
+ Rate: rate,
+ Burst: burst,
+ Period: time.Minute,
+ }
+}
+
+func PerSecond(rate, burst int) redis_rate.Limit {
+ return redis_rate.Limit{
+ Rate: rate,
+ Burst: burst,
+ Period: time.Second,
+ }
+}
+
+func PerHour(rate, burst int) redis_rate.Limit {
+ return redis_rate.Limit{
+ Rate: rate,
+ Burst: burst,
+ Period: time.Hour,
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/request_id.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/request_id.go
new file mode 100644
index 00000000..77d98165
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/middleware/request_id.go
@@ -0,0 +1,40 @@
+// AngelaMos | 2026
+// request_id.go
+
+package middleware
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/google/uuid"
+)
+
+type contextKey string
+
+const RequestIDKey contextKey = "request_id"
+
+const RequestIDHeader = "X-Request-ID"
+
+func RequestID(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requestID := r.Header.Get(RequestIDHeader)
+
+ if requestID == "" {
+ requestID = uuid.New().String()
+ }
+
+ ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
+
+ w.Header().Set(RequestIDHeader, requestID)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+func GetRequestID(ctx context.Context) string {
+ if id, ok := ctx.Value(RequestIDKey).(string); ok {
+ return id
+ }
+ return ""
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/server/server.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/server/server.go
new file mode 100644
index 00000000..58d5cbac
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/server/server.go
@@ -0,0 +1,108 @@
+// AngelaMos | 2026
+// server.go
+
+package server
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "time"
+
+ "github.com/go-chi/chi/v5"
+ chimw "github.com/go-chi/chi/v5/middleware"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/config"
+ "github.com/carterperez-dev/templates/go-backend/internal/health"
+)
+
+type Server struct {
+ httpServer *http.Server
+ router *chi.Mux
+ config config.ServerConfig
+ healthHandler *health.Handler
+ logger *slog.Logger
+}
+
+type Config struct {
+ ServerConfig config.ServerConfig
+ HealthHandler *health.Handler
+ Logger *slog.Logger
+}
+
+func New(cfg Config) *Server {
+ router := chi.NewRouter()
+
+ router.Use(chimw.CleanPath)
+ router.Use(chimw.StripSlashes)
+
+ return &Server{
+ httpServer: &http.Server{
+ Addr: cfg.ServerConfig.Address(),
+ Handler: router,
+ ReadTimeout: cfg.ServerConfig.ReadTimeout,
+ WriteTimeout: cfg.ServerConfig.WriteTimeout,
+ IdleTimeout: cfg.ServerConfig.IdleTimeout,
+ },
+ router: router,
+ config: cfg.ServerConfig,
+ healthHandler: cfg.HealthHandler,
+ logger: cfg.Logger,
+ }
+}
+
+func (s *Server) Router() *chi.Mux {
+ return s.router
+}
+
+func (s *Server) Start() error {
+ s.logger.Info("starting HTTP server",
+ "addr", s.config.Address(),
+ "read_timeout", s.config.ReadTimeout,
+ "write_timeout", s.config.WriteTimeout,
+ "idle_timeout", s.config.IdleTimeout,
+ )
+
+ if err := s.httpServer.ListenAndServe(); err != nil &&
+ !errors.Is(err, http.ErrServerClosed) {
+ return fmt.Errorf("http server error: %w", err)
+ }
+
+ return nil
+}
+
+func (s *Server) Shutdown(ctx context.Context, drainDelay time.Duration) error {
+ s.logger.Info("initiating graceful shutdown")
+
+ s.logger.Info("marking server as not ready")
+ if s.healthHandler != nil {
+ s.healthHandler.SetReady(false)
+ s.healthHandler.SetShutdown(true)
+ }
+
+ s.logger.Info("waiting for load balancer to drain",
+ "delay", drainDelay,
+ )
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(drainDelay):
+ }
+
+ s.logger.Info("stopping HTTP server")
+ shutdownCtx, cancel := context.WithTimeout(ctx, s.config.ShutdownTimeout)
+ defer cancel()
+
+ if err := s.httpServer.Shutdown(shutdownCtx); err != nil {
+ return fmt.Errorf("http server shutdown: %w", err)
+ }
+
+ s.logger.Info("HTTP server stopped gracefully")
+ return nil
+}
+
+func (s *Server) Address() string {
+ return s.httpServer.Addr
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/dto.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/dto.go
new file mode 100644
index 00000000..ba2b0686
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/dto.go
@@ -0,0 +1,84 @@
+// AngelaMos | 2026
+// dto.go
+
+package user
+
+import (
+ "time"
+)
+
+type CreateUserRequest struct {
+ Email string `json:"email" validate:"required,email,max=255"`
+ Password string `json:"password" validate:"required,min=8,max=128"`
+ Name string `json:"name" validate:"required,min=1,max=100"`
+}
+
+type UpdateUserRequest struct {
+ Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
+}
+
+type UpdateUserRoleRequest struct {
+ Role string `json:"role" validate:"required,oneof=user admin"`
+}
+
+type UpdateUserTierRequest struct {
+ Tier string `json:"tier" validate:"required,oneof=free pro enterprise"`
+}
+
+type UserResponse struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Role string `json:"role"`
+ Tier string `json:"tier"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+type UserListResponse struct {
+ Users []UserResponse `json:"users"`
+}
+
+type ListUsersParams struct {
+ Page int `json:"page"`
+ PageSize int `json:"page_size"`
+ Search string `json:"search"`
+ Role string `json:"role"`
+ Tier string `json:"tier"`
+}
+
+func (p *ListUsersParams) Normalize() {
+ if p.Page < 1 {
+ p.Page = 1
+ }
+ if p.PageSize < 1 {
+ p.PageSize = 20
+ }
+ if p.PageSize > 100 {
+ p.PageSize = 100
+ }
+}
+
+func (p *ListUsersParams) Offset() int {
+ return (p.Page - 1) * p.PageSize
+}
+
+func ToUserResponse(u *User) UserResponse {
+ return UserResponse{
+ ID: u.ID,
+ Email: u.Email,
+ Name: u.Name,
+ Role: u.Role,
+ Tier: u.Tier,
+ CreatedAt: u.CreatedAt,
+ UpdatedAt: u.UpdatedAt,
+ }
+}
+
+func ToUserResponseList(users []User) []UserResponse {
+ responses := make([]UserResponse, 0, len(users))
+ for _, u := range users {
+ responses = append(responses, ToUserResponse(&u))
+ }
+ return responses
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/entity.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/entity.go
new file mode 100644
index 00000000..f830b054
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/entity.go
@@ -0,0 +1,40 @@
+// AngelaMos | 2026
+// entity.go
+
+package user
+
+import (
+ "time"
+)
+
+type User struct {
+ ID string `db:"id"`
+ Email string `db:"email"`
+ PasswordHash string `db:"password_hash"`
+ Name string `db:"name"`
+ Role string `db:"role"`
+ Tier string `db:"tier"`
+ TokenVersion int `db:"token_version"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+ DeletedAt *time.Time `db:"deleted_at"`
+}
+
+func (u *User) IsDeleted() bool {
+ return u.DeletedAt != nil
+}
+
+func (u *User) IsAdmin() bool {
+ return u.Role == RoleAdmin
+}
+
+const (
+ RoleUser = "user"
+ RoleAdmin = "admin"
+)
+
+const (
+ TierFree = "free"
+ TierPro = "pro"
+ TierEnterprise = "enterprise"
+)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/handler.go
new file mode 100644
index 00000000..8a21889b
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/handler.go
@@ -0,0 +1,288 @@
+// AngelaMos | 2026
+// handler.go
+
+package user
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strconv"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-playground/validator/v10"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+ "github.com/carterperez-dev/templates/go-backend/internal/middleware"
+)
+
+type Handler struct {
+ service *Service
+ validator *validator.Validate
+}
+
+func NewHandler(service *Service) *Handler {
+ return &Handler{
+ service: service,
+ validator: validator.New(validator.WithRequiredStructEnabled()),
+ }
+}
+
+func (h *Handler) RegisterRoutes(
+ r chi.Router,
+ authenticator func(http.Handler) http.Handler,
+) {
+ r.Route("/users", func(r chi.Router) {
+ r.Use(authenticator)
+
+ r.Get("/me", h.GetMe)
+ r.Put("/me", h.UpdateMe)
+ r.Delete("/me", h.DeleteMe)
+ })
+}
+
+func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+
+ user, err := h.service.GetMe(r.Context(), userID)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+
+ var req UpdateUserRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ user, err := h.service.UpdateMe(r.Context(), userID, req)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+func (h *Handler) DeleteMe(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r.Context())
+
+ if err := h.service.DeleteMe(r.Context(), userID); err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+// RegisterAdminRoutes registers admin-only user management endpoints.
+func (h *Handler) RegisterAdminRoutes(
+ r chi.Router,
+ authenticator, adminOnly func(http.Handler) http.Handler,
+) {
+ r.Route("/admin/users", func(r chi.Router) {
+ r.Use(authenticator)
+ r.Use(adminOnly)
+
+ r.Get("/", h.ListUsers)
+ r.Get("/{userID}", h.GetUser)
+ r.Put("/{userID}", h.UpdateUser)
+ r.Put("/{userID}/role", h.UpdateUserRole)
+ r.Put("/{userID}/tier", h.UpdateUserTier)
+ r.Delete("/{userID}", h.DeleteUser)
+ })
+}
+
+// ListUsers returns a paginated list of users with optional filtering.
+func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
+ params := ListUsersParams{
+ Page: parseIntQuery(r, "page", 1),
+ PageSize: parseIntQuery(r, "page_size", 20),
+ Search: r.URL.Query().Get("search"),
+ Role: r.URL.Query().Get("role"),
+ Tier: r.URL.Query().Get("tier"),
+ }
+
+ users, total, err := h.service.ListUsers(r.Context(), params)
+ if err != nil {
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.Paginated(
+ w,
+ ToUserResponseList(users),
+ params.Page,
+ params.PageSize,
+ total,
+ )
+}
+
+// GetUser returns a specific user by ID (admin only).
+func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
+ userID := chi.URLParam(r, "userID")
+
+ user, err := h.service.GetUser(r.Context(), userID)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+// UpdateUser updates a specific user's profile (admin only).
+func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
+ userID := chi.URLParam(r, "userID")
+
+ var req UpdateUserRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ user, err := h.service.UpdateUser(r.Context(), userID, req)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+// UpdateUserRole changes a user's role (admin only).
+func (h *Handler) UpdateUserRole(w http.ResponseWriter, r *http.Request) {
+ userID := chi.URLParam(r, "userID")
+
+ var req UpdateUserRoleRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ user, err := h.service.UpdateUserRole(r.Context(), userID, req.Role)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+// UpdateUserTier changes a user's subscription tier (admin only).
+func (h *Handler) UpdateUserTier(w http.ResponseWriter, r *http.Request) {
+ userID := chi.URLParam(r, "userID")
+
+ var req UpdateUserTierRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ core.BadRequest(w, "invalid request body")
+ return
+ }
+
+ if err := h.validator.Struct(req); err != nil {
+ core.BadRequest(w, core.FormatValidationError(err))
+ return
+ }
+
+ user, err := h.service.UpdateUserTier(r.Context(), userID, req.Tier)
+ if err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.OK(w, ToUserResponse(user))
+}
+
+// DeleteUser soft deletes a user account (admin only).
+func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
+ requesterID := middleware.GetUserID(r.Context())
+ targetID := chi.URLParam(r, "userID")
+
+ if err := h.service.CanDeleteUser(r.Context(), requesterID, targetID); err != nil {
+ if errors.Is(err, core.ErrForbidden) {
+ core.Forbidden(w, "insufficient permissions")
+ return
+ }
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ if err := h.service.DeleteUser(r.Context(), targetID); err != nil {
+ if errors.Is(err, core.ErrNotFound) {
+ core.NotFound(w, "user")
+ return
+ }
+ core.InternalServerError(w, err)
+ return
+ }
+
+ core.NoContent(w)
+}
+
+func parseIntQuery(r *http.Request, key string, defaultVal int) int {
+ val := r.URL.Query().Get(key)
+ if val == "" {
+ return defaultVal
+ }
+
+ parsed, err := strconv.Atoi(val)
+ if err != nil {
+ return defaultVal
+ }
+
+ return parsed
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/repository.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/repository.go
new file mode 100644
index 00000000..a255b08e
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/repository.go
@@ -0,0 +1,289 @@
+// AngelaMos | 2026
+// repository.go
+
+package user
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/jackc/pgx/v5/pgconn"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+type Repository interface {
+ Create(ctx context.Context, user *User) error
+ GetByID(ctx context.Context, id string) (*User, error)
+ GetByEmail(ctx context.Context, email string) (*User, error)
+ Update(ctx context.Context, user *User) error
+ UpdatePassword(ctx context.Context, id, passwordHash string) error
+ IncrementTokenVersion(ctx context.Context, id string) error
+ SoftDelete(ctx context.Context, id string) error
+ List(ctx context.Context, params ListUsersParams) ([]User, int, error)
+ ExistsByEmail(ctx context.Context, email string) (bool, error)
+}
+
+type repository struct {
+ db core.DBTX
+}
+
+func NewRepository(db core.DBTX) Repository {
+ return &repository{db: db}
+}
+
+func (r *repository) Create(ctx context.Context, user *User) error {
+ query := `
+ INSERT INTO users (id, email, password_hash, name, role, tier)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING created_at, updated_at, token_version`
+
+ err := r.db.GetContext(ctx, user, query,
+ user.ID,
+ user.Email,
+ user.PasswordHash,
+ user.Name,
+ user.Role,
+ user.Tier,
+ )
+ if err != nil {
+ if isDuplicateKeyError(err) {
+ return fmt.Errorf("create user: %w", core.ErrDuplicateKey)
+ }
+ return fmt.Errorf("create user: %w", err)
+ }
+
+ return nil
+}
+
+func (r *repository) GetByID(ctx context.Context, id string) (*User, error) {
+ query := `
+ SELECT id, email, password_hash, name, role, tier, token_version,
+ created_at, updated_at, deleted_at
+ FROM users
+ WHERE id = $1 AND deleted_at IS NULL`
+
+ var user User
+ err := r.db.GetContext(ctx, &user, query, id)
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, fmt.Errorf("get user: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get user: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (r *repository) GetByEmail(
+ ctx context.Context,
+ email string,
+) (*User, error) {
+ query := `
+ SELECT id, email, password_hash, name, role, tier, token_version,
+ created_at, updated_at, deleted_at
+ FROM users
+ WHERE email = $1 AND deleted_at IS NULL`
+
+ var user User
+ err := r.db.GetContext(ctx, &user, query, email)
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, fmt.Errorf("get user by email: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get user by email: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (r *repository) Update(ctx context.Context, user *User) error {
+ query := `
+ UPDATE users
+ SET name = $2, role = $3, tier = $4, updated_at = NOW()
+ WHERE id = $1 AND deleted_at IS NULL
+ RETURNING updated_at`
+
+ err := r.db.GetContext(ctx, &user.UpdatedAt, query,
+ user.ID,
+ user.Name,
+ user.Role,
+ user.Tier,
+ )
+ if errors.Is(err, sql.ErrNoRows) {
+ return fmt.Errorf("update user: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return fmt.Errorf("update user: %w", err)
+ }
+
+ return nil
+}
+
+func (r *repository) UpdatePassword(
+ ctx context.Context,
+ id, passwordHash string,
+) error {
+ query := `
+ UPDATE users
+ SET password_hash = $2, updated_at = NOW()
+ WHERE id = $1 AND deleted_at IS NULL`
+
+ result, err := r.db.ExecContext(ctx, query, id, passwordHash)
+ if err != nil {
+ return fmt.Errorf("update password: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("update password: %w", err)
+ }
+
+ if rows == 0 {
+ return fmt.Errorf("update password: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+
+func (r *repository) IncrementTokenVersion(
+ ctx context.Context,
+ id string,
+) error {
+ query := `
+ UPDATE users
+ SET token_version = token_version + 1, updated_at = NOW()
+ WHERE id = $1 AND deleted_at IS NULL`
+
+ result, err := r.db.ExecContext(ctx, query, id)
+ if err != nil {
+ return fmt.Errorf("increment token version: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("increment token version: %w", err)
+ }
+
+ if rows == 0 {
+ return fmt.Errorf("increment token version: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+
+func (r *repository) SoftDelete(ctx context.Context, id string) error {
+ query := `
+ UPDATE users
+ SET deleted_at = NOW(), updated_at = NOW()
+ WHERE id = $1 AND deleted_at IS NULL`
+
+ result, err := r.db.ExecContext(ctx, query, id)
+ if err != nil {
+ return fmt.Errorf("delete user: %w", err)
+ }
+
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("delete user: %w", err)
+ }
+
+ if rows == 0 {
+ return fmt.Errorf("delete user: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+
+func (r *repository) List(
+ ctx context.Context,
+ params ListUsersParams,
+) ([]User, int, error) {
+ params.Normalize()
+
+ var conditions []string
+ var args []any
+ argIdx := 1
+
+ conditions = append(conditions, "deleted_at IS NULL")
+
+ if params.Search != "" {
+ conditions = append(conditions, fmt.Sprintf(
+ "(email ILIKE $%d OR name ILIKE $%d)", argIdx, argIdx))
+ args = append(args, "%"+escapeLike(params.Search)+"%")
+ argIdx++
+ }
+
+ if params.Role != "" {
+ conditions = append(conditions, fmt.Sprintf("role = $%d", argIdx))
+ args = append(args, params.Role)
+ argIdx++
+ }
+
+ if params.Tier != "" {
+ conditions = append(conditions, fmt.Sprintf("tier = $%d", argIdx))
+ args = append(args, params.Tier)
+ argIdx++
+ }
+
+ whereClause := strings.Join(conditions, " AND ")
+
+ countQuery := fmt.Sprintf(
+ "SELECT COUNT(*) FROM users WHERE %s",
+ whereClause,
+ )
+ var total int
+ if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
+ return nil, 0, fmt.Errorf("count users: %w", err)
+ }
+
+ query := fmt.Sprintf(`
+ SELECT id, email, name, role, tier, token_version,
+ created_at, updated_at, deleted_at
+ FROM users
+ WHERE %s
+ ORDER BY created_at DESC
+ LIMIT $%d OFFSET $%d`,
+ whereClause, argIdx, argIdx+1)
+
+ args = append(args, params.PageSize, params.Offset())
+
+ var users []User
+ if err := r.db.SelectContext(ctx, &users, query, args...); err != nil {
+ return nil, 0, fmt.Errorf("list users: %w", err)
+ }
+
+ return users, total, nil
+}
+
+func (r *repository) ExistsByEmail(
+ ctx context.Context,
+ email string,
+) (bool, error) {
+ query := `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1 AND deleted_at IS NULL)`
+
+ var exists bool
+ if err := r.db.GetContext(ctx, &exists, query, email); err != nil {
+ return false, fmt.Errorf("check email exists: %w", err)
+ }
+
+ return exists, nil
+}
+
+func isDuplicateKeyError(err error) bool {
+ var pgErr *pgconn.PgError
+ if errors.As(err, &pgErr) {
+ return pgErr.Code == "23505"
+ }
+ return false
+}
+
+func escapeLike(s string) string {
+ s = strings.ReplaceAll(s, "\\", "\\\\")
+ s = strings.ReplaceAll(s, "%", "\\%")
+ s = strings.ReplaceAll(s, "_", "\\_")
+ return s
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/service.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/service.go
new file mode 100644
index 00000000..699d24d5
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/user/service.go
@@ -0,0 +1,256 @@
+// AngelaMos | 2026
+// service.go
+
+package user
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/google/uuid"
+
+ "github.com/carterperez-dev/templates/go-backend/internal/auth"
+ "github.com/carterperez-dev/templates/go-backend/internal/core"
+)
+
+type Service struct {
+ repo Repository
+}
+
+func NewService(repo Repository) *Service {
+ return &Service{repo: repo}
+}
+
+func (s *Service) GetByID(
+ ctx context.Context,
+ id string,
+) (*auth.UserInfo, error) {
+ user, err := s.repo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ return toUserInfo(user), nil
+}
+
+func (s *Service) GetByEmail(
+ ctx context.Context,
+ email string,
+) (*auth.UserInfo, error) {
+ user, err := s.repo.GetByEmail(ctx, strings.ToLower(email))
+ if err != nil {
+ return nil, err
+ }
+
+ return toUserInfo(user), nil
+}
+
+func (s *Service) Create(
+ ctx context.Context,
+ email, passwordHash, name string,
+) (*auth.UserInfo, error) {
+ user := &User{
+ ID: uuid.New().String(),
+ Email: strings.ToLower(email),
+ PasswordHash: passwordHash,
+ Name: name,
+ Role: RoleUser,
+ Tier: TierFree,
+ }
+
+ if err := s.repo.Create(ctx, user); err != nil {
+ return nil, err
+ }
+
+ return toUserInfo(user), nil
+}
+
+func (s *Service) IncrementTokenVersion(
+ ctx context.Context,
+ userID string,
+) error {
+ return s.repo.IncrementTokenVersion(ctx, userID)
+}
+
+func (s *Service) UpdatePassword(
+ ctx context.Context,
+ userID, passwordHash string,
+) error {
+ return s.repo.UpdatePassword(ctx, userID, passwordHash)
+}
+
+func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
+ return s.repo.GetByID(ctx, id)
+}
+
+func (s *Service) UpdateUser(
+ ctx context.Context,
+ id string,
+ req UpdateUserRequest,
+) (*User, error) {
+ user, err := s.repo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ if req.Name != nil {
+ user.Name = *req.Name
+ }
+
+ if err := s.repo.Update(ctx, user); err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func (s *Service) UpdateUserRole(
+ ctx context.Context,
+ id, role string,
+) (*User, error) {
+ if role != RoleUser && role != RoleAdmin {
+ return nil, fmt.Errorf(
+ "update role: invalid role %q: %w",
+ role,
+ core.ErrInvalidInput,
+ )
+ }
+
+ user, err := s.repo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ user.Role = role
+
+ if err := s.repo.Update(ctx, user); err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func (s *Service) UpdateUserTier(
+ ctx context.Context,
+ id, tier string,
+) (*User, error) {
+ if tier != TierFree && tier != TierPro && tier != TierEnterprise {
+ return nil, fmt.Errorf(
+ "update tier: invalid tier %q: %w",
+ tier,
+ core.ErrInvalidInput,
+ )
+ }
+
+ user, err := s.repo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ user.Tier = tier
+
+ if err := s.repo.Update(ctx, user); err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func (s *Service) DeleteUser(ctx context.Context, id string) error {
+ return s.repo.SoftDelete(ctx, id)
+}
+
+func (s *Service) ListUsers(
+ ctx context.Context,
+ params ListUsersParams,
+) ([]User, int, error) {
+ return s.repo.List(ctx, params)
+}
+
+func (s *Service) GetMe(ctx context.Context, userID string) (*User, error) {
+ if userID == "" {
+ return nil, fmt.Errorf("get me: %w", core.ErrUnauthorized)
+ }
+
+ user, err := s.repo.GetByID(ctx, userID)
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func (s *Service) UpdateMe(
+ ctx context.Context,
+ userID string,
+ req UpdateUserRequest,
+) (*User, error) {
+ if userID == "" {
+ return nil, fmt.Errorf("update me: %w", core.ErrUnauthorized)
+ }
+
+ return s.UpdateUser(ctx, userID, req)
+}
+
+func (s *Service) DeleteMe(ctx context.Context, userID string) error {
+ if userID == "" {
+ return fmt.Errorf("delete me: %w", core.ErrUnauthorized)
+ }
+
+ return s.repo.SoftDelete(ctx, userID)
+}
+
+func (s *Service) EmailExists(
+ ctx context.Context,
+ email string,
+) (bool, error) {
+ exists, err := s.repo.ExistsByEmail(ctx, email)
+ if err != nil {
+ return false, err
+ }
+ return exists, nil
+}
+
+func (s *Service) CanDeleteUser(
+ ctx context.Context,
+ requesterID, targetID string,
+) error {
+ if requesterID == targetID {
+ return nil
+ }
+
+ requester, err := s.repo.GetByID(ctx, requesterID)
+ if err != nil {
+ return err
+ }
+
+ if !requester.IsAdmin() {
+ return fmt.Errorf("delete user: %w", core.ErrForbidden)
+ }
+
+ target, err := s.repo.GetByID(ctx, targetID)
+ if err != nil {
+ return err
+ }
+
+ if target.IsAdmin() {
+ return fmt.Errorf("cannot delete admin users: %w", core.ErrForbidden)
+ }
+
+ return nil
+}
+
+func toUserInfo(u *User) *auth.UserInfo {
+ return &auth.UserInfo{
+ ID: u.ID,
+ Email: u.Email,
+ Name: u.Name,
+ PasswordHash: u.PasswordHash,
+ Role: u.Role,
+ Tier: u.Tier,
+ TokenVersion: u.TokenVersion,
+ }
+}
+
+var _ auth.UserProvider = (*Service)(nil)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/go.docker b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/go.docker
new file mode 100644
index 00000000..557fd500
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/go.docker
@@ -0,0 +1,19 @@
+# ©AngelaMos | 2026
+# go.docker
+
+FROM golang:1.25-bookworm
+
+WORKDIR /app
+
+RUN go install github.com/air-verse/air@latest \
+ && go install github.com/pressly/goose/v3/cmd/goose@latest
+
+COPY backend/go.mod backend/go.sum ./
+RUN go mod download
+
+ENV GO_ENV=development \
+ PATH=/go/bin:$PATH
+
+EXPOSE 8080
+
+CMD ["air", "-c", ".air.toml"]
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/vite.docker b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/vite.docker
new file mode 100644
index 00000000..e3400dcb
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/dev/vite.docker
@@ -0,0 +1,14 @@
+# ©AngelaMos | 2026
+# vite.docker
+
+FROM node:24-alpine
+
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+WORKDIR /app
+
+ENV NODE_ENV=development
+
+EXPOSE 5173
+
+CMD ["pnpm", "dev", "--host", "0.0.0.0"]
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/go.docker b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/go.docker
new file mode 100644
index 00000000..d903317f
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/go.docker
@@ -0,0 +1,26 @@
+# ©AngelaMos | 2026
+# go.docker
+
+FROM golang:1.25-bookworm AS builder
+
+WORKDIR /build
+
+COPY backend/go.mod backend/go.sum ./
+RUN go mod download
+
+COPY backend/ .
+
+RUN CGO_ENABLED=0 GOOS=linux \
+ go build -trimpath -ldflags="-s -w" -o /app ./cmd/api
+
+FROM gcr.io/distroless/static-debian12:nonroot
+
+COPY --from=builder /app /app
+COPY --from=builder /build/migrations /migrations
+COPY --from=builder /build/config.yaml /config.yaml
+
+USER nonroot:nonroot
+
+EXPOSE 8080
+
+ENTRYPOINT ["/app", "-config", "/config.yaml"]
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/vite.docker b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/vite.docker
new file mode 100644
index 00000000..9d5bf2b5
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/conf/docker/prod/vite.docker
@@ -0,0 +1,26 @@
+# ©AngelaMos | 2026
+# vite.docker
+
+FROM node:24-alpine AS build
+
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+WORKDIR /app
+
+COPY frontend/package.json frontend/pnpm-lock.yaml* ./
+
+RUN pnpm install --frozen-lockfile || pnpm install
+
+COPY frontend/ .
+
+RUN pnpm build
+
+FROM nginx:1.27-alpine
+
+COPY conf/nginx/nginx.prod.conf /etc/nginx/nginx.conf
+COPY conf/nginx/prod.nginx /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.dockerignore b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.dockerignore
new file mode 100644
index 00000000..a0256ec7
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.dockerignore
@@ -0,0 +1,15 @@
+node_modules
+build
+dist
+.git
+.gitignore
+*.md
+.env*
+.vscode
+.idea
+*.log
+npm-debug.log*
+pnpm-debug.log*
+.DS_Store
+coverage
+.nyc_output
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.gitignore b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.gitignore
new file mode 100644
index 00000000..61cb0c2e
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+.vite
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.stylelintignore b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.stylelintignore
new file mode 100644
index 00000000..37da03e1
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/.stylelintignore
@@ -0,0 +1,22 @@
+# ©AngelaMos | 2025
+# .stylelintignore
+
+# Dependencies
+node_modules/
+
+# Production builds
+dist/
+build/
+out/
+
+# JS/TS files
+**/*.js
+**/*.jsx
+**/*.ts
+**/*.tsx
+
+# Generated files
+*.min.css
+
+# Error system styles - ignore from linting
+src/core/app/_toastStyles.scss
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/biome.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/biome.json
new file mode 100644
index 00000000..7f29029a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/biome.json
@@ -0,0 +1,94 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 82,
+ "lineEnding": "lf"
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "jsxQuoteStyle": "double",
+ "semicolons": "asNeeded",
+ "trailingCommas": "es5",
+ "arrowParentheses": "always"
+ }
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "complexity": {
+ "noExcessiveCognitiveComplexity": {
+ "level": "error",
+ "options": { "maxAllowedComplexity": 25 }
+ },
+ "noForEach": "off",
+ "useLiteralKeys": "off"
+ },
+ "correctness": {
+ "noUnusedVariables": "error",
+ "noUnusedImports": "error",
+ "useExhaustiveDependencies": "warn",
+ "useHookAtTopLevel": "error",
+ "noUndeclaredVariables": "error"
+ },
+ "style": {
+ "useImportType": "error",
+ "useConst": "error",
+ "useTemplate": "error",
+ "useSelfClosingElements": "error",
+ "useFragmentSyntax": "error",
+ "noNonNullAssertion": "error",
+ "useConsistentArrayType": {
+ "level": "error",
+ "options": { "syntax": "shorthand" }
+ },
+ "useNamingConvention": "off"
+ },
+ "suspicious": {
+ "noExplicitAny": "error",
+ "noDebugger": "error",
+ "noConsole": "warn",
+ "noArrayIndexKey": "warn",
+ "noAssignInExpressions": "error",
+ "noDoubleEquals": "error",
+ "noRedeclare": "error",
+ "noVar": "error"
+ },
+ "security": {
+ "noDangerouslySetInnerHtml": "error"
+ },
+ "a11y": {
+ "useAltText": "error",
+ "useAnchorContent": "error",
+ "useKeyWithClickEvents": "error",
+ "noStaticElementInteractions": "error",
+ "useButtonType": "error",
+ "useValidAnchor": "error"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "includes": ["src/main.tsx"],
+ "linter": {
+ "rules": {
+ "style": {
+ "noNonNullAssertion": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/index.html b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/index.html
new file mode 100644
index 00000000..ce1f11bb
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+ Monitor The Situation Dashboard
+
+
+
+
+
+
+
+
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json
new file mode 100644
index 00000000..fc6e78c7
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "monitor-the-situation-dashboard",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "lint": "biome check .",
+ "lint:fix": "biome check --write .",
+ "format": "biome format --write .",
+ "typecheck": "tsc -b",
+ "lint:scss": "stylelint '**/*.scss'",
+ "lint:scss:fix": "stylelint '**/*.scss' --fix"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.12",
+ "axios": "^1.13.0",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.0",
+ "react-error-boundary": "^6.0.0",
+ "react-icon": "^1.0.0",
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.1.1",
+ "sonner": "^2.0.7",
+ "zod": "^4.1.13",
+ "zustand": "^5.0.9"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.3.8",
+ "@tanstack/react-query-devtools": "^5.91.1",
+ "@types/node": "^24.10.2",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "sass": "^1.95.0",
+ "stylelint": "^16.26.1",
+ "stylelint-config-prettier-scss": "^1.0.0",
+ "stylelint-config-standard-scss": "^16.0.0",
+ "typescript": "~5.9.3",
+ "vite": "npm:rolldown-vite@7.2.5",
+ "vite-tsconfig-paths": "^5.1.0"
+ },
+ "pnpm": {
+ "overrides": {
+ "vite": "npm:rolldown-vite@7.2.5"
+ }
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml
new file mode 100644
index 00000000..d04a7163
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml
@@ -0,0 +1,2603 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+overrides:
+ vite: npm:rolldown-vite@7.2.5
+
+importers:
+
+ .:
+ dependencies:
+ '@tanstack/react-query':
+ specifier: ^5.90.12
+ version: 5.90.12(react@19.2.1)
+ axios:
+ specifier: ^1.13.0
+ version: 1.13.2
+ react:
+ specifier: ^19.2.1
+ version: 19.2.1
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.1(react@19.2.1)
+ react-error-boundary:
+ specifier: ^6.0.0
+ version: 6.0.0(react@19.2.1)
+ react-icon:
+ specifier: ^1.0.0
+ version: 1.0.0(babel-runtime@5.8.38)(react@19.2.1)
+ react-icons:
+ specifier: ^5.5.0
+ version: 5.5.0(react@19.2.1)
+ react-router-dom:
+ specifier: ^7.1.1
+ version: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ zod:
+ specifier: ^4.1.13
+ version: 4.1.13
+ zustand:
+ specifier: ^5.0.9
+ version: 5.0.9(@types/react@19.2.7)(react@19.2.1)
+ devDependencies:
+ '@biomejs/biome':
+ specifier: ^2.3.8
+ version: 2.3.8
+ '@tanstack/react-query-devtools':
+ specifier: ^5.91.1
+ version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)
+ '@types/node':
+ specifier: ^24.10.2
+ version: 24.10.2
+ '@types/react':
+ specifier: ^19.2.7
+ version: 19.2.7
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.7)
+ '@vitejs/plugin-react':
+ specifier: ^5.1.1
+ version: 5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))
+ sass:
+ specifier: ^1.95.0
+ version: 1.95.0
+ stylelint:
+ specifier: ^16.26.1
+ version: 16.26.1(typescript@5.9.3)
+ stylelint-config-prettier-scss:
+ specifier: ^1.0.0
+ version: 1.0.0(stylelint@16.26.1(typescript@5.9.3))
+ stylelint-config-standard-scss:
+ specifier: ^16.0.0
+ version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3))
+ typescript:
+ specifier: ~5.9.3
+ version: 5.9.3
+ vite:
+ specifier: npm:rolldown-vite@7.2.5
+ version: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)
+ vite-tsconfig-paths:
+ specifier: ^5.1.0
+ version: 5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3)
+
+packages:
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.5':
+ resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.5':
+ resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.5':
+ resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.3':
+ resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.4':
+ resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.5':
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/runtime@7.28.4':
+ resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.5':
+ resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.5':
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+ engines: {node: '>=6.9.0'}
+
+ '@biomejs/biome@2.3.8':
+ resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+
+ '@biomejs/cli-darwin-arm64@2.3.8':
+ resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@biomejs/cli-darwin-x64@2.3.8':
+ resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@biomejs/cli-linux-arm64-musl@2.3.8':
+ resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-arm64@2.3.8':
+ resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64-musl@2.3.8':
+ resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64@2.3.8':
+ resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-win32-arm64@2.3.8':
+ resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@biomejs/cli-win32-x64@2.3.8':
+ resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+
+ '@cacheable/memory@2.0.6':
+ resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==}
+
+ '@cacheable/utils@2.3.2':
+ resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==}
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.20':
+ resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
+ '@csstools/media-query-list-parser@4.0.3':
+ resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/selector-specificity@5.0.0':
+ resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ postcss-selector-parser: ^7.0.0
+
+ '@dual-bundle/import-meta-resolve@4.2.1':
+ resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==}
+
+ '@emnapi/core@1.7.1':
+ resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
+
+ '@emnapi/runtime@1.7.1':
+ resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
+
+ '@emnapi/wasi-threads@1.1.0':
+ resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@keyv/bigmap@1.3.0':
+ resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ keyv: ^5.5.4
+
+ '@keyv/serialize@1.1.1':
+ resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
+
+ '@napi-rs/wasm-runtime@1.1.0':
+ resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@oxc-project/runtime@0.97.0':
+ resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@oxc-project/types@0.97.0':
+ resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==}
+
+ '@parcel/watcher-android-arm64@2.5.1':
+ resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ '@parcel/watcher-darwin-arm64@2.5.1':
+ resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@parcel/watcher-darwin-x64@2.5.1':
+ resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@parcel/watcher-freebsd-x64@2.5.1':
+ resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@parcel/watcher-linux-arm-glibc@2.5.1':
+ resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm-musl@2.5.1':
+ resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.1':
+ resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.1':
+ resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.1':
+ resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-musl@2.5.1':
+ resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-win32-arm64@2.5.1':
+ resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@parcel/watcher-win32-ia32@2.5.1':
+ resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@parcel/watcher-win32-x64@2.5.1':
+ resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@parcel/watcher@2.5.1':
+ resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
+ engines: {node: '>= 10.0.0'}
+
+ '@rolldown/binding-android-arm64@1.0.0-beta.50':
+ resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.50':
+ resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-beta.50':
+ resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.50':
+ resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
+ resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
+ resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
+ resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
+ resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
+ resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
+ resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
+ resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
+ resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
+ resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
+ resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-beta.50':
+ resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
+
+ '@rolldown/pluginutils@1.0.0-beta.53':
+ resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
+
+ '@tanstack/query-core@5.90.12':
+ resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==}
+
+ '@tanstack/query-devtools@5.91.1':
+ resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==}
+
+ '@tanstack/react-query-devtools@5.91.1':
+ resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.90.10
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.90.12':
+ resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/node@24.10.2':
+ resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.7':
+ resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
+
+ '@vitejs/plugin-react@5.1.2':
+ resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ ajv@8.17.1:
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ astral-regex@2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.13.2:
+ resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
+
+ babel-runtime@5.8.38:
+ resolution: {integrity: sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg==}
+
+ balanced-match@2.0.0:
+ resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
+
+ baseline-browser-mapping@2.9.5:
+ resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==}
+ hasBin: true
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ cacheable@2.3.0:
+ resolution: {integrity: sha512-HHiAvOBmlcR2f3SQ7kdlYD8+AUJG+wlFZ/Ze8tl1Vzvz0MdOh8IYA/EFU4ve8t1/sZ0j4MGi7ST5MoTwHessQA==}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001759:
+ resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colord@2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ core-js@1.2.7:
+ resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==}
+ deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ css-functions-list@3.2.3:
+ resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
+ engines: {node: '>=12 || >=16'}
+
+ css-tree@3.1.0:
+ resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ detect-libc@1.0.3:
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.267:
+ resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fastest-levenshtein@1.0.16:
+ resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
+ engines: {node: '>= 4.9.1'}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@11.1.1:
+ resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ flat-cache@6.1.19:
+ resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ global-modules@2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+
+ global-prefix@3.0.0:
+ resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
+ engines: {node: '>=6'}
+
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
+ globjoin@0.1.4:
+ resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
+
+ globrex@0.1.2:
+ resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hashery@1.3.0:
+ resolution: {integrity: sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==}
+ engines: {node: '>=20'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hookified@1.14.0:
+ resolution: {integrity: sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==}
+
+ html-tags@3.3.1:
+ resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
+ engines: {node: '>=8'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ immutable@5.1.4:
+ resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@5.5.5:
+ resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
+ known-css-properties@0.37.0:
+ resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
+
+ lightningcss-android-arm64@1.30.2:
+ resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.30.2:
+ resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.30.2:
+ resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.30.2:
+ resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.30.2:
+ resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.30.2:
+ resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ lodash.truncate@4.4.2:
+ resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mathml-tag-names@2.1.3:
+ resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
+
+ mdn-data@2.12.2:
+ resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+
+ mdn-data@2.25.0:
+ resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ postcss-media-query-parser@0.2.3:
+ resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
+
+ postcss-resolve-nested-selector@0.1.6:
+ resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
+
+ postcss-safe-parser@7.0.1:
+ resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
+ engines: {node: '>=18.0'}
+ peerDependencies:
+ postcss: ^8.4.31
+
+ postcss-scss@4.0.9:
+ resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.4.29
+
+ postcss-selector-parser@7.1.1:
+ resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
+ engines: {node: '>=4'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ qified@0.5.3:
+ resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==}
+ engines: {node: '>=20'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@19.2.1:
+ resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
+ peerDependencies:
+ react: ^19.2.1
+
+ react-error-boundary@6.0.0:
+ resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==}
+ peerDependencies:
+ react: '>=16.13.1'
+
+ react-icon@1.0.0:
+ resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==}
+ peerDependencies:
+ babel-runtime: ^5.3.3
+ react: '>=0.12.0'
+
+ react-icons@5.5.0:
+ resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
+ peerDependencies:
+ react: '*'
+
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react-router-dom@7.10.1:
+ resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.10.1:
+ resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.2.1:
+ resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
+ engines: {node: '>=0.10.0'}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rolldown-vite@7.2.5:
+ resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ esbuild: ^0.25.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ rolldown@1.0.0-beta.50:
+ resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ sass@1.95.0:
+ resolution: {integrity: sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
+ slice-ansi@4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ stylelint-config-prettier-scss@1.0.0:
+ resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==}
+ engines: {node: 14.* || 16.* || >= 18}
+ hasBin: true
+ peerDependencies:
+ stylelint: '>=15.0.0'
+
+ stylelint-config-recommended-scss@16.0.2:
+ resolution: {integrity: sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^16.24.0
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+
+ stylelint-config-recommended@17.0.0:
+ resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ stylelint: ^16.23.0
+
+ stylelint-config-standard-scss@16.0.0:
+ resolution: {integrity: sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^16.23.1
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+
+ stylelint-config-standard@39.0.1:
+ resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ stylelint: ^16.23.0
+
+ stylelint-scss@6.13.0:
+ resolution: {integrity: sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ stylelint: ^16.8.2
+
+ stylelint@16.26.1:
+ resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==}
+ engines: {node: '>=18.12.0'}
+ hasBin: true
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-hyperlinks@3.2.0:
+ resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
+ engines: {node: '>=14.18'}
+
+ svg-tags@1.0.0:
+ resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
+
+ table@6.9.0:
+ resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
+ engines: {node: '>=10.0.0'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ tsconfck@3.1.6:
+ resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ update-browserslist-db@1.2.2:
+ resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite-tsconfig-paths@5.1.4:
+ resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
+ peerDependencies:
+ vite: '*'
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ which@1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+
+ write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ zod@4.1.13:
+ resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
+
+ zustand@5.0.9:
+ resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
+snapshots:
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.5': {}
+
+ '@babel/core@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
+ '@babel/helpers': 7.28.4
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.28.5
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.4':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+
+ '@babel/parser@7.28.5':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/runtime@7.28.4': {}
+
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@babel/traverse@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.5':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@biomejs/biome@2.3.8':
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 2.3.8
+ '@biomejs/cli-darwin-x64': 2.3.8
+ '@biomejs/cli-linux-arm64': 2.3.8
+ '@biomejs/cli-linux-arm64-musl': 2.3.8
+ '@biomejs/cli-linux-x64': 2.3.8
+ '@biomejs/cli-linux-x64-musl': 2.3.8
+ '@biomejs/cli-win32-arm64': 2.3.8
+ '@biomejs/cli-win32-x64': 2.3.8
+
+ '@biomejs/cli-darwin-arm64@2.3.8':
+ optional: true
+
+ '@biomejs/cli-darwin-x64@2.3.8':
+ optional: true
+
+ '@biomejs/cli-linux-arm64-musl@2.3.8':
+ optional: true
+
+ '@biomejs/cli-linux-arm64@2.3.8':
+ optional: true
+
+ '@biomejs/cli-linux-x64-musl@2.3.8':
+ optional: true
+
+ '@biomejs/cli-linux-x64@2.3.8':
+ optional: true
+
+ '@biomejs/cli-win32-arm64@2.3.8':
+ optional: true
+
+ '@biomejs/cli-win32-x64@2.3.8':
+ optional: true
+
+ '@cacheable/memory@2.0.6':
+ dependencies:
+ '@cacheable/utils': 2.3.2
+ '@keyv/bigmap': 1.3.0(keyv@5.5.5)
+ hookified: 1.14.0
+ keyv: 5.5.5
+
+ '@cacheable/utils@2.3.2':
+ dependencies:
+ hashery: 1.3.0
+ keyv: 5.5.5
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.20': {}
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
+ '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)':
+ dependencies:
+ postcss-selector-parser: 7.1.1
+
+ '@dual-bundle/import-meta-resolve@4.2.1': {}
+
+ '@emnapi/core@1.7.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.1.0
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.7.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.1.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@keyv/bigmap@1.3.0(keyv@5.5.5)':
+ dependencies:
+ hashery: 1.3.0
+ hookified: 1.14.0
+ keyv: 5.5.5
+
+ '@keyv/serialize@1.1.1': {}
+
+ '@napi-rs/wasm-runtime@1.1.0':
+ dependencies:
+ '@emnapi/core': 1.7.1
+ '@emnapi/runtime': 1.7.1
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@oxc-project/runtime@0.97.0': {}
+
+ '@oxc-project/types@0.97.0': {}
+
+ '@parcel/watcher-android-arm64@2.5.1':
+ optional: true
+
+ '@parcel/watcher-darwin-arm64@2.5.1':
+ optional: true
+
+ '@parcel/watcher-darwin-x64@2.5.1':
+ optional: true
+
+ '@parcel/watcher-freebsd-x64@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-arm-glibc@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-arm-musl@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-musl@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-x64-glibc@2.5.1':
+ optional: true
+
+ '@parcel/watcher-linux-x64-musl@2.5.1':
+ optional: true
+
+ '@parcel/watcher-win32-arm64@2.5.1':
+ optional: true
+
+ '@parcel/watcher-win32-ia32@2.5.1':
+ optional: true
+
+ '@parcel/watcher-win32-x64@2.5.1':
+ optional: true
+
+ '@parcel/watcher@2.5.1':
+ dependencies:
+ detect-libc: 1.0.3
+ is-glob: 4.0.3
+ micromatch: 4.0.8
+ node-addon-api: 7.1.1
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.5.1
+ '@parcel/watcher-darwin-arm64': 2.5.1
+ '@parcel/watcher-darwin-x64': 2.5.1
+ '@parcel/watcher-freebsd-x64': 2.5.1
+ '@parcel/watcher-linux-arm-glibc': 2.5.1
+ '@parcel/watcher-linux-arm-musl': 2.5.1
+ '@parcel/watcher-linux-arm64-glibc': 2.5.1
+ '@parcel/watcher-linux-arm64-musl': 2.5.1
+ '@parcel/watcher-linux-x64-glibc': 2.5.1
+ '@parcel/watcher-linux-x64-musl': 2.5.1
+ '@parcel/watcher-win32-arm64': 2.5.1
+ '@parcel/watcher-win32-ia32': 2.5.1
+ '@parcel/watcher-win32-x64': 2.5.1
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.1.0
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-beta.50': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.53': {}
+
+ '@tanstack/query-core@5.90.12': {}
+
+ '@tanstack/query-devtools@5.91.1': {}
+
+ '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)':
+ dependencies:
+ '@tanstack/query-devtools': 5.91.1
+ '@tanstack/react-query': 5.90.12(react@19.2.1)
+ react: 19.2.1
+
+ '@tanstack/react-query@5.90.12(react@19.2.1)':
+ dependencies:
+ '@tanstack/query-core': 5.90.12
+ react: 19.2.1
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/node@24.10.2':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.7)':
+ dependencies:
+ '@types/react': 19.2.7
+
+ '@types/react@19.2.7':
+ dependencies:
+ csstype: 3.2.3
+
+ '@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
+ '@rolldown/pluginutils': 1.0.0-beta.53
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ ajv@8.17.1:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ array-union@2.1.0: {}
+
+ astral-regex@2.0.0: {}
+
+ asynckit@0.4.0: {}
+
+ axios@1.13.2:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.5
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ babel-runtime@5.8.38:
+ dependencies:
+ core-js: 1.2.7
+
+ balanced-match@2.0.0: {}
+
+ baseline-browser-mapping@2.9.5: {}
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.9.5
+ caniuse-lite: 1.0.30001759
+ electron-to-chromium: 1.5.267
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.2(browserslist@4.28.1)
+
+ cacheable@2.3.0:
+ dependencies:
+ '@cacheable/memory': 2.0.6
+ '@cacheable/utils': 2.3.2
+ hookified: 1.14.0
+ keyv: 5.5.5
+ qified: 0.5.3
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001759: {}
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ colord@2.9.3: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ convert-source-map@2.0.0: {}
+
+ cookie@1.1.1: {}
+
+ core-js@1.2.7: {}
+
+ cosmiconfig@9.0.0(typescript@5.9.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 5.9.3
+
+ css-functions-list@3.2.3: {}
+
+ css-tree@3.1.0:
+ dependencies:
+ mdn-data: 2.12.2
+ source-map-js: 1.2.1
+
+ cssesc@3.0.0: {}
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ delayed-stream@1.0.0: {}
+
+ detect-libc@1.0.3:
+ optional: true
+
+ detect-libc@2.1.2: {}
+
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.267: {}
+
+ emoji-regex@8.0.0: {}
+
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ escalade@3.2.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-uri@3.1.0: {}
+
+ fastest-levenshtein@1.0.16: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ file-entry-cache@11.1.1:
+ dependencies:
+ flat-cache: 6.1.19
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ flat-cache@6.1.19:
+ dependencies:
+ cacheable: 2.3.0
+ flatted: 3.3.3
+ hookified: 1.14.0
+
+ flatted@3.3.3: {}
+
+ follow-redirects@1.15.11: {}
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ global-modules@2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+
+ global-prefix@3.0.0:
+ dependencies:
+ ini: 1.3.8
+ kind-of: 6.0.3
+ which: 1.3.1
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
+
+ globjoin@0.1.4: {}
+
+ globrex@0.1.2: {}
+
+ gopd@1.2.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hashery@1.3.0:
+ dependencies:
+ hookified: 1.14.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hookified@1.14.0: {}
+
+ html-tags@3.3.1: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ immutable@5.1.4: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ ini@1.3.8: {}
+
+ is-arrayish@0.2.1: {}
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ is-plain-object@5.0.0: {}
+
+ isexe@2.0.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json5@2.2.3: {}
+
+ keyv@5.5.5:
+ dependencies:
+ '@keyv/serialize': 1.1.1
+
+ kind-of@6.0.3: {}
+
+ known-css-properties@0.37.0: {}
+
+ lightningcss-android-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-x64@1.30.2:
+ optional: true
+
+ lightningcss-freebsd-x64@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.30.2:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ optional: true
+
+ lightningcss@1.30.2:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.30.2
+ lightningcss-darwin-arm64: 1.30.2
+ lightningcss-darwin-x64: 1.30.2
+ lightningcss-freebsd-x64: 1.30.2
+ lightningcss-linux-arm-gnueabihf: 1.30.2
+ lightningcss-linux-arm64-gnu: 1.30.2
+ lightningcss-linux-arm64-musl: 1.30.2
+ lightningcss-linux-x64-gnu: 1.30.2
+ lightningcss-linux-x64-musl: 1.30.2
+ lightningcss-win32-arm64-msvc: 1.30.2
+ lightningcss-win32-x64-msvc: 1.30.2
+
+ lines-and-columns@1.2.4: {}
+
+ lodash.truncate@4.4.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ math-intrinsics@1.1.0: {}
+
+ mathml-tag-names@2.1.3: {}
+
+ mdn-data@2.12.2: {}
+
+ mdn-data@2.25.0: {}
+
+ meow@13.2.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ node-addon-api@7.1.1:
+ optional: true
+
+ node-releases@2.0.27: {}
+
+ normalize-path@3.0.0: {}
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ path-type@4.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ postcss-media-query-parser@0.2.3: {}
+
+ postcss-resolve-nested-selector@0.1.6: {}
+
+ postcss-safe-parser@7.0.1(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+
+ postcss-scss@4.0.9(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+
+ postcss-selector-parser@7.1.1:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ proxy-from-env@1.1.0: {}
+
+ qified@0.5.3:
+ dependencies:
+ hookified: 1.14.0
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@19.2.1(react@19.2.1):
+ dependencies:
+ react: 19.2.1
+ scheduler: 0.27.0
+
+ react-error-boundary@6.0.0(react@19.2.1):
+ dependencies:
+ '@babel/runtime': 7.28.4
+ react: 19.2.1
+
+ react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.1):
+ dependencies:
+ babel-runtime: 5.8.38
+ react: 19.2.1
+
+ react-icons@5.5.0(react@19.2.1):
+ dependencies:
+ react: 19.2.1
+
+ react-refresh@0.18.0: {}
+
+ react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
+ dependencies:
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ react-router: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+
+ react-router@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.1
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.1(react@19.2.1)
+
+ react@19.2.1: {}
+
+ readdirp@4.1.2: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ reusify@1.1.0: {}
+
+ rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0):
+ dependencies:
+ '@oxc-project/runtime': 0.97.0
+ fdir: 6.5.0(picomatch@4.0.3)
+ lightningcss: 1.30.2
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rolldown: 1.0.0-beta.50
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 24.10.2
+ fsevents: 2.3.3
+ sass: 1.95.0
+
+ rolldown@1.0.0-beta.50:
+ dependencies:
+ '@oxc-project/types': 0.97.0
+ '@rolldown/pluginutils': 1.0.0-beta.50
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-beta.50
+ '@rolldown/binding-darwin-arm64': 1.0.0-beta.50
+ '@rolldown/binding-darwin-x64': 1.0.0-beta.50
+ '@rolldown/binding-freebsd-x64': 1.0.0-beta.50
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50
+ '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50
+ '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50
+ '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50
+ '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ sass@1.95.0:
+ dependencies:
+ chokidar: 4.0.3
+ immutable: 5.1.4
+ source-map-js: 1.2.1
+ optionalDependencies:
+ '@parcel/watcher': 2.5.1
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ set-cookie-parser@2.7.2: {}
+
+ signal-exit@4.1.0: {}
+
+ slash@3.0.0: {}
+
+ slice-ansi@4.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+
+ sonner@2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
+ dependencies:
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+
+ source-map-js@1.2.1: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ stylelint: 16.26.1(typescript@5.9.3)
+
+ stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ postcss-scss: 4.0.9(postcss@8.5.6)
+ stylelint: 16.26.1(typescript@5.9.3)
+ stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3))
+ stylelint-scss: 6.13.0(stylelint@16.26.1(typescript@5.9.3))
+ optionalDependencies:
+ postcss: 8.5.6
+
+ stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ stylelint: 16.26.1(typescript@5.9.3)
+
+ stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ stylelint: 16.26.1(typescript@5.9.3)
+ stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3))
+ stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3))
+ optionalDependencies:
+ postcss: 8.5.6
+
+ stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ stylelint: 16.26.1(typescript@5.9.3)
+ stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3))
+
+ stylelint-scss@6.13.0(stylelint@16.26.1(typescript@5.9.3)):
+ dependencies:
+ css-tree: 3.1.0
+ is-plain-object: 5.0.0
+ known-css-properties: 0.37.0
+ mdn-data: 2.25.0
+ postcss-media-query-parser: 0.2.3
+ postcss-resolve-nested-selector: 0.1.6
+ postcss-selector-parser: 7.1.1
+ postcss-value-parser: 4.2.0
+ stylelint: 16.26.1(typescript@5.9.3)
+
+ stylelint@16.26.1(typescript@5.9.3):
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-syntax-patches-for-csstree': 1.0.20
+ '@csstools/css-tokenizer': 3.0.4
+ '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
+ '@dual-bundle/import-meta-resolve': 4.2.1
+ balanced-match: 2.0.0
+ colord: 2.9.3
+ cosmiconfig: 9.0.0(typescript@5.9.3)
+ css-functions-list: 3.2.3
+ css-tree: 3.1.0
+ debug: 4.4.3
+ fast-glob: 3.3.3
+ fastest-levenshtein: 1.0.16
+ file-entry-cache: 11.1.1
+ global-modules: 2.0.0
+ globby: 11.1.0
+ globjoin: 0.1.4
+ html-tags: 3.3.1
+ ignore: 7.0.5
+ imurmurhash: 0.1.4
+ is-plain-object: 5.0.0
+ known-css-properties: 0.37.0
+ mathml-tag-names: 2.1.3
+ meow: 13.2.0
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-resolve-nested-selector: 0.1.6
+ postcss-safe-parser: 7.0.1(postcss@8.5.6)
+ postcss-selector-parser: 7.1.1
+ postcss-value-parser: 4.2.0
+ resolve-from: 5.0.0
+ string-width: 4.2.3
+ supports-hyperlinks: 3.2.0
+ svg-tags: 1.0.0
+ table: 6.9.0
+ write-file-atomic: 5.0.1
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-hyperlinks@3.2.0:
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.2.0
+
+ svg-tags@1.0.0: {}
+
+ table@6.9.0:
+ dependencies:
+ ajv: 8.17.1
+ lodash.truncate: 4.4.2
+ slice-ansi: 4.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ tsconfck@3.1.6(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@5.9.3: {}
+
+ undici-types@7.16.0: {}
+
+ update-browserslist-db@1.2.2(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ util-deprecate@1.0.2: {}
+
+ vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3):
+ dependencies:
+ debug: 4.4.3
+ globrex: 0.1.2
+ tsconfck: 3.1.6(typescript@5.9.3)
+ optionalDependencies:
+ vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ which@1.3.1:
+ dependencies:
+ isexe: 2.0.0
+
+ write-file-atomic@5.0.1:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 4.1.0
+
+ yallist@3.1.1: {}
+
+ zod@4.1.13: {}
+
+ zustand@5.0.9(@types/react@19.2.7)(react@19.2.1):
+ optionalDependencies:
+ '@types/react': 19.2.7
+ react: 19.2.1
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-192x192.png
new file mode 100644
index 00000000..01d0a08e
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-192x192.png differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-512x512.png b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-512x512.png
new file mode 100644
index 00000000..3ea8cdf1
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/android-chrome-512x512.png differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/apple-touch-icon.png b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/apple-touch-icon.png
new file mode 100644
index 00000000..ec126db9
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/apple-touch-icon.png differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-16x16.png b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-16x16.png
new file mode 100644
index 00000000..75b62905
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-16x16.png differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-32x32.png b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-32x32.png
new file mode 100644
index 00000000..338e90d0
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon-32x32.png differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon.ico b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon.ico
new file mode 100644
index 00000000..cf617467
Binary files /dev/null and b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/favicon.ico differ
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/site.webmanifest b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/site.webmanifest
new file mode 100644
index 00000000..1dd91123
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/public/assets/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/App.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/App.tsx
new file mode 100644
index 00000000..77f8c360
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/App.tsx
@@ -0,0 +1,36 @@
+// ===========================
+// ©AngelaMos | 2025
+// App.tsx
+// ===========================
+
+import { QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import { RouterProvider } from 'react-router-dom'
+import { Toaster } from 'sonner'
+
+import { queryClient } from '@/core/api'
+import { router } from '@/core/app/routers'
+import '@/core/app/toast.module.scss'
+
+export default function App(): React.ReactElement {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/index.ts
new file mode 100644
index 00000000..1bc139b2
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/index.ts
@@ -0,0 +1,8 @@
+// ===================
+// © AngelaMos | 2025
+// index.ts
+// ===================
+
+export * from './useAdmin'
+export * from './useAuth'
+export * from './useUsers'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAdmin.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAdmin.ts
new file mode 100644
index 00000000..b339b0de
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAdmin.ts
@@ -0,0 +1,219 @@
+// ===================
+// © AngelaMos | 2025
+// useAdmin.ts
+// ===================
+
+import {
+ type UseMutationResult,
+ type UseQueryResult,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query'
+import { toast } from 'sonner'
+import {
+ type AdminUserCreateRequest,
+ type AdminUserUpdateRequest,
+ isValidUserListResponse,
+ isValidUserResponse,
+ USER_ERROR_MESSAGES,
+ USER_SUCCESS_MESSAGES,
+ type UserListResponse,
+ type UserResponse,
+ UserResponseError,
+} from '@/api/types'
+import { API_ENDPOINTS, PAGINATION, QUERY_KEYS } from '@/config'
+import { apiClient, QUERY_STRATEGIES } from '@/core/api'
+
+export const adminQueries = {
+ all: () => QUERY_KEYS.ADMIN.ALL,
+ users: {
+ all: () => QUERY_KEYS.ADMIN.USERS.ALL(),
+ list: (page: number, size: number) => QUERY_KEYS.ADMIN.USERS.LIST(page, size),
+ byId: (id: string) => QUERY_KEYS.ADMIN.USERS.BY_ID(id),
+ },
+} as const
+
+interface UseAdminUsersParams {
+ page?: number
+ size?: number
+}
+
+const fetchAdminUsers = async (
+ page: number,
+ size: number
+): Promise => {
+ const response = await apiClient.get(API_ENDPOINTS.ADMIN.USERS.LIST, {
+ params: { page, size },
+ })
+ const data: unknown = response.data
+
+ if (!isValidUserListResponse(data)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_LIST_RESPONSE,
+ API_ENDPOINTS.ADMIN.USERS.LIST
+ )
+ }
+
+ return data
+}
+
+export const useAdminUsers = (
+ params: UseAdminUsersParams = {}
+): UseQueryResult => {
+ const page = params.page ?? PAGINATION.DEFAULT_PAGE
+ const size = params.size ?? PAGINATION.DEFAULT_SIZE
+
+ return useQuery({
+ queryKey: adminQueries.users.list(page, size),
+ queryFn: () => fetchAdminUsers(page, size),
+ ...QUERY_STRATEGIES.standard,
+ })
+}
+
+const fetchAdminUserById = async (id: string): Promise => {
+ const response = await apiClient.get(
+ API_ENDPOINTS.ADMIN.USERS.BY_ID(id)
+ )
+ const data: unknown = response.data
+
+ if (!isValidUserResponse(data)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.ADMIN.USERS.BY_ID(id)
+ )
+ }
+
+ return data
+}
+
+export const useAdminUser = (id: string): UseQueryResult => {
+ return useQuery({
+ queryKey: adminQueries.users.byId(id),
+ queryFn: () => fetchAdminUserById(id),
+ enabled: id.length > 0,
+ ...QUERY_STRATEGIES.standard,
+ })
+}
+
+const performAdminCreateUser = async (
+ data: AdminUserCreateRequest
+): Promise => {
+ const response = await apiClient.post(
+ API_ENDPOINTS.ADMIN.USERS.CREATE,
+ data
+ )
+ const responseData: unknown = response.data
+
+ if (!isValidUserResponse(responseData)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.ADMIN.USERS.CREATE
+ )
+ }
+
+ return responseData
+}
+
+export const useAdminCreateUser = (): UseMutationResult<
+ UserResponse,
+ Error,
+ AdminUserCreateRequest
+> => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: performAdminCreateUser,
+ onSuccess: async (): Promise => {
+ await queryClient.invalidateQueries({ queryKey: adminQueries.users.all() })
+
+ toast.success(USER_SUCCESS_MESSAGES.CREATED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof UserResponseError
+ ? error.message
+ : USER_ERROR_MESSAGES.FAILED_TO_CREATE
+ toast.error(message)
+ },
+ })
+}
+
+interface AdminUpdateUserParams {
+ id: string
+ data: AdminUserUpdateRequest
+}
+
+const performAdminUpdateUser = async (
+ params: AdminUpdateUserParams
+): Promise => {
+ const response = await apiClient.patch(
+ API_ENDPOINTS.ADMIN.USERS.UPDATE(params.id),
+ params.data
+ )
+ const responseData: unknown = response.data
+
+ if (!isValidUserResponse(responseData)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.ADMIN.USERS.UPDATE(params.id)
+ )
+ }
+
+ return responseData
+}
+
+export const useAdminUpdateUser = (): UseMutationResult<
+ UserResponse,
+ Error,
+ AdminUpdateUserParams
+> => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: performAdminUpdateUser,
+ onSuccess: async (
+ data: UserResponse,
+ variables: AdminUpdateUserParams
+ ): Promise => {
+ queryClient.setQueryData(adminQueries.users.byId(variables.id), data)
+
+ await queryClient.invalidateQueries({ queryKey: adminQueries.users.all() })
+
+ toast.success(USER_SUCCESS_MESSAGES.UPDATED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof UserResponseError
+ ? error.message
+ : USER_ERROR_MESSAGES.FAILED_TO_UPDATE
+ toast.error(message)
+ },
+ })
+}
+
+const performAdminDeleteUser = async (id: string): Promise => {
+ await apiClient.delete(API_ENDPOINTS.ADMIN.USERS.DELETE(id))
+}
+
+export const useAdminDeleteUser = (): UseMutationResult => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: performAdminDeleteUser,
+ onSuccess: async (_, id: string): Promise => {
+ queryClient.removeQueries({ queryKey: adminQueries.users.byId(id) })
+
+ await queryClient.invalidateQueries({ queryKey: adminQueries.users.all() })
+
+ toast.success(USER_SUCCESS_MESSAGES.DELETED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof UserResponseError
+ ? error.message
+ : USER_ERROR_MESSAGES.FAILED_TO_DELETE
+ toast.error(message)
+ },
+ })
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAuth.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAuth.ts
new file mode 100644
index 00000000..a0aea655
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useAuth.ts
@@ -0,0 +1,243 @@
+// ===================
+// © AngelaMos | 2025
+// useAuth.ts
+// ===================
+
+import {
+ type UseMutationResult,
+ type UseQueryResult,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query'
+import { toast } from 'sonner'
+import {
+ AUTH_ERROR_MESSAGES,
+ AUTH_SUCCESS_MESSAGES,
+ AuthResponseError,
+ isValidLogoutAllResponse,
+ isValidTokenResponse,
+ isValidTokenWithUserResponse,
+ isValidUserResponse,
+ type LoginRequest,
+ type LogoutAllResponse,
+ type PasswordChangeRequest,
+ type TokenWithUserResponse,
+ type UserResponse,
+} from '@/api/types'
+import { API_ENDPOINTS, QUERY_KEYS, ROUTES } from '@/config'
+import { apiClient, QUERY_STRATEGIES } from '@/core/api'
+import { useAuthStore } from '@/core/lib'
+
+export const authQueries = {
+ all: () => QUERY_KEYS.AUTH.ALL,
+ me: () => QUERY_KEYS.AUTH.ME(),
+} as const
+
+const fetchCurrentUser = async (): Promise => {
+ const response = await apiClient.get(API_ENDPOINTS.AUTH.ME)
+ const data: unknown = response.data
+
+ if (!isValidUserResponse(data)) {
+ throw new AuthResponseError(
+ AUTH_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.AUTH.ME
+ )
+ }
+
+ return data
+}
+
+export const useCurrentUser = (): UseQueryResult => {
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
+
+ return useQuery({
+ queryKey: authQueries.me(),
+ queryFn: fetchCurrentUser,
+ enabled: isAuthenticated,
+ ...QUERY_STRATEGIES.auth,
+ })
+}
+
+const performLogin = async (
+ credentials: LoginRequest
+): Promise => {
+ const formData = new URLSearchParams()
+ formData.append('username', credentials.username)
+ formData.append('password', credentials.password)
+
+ const response = await apiClient.post(
+ API_ENDPOINTS.AUTH.LOGIN,
+ formData,
+ {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ }
+ )
+
+ const data: unknown = response.data
+
+ if (!isValidTokenWithUserResponse(data)) {
+ throw new AuthResponseError(
+ AUTH_ERROR_MESSAGES.INVALID_LOGIN_RESPONSE,
+ API_ENDPOINTS.AUTH.LOGIN
+ )
+ }
+
+ return data
+}
+
+export const useLogin = (): UseMutationResult<
+ TokenWithUserResponse,
+ Error,
+ LoginRequest
+> => {
+ const queryClient = useQueryClient()
+ const login = useAuthStore((s) => s.login)
+
+ return useMutation({
+ mutationFn: performLogin,
+ onSuccess: (data: TokenWithUserResponse): void => {
+ login(data.user, data.access_token)
+
+ queryClient.setQueryData(authQueries.me(), data.user)
+
+ const welcomeMessage = AUTH_SUCCESS_MESSAGES.WELCOME_BACK(
+ data.user.full_name
+ )
+ toast.success(welcomeMessage)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof AuthResponseError ? error.message : 'Login failed'
+ toast.error(message)
+ },
+ })
+}
+
+const performLogout = async (): Promise => {
+ await apiClient.post(API_ENDPOINTS.AUTH.LOGOUT)
+}
+
+export const useLogout = (): UseMutationResult => {
+ const queryClient = useQueryClient()
+ const logout = useAuthStore((s) => s.logout)
+
+ return useMutation({
+ mutationFn: performLogout,
+ onSuccess: (): void => {
+ logout()
+
+ queryClient.removeQueries({ queryKey: authQueries.all() })
+
+ toast.success(AUTH_SUCCESS_MESSAGES.LOGOUT_SUCCESS)
+
+ window.location.href = ROUTES.LOGIN
+ },
+ onError: (): void => {
+ logout()
+ queryClient.removeQueries({ queryKey: authQueries.all() })
+ window.location.href = ROUTES.LOGIN
+ },
+ })
+}
+
+const performLogoutAll = async (): Promise => {
+ const response = await apiClient.post(API_ENDPOINTS.AUTH.LOGOUT_ALL)
+ const data: unknown = response.data
+
+ if (!isValidLogoutAllResponse(data)) {
+ throw new AuthResponseError(
+ AUTH_ERROR_MESSAGES.INVALID_LOGOUT_RESPONSE,
+ API_ENDPOINTS.AUTH.LOGOUT_ALL
+ )
+ }
+
+ return data
+}
+
+export const useLogoutAll = (): UseMutationResult<
+ LogoutAllResponse,
+ Error,
+ void
+> => {
+ const queryClient = useQueryClient()
+ const logout = useAuthStore((s) => s.logout)
+
+ return useMutation({
+ mutationFn: performLogoutAll,
+ onSuccess: (data: LogoutAllResponse): void => {
+ logout()
+
+ queryClient.removeQueries({ queryKey: authQueries.all() })
+
+ toast.success(`Logged out from ${data.revoked_sessions} session(s)`)
+
+ window.location.href = ROUTES.LOGIN
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof AuthResponseError
+ ? error.message
+ : 'Failed to logout all sessions'
+ toast.error(message)
+ },
+ })
+}
+
+const performPasswordChange = async (
+ data: PasswordChangeRequest
+): Promise => {
+ await apiClient.post(API_ENDPOINTS.AUTH.CHANGE_PASSWORD, data)
+}
+
+export const useChangePassword = (): UseMutationResult<
+ void,
+ Error,
+ PasswordChangeRequest
+> => {
+ return useMutation({
+ mutationFn: performPasswordChange,
+ onSuccess: (): void => {
+ toast.success(AUTH_SUCCESS_MESSAGES.PASSWORD_CHANGED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof AuthResponseError
+ ? error.message
+ : 'Failed to change password'
+ toast.error(message)
+ },
+ })
+}
+
+export const useRefreshAuth = (): (() => Promise) => {
+ const queryClient = useQueryClient()
+ const { setAccessToken, login, logout } = useAuthStore()
+
+ return async (): Promise => {
+ try {
+ const response = await apiClient.post(API_ENDPOINTS.AUTH.REFRESH)
+ const data: unknown = response.data
+
+ if (!isValidTokenResponse(data)) {
+ throw new AuthResponseError(
+ AUTH_ERROR_MESSAGES.INVALID_TOKEN_RESPONSE,
+ API_ENDPOINTS.AUTH.REFRESH
+ )
+ }
+
+ setAccessToken(data.access_token)
+
+ const userResponse = await apiClient.get(API_ENDPOINTS.AUTH.ME)
+ const userData: unknown = userResponse.data
+
+ if (isValidUserResponse(userData)) {
+ login(userData, data.access_token)
+ queryClient.setQueryData(authQueries.me(), userData)
+ }
+ } catch {
+ logout()
+ queryClient.removeQueries({ queryKey: authQueries.all() })
+ }
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useUsers.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useUsers.ts
new file mode 100644
index 00000000..ba3a2264
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/hooks/useUsers.ts
@@ -0,0 +1,138 @@
+// ===================
+// © AngelaMos | 2025
+// useUsers.ts
+// ===================
+
+import {
+ type UseMutationResult,
+ type UseQueryResult,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query'
+import { toast } from 'sonner'
+import {
+ isValidUserResponse,
+ USER_ERROR_MESSAGES,
+ USER_SUCCESS_MESSAGES,
+ type UserCreateRequest,
+ type UserResponse,
+ UserResponseError,
+ type UserUpdateRequest,
+} from '@/api/types'
+import { API_ENDPOINTS, QUERY_KEYS } from '@/config'
+import { apiClient, QUERY_STRATEGIES } from '@/core/api'
+import { useAuthStore } from '@/core/lib'
+import { authQueries } from './useAuth'
+
+export const userQueries = {
+ all: () => QUERY_KEYS.USERS.ALL,
+ byId: (id: string) => QUERY_KEYS.USERS.BY_ID(id),
+ me: () => QUERY_KEYS.USERS.ME(),
+} as const
+
+const fetchUserById = async (id: string): Promise => {
+ const response = await apiClient.get(API_ENDPOINTS.USERS.BY_ID(id))
+ const data: unknown = response.data
+
+ if (!isValidUserResponse(data)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.USERS.BY_ID(id)
+ )
+ }
+
+ return data
+}
+
+export const useUser = (id: string): UseQueryResult => {
+ return useQuery({
+ queryKey: userQueries.byId(id),
+ queryFn: () => fetchUserById(id),
+ enabled: id.length > 0,
+ ...QUERY_STRATEGIES.standard,
+ })
+}
+
+const performRegister = async (
+ data: UserCreateRequest
+): Promise => {
+ const response = await apiClient.post(
+ API_ENDPOINTS.USERS.REGISTER,
+ data
+ )
+ const responseData: unknown = response.data
+
+ if (!isValidUserResponse(responseData)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.USERS.REGISTER
+ )
+ }
+
+ return responseData
+}
+
+export const useRegister = (): UseMutationResult<
+ UserResponse,
+ Error,
+ UserCreateRequest
+> => {
+ return useMutation({
+ mutationFn: performRegister,
+ onSuccess: (): void => {
+ toast.success(USER_SUCCESS_MESSAGES.REGISTERED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof UserResponseError
+ ? error.message
+ : USER_ERROR_MESSAGES.FAILED_TO_CREATE
+ toast.error(message)
+ },
+ })
+}
+
+const performUpdateProfile = async (
+ data: UserUpdateRequest
+): Promise => {
+ const response = await apiClient.patch(API_ENDPOINTS.USERS.ME, data)
+ const responseData: unknown = response.data
+
+ if (!isValidUserResponse(responseData)) {
+ throw new UserResponseError(
+ USER_ERROR_MESSAGES.INVALID_USER_RESPONSE,
+ API_ENDPOINTS.USERS.ME
+ )
+ }
+
+ return responseData
+}
+
+export const useUpdateProfile = (): UseMutationResult<
+ UserResponse,
+ Error,
+ UserUpdateRequest
+> => {
+ const queryClient = useQueryClient()
+ const updateUser = useAuthStore((s) => s.updateUser)
+
+ return useMutation({
+ mutationFn: performUpdateProfile,
+ onSuccess: (data: UserResponse): void => {
+ updateUser(data)
+
+ queryClient.setQueryData(authQueries.me(), data)
+ queryClient.setQueryData(userQueries.me(), data)
+
+ toast.success(USER_SUCCESS_MESSAGES.PROFILE_UPDATED)
+ },
+ onError: (error: Error): void => {
+ const message =
+ error instanceof UserResponseError
+ ? error.message
+ : USER_ERROR_MESSAGES.FAILED_TO_UPDATE
+ toast.error(message)
+ },
+ })
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/index.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/index.ts
new file mode 100644
index 00000000..0840a7c1
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/index.ts
@@ -0,0 +1,7 @@
+// ===================
+// © AngelaMos | 2025
+// index.ts
+// ===================
+
+export * from './hooks'
+export * from './types'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/auth.types.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/auth.types.ts
new file mode 100644
index 00000000..bf1ff764
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/auth.types.ts
@@ -0,0 +1,140 @@
+// ===================
+// © AngelaMos | 2025
+// auth.types.ts
+// ===================
+
+import { z } from 'zod'
+import { PASSWORD_CONSTRAINTS } from '@/config'
+
+export const UserRole = {
+ UNKNOWN: 'unknown',
+ USER: 'user',
+ ADMIN: 'admin',
+} as const
+
+export type UserRole = (typeof UserRole)[keyof typeof UserRole]
+
+export const userResponseSchema = z.object({
+ id: z.string().uuid(),
+ created_at: z.string().datetime(),
+ updated_at: z.string().datetime().nullable(),
+ email: z.string().email(),
+ full_name: z.string().nullable(),
+ is_active: z.boolean(),
+ is_verified: z.boolean(),
+ role: z.nativeEnum(UserRole),
+})
+
+export const tokenResponseSchema = z.object({
+ access_token: z.string(),
+ token_type: z.string(),
+})
+
+export const tokenWithUserResponseSchema = tokenResponseSchema.extend({
+ user: userResponseSchema,
+})
+
+export const loginRequestSchema = z.object({
+ username: z.string().email(),
+ password: z
+ .string()
+ .min(PASSWORD_CONSTRAINTS.MIN_LENGTH)
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+})
+
+export const registerRequestSchema = z.object({
+ email: z.string().email(),
+ password: z
+ .string()
+ .min(PASSWORD_CONSTRAINTS.MIN_LENGTH)
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+ full_name: z.string().max(255).optional(),
+})
+
+export const passwordChangeRequestSchema = z.object({
+ current_password: z.string(),
+ new_password: z
+ .string()
+ .min(PASSWORD_CONSTRAINTS.MIN_LENGTH)
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+})
+
+export const logoutAllResponseSchema = z.object({
+ revoked_sessions: z.number(),
+})
+
+export type UserResponse = z.infer
+export type TokenResponse = z.infer
+export type TokenWithUserResponse = z.infer
+export type LoginRequest = z.infer
+export type RegisterRequest = z.infer
+export type PasswordChangeRequest = z.infer
+export type LogoutAllResponse = z.infer
+
+export const isValidUserResponse = (data: unknown): data is UserResponse => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = userResponseSchema.safeParse(data)
+ return result.success
+}
+
+export const isValidTokenResponse = (data: unknown): data is TokenResponse => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = tokenResponseSchema.safeParse(data)
+ return result.success
+}
+
+export const isValidTokenWithUserResponse = (
+ data: unknown
+): data is TokenWithUserResponse => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = tokenWithUserResponseSchema.safeParse(data)
+ return result.success
+}
+
+export const isValidLogoutAllResponse = (
+ data: unknown
+): data is LogoutAllResponse => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = logoutAllResponseSchema.safeParse(data)
+ return result.success
+}
+
+export class AuthResponseError extends Error {
+ readonly endpoint?: string
+
+ constructor(message: string, endpoint?: string) {
+ super(message)
+ this.name = 'AuthResponseError'
+ this.endpoint = endpoint
+ Object.setPrototypeOf(this, AuthResponseError.prototype)
+ }
+}
+
+export const AUTH_ERROR_MESSAGES = {
+ INVALID_USER_RESPONSE: 'Invalid user data from server',
+ INVALID_LOGIN_RESPONSE: 'Invalid login response from server',
+ INVALID_TOKEN_RESPONSE: 'Invalid token response from server',
+ INVALID_LOGOUT_RESPONSE: 'Invalid logout response from server',
+ NO_REFRESH_TOKEN: 'No refresh token available',
+ SESSION_EXPIRED: 'Session expired',
+} as const
+
+export const AUTH_SUCCESS_MESSAGES = {
+ WELCOME_BACK: (name: string | null) =>
+ `Welcome back${name !== null ? `, ${name}` : ''}!`,
+ LOGOUT_SUCCESS: 'Logged out successfully',
+ PASSWORD_CHANGED: 'Password changed successfully',
+ REGISTERED: 'Account created successfully!',
+} as const
+
+export type AuthErrorMessage =
+ (typeof AUTH_ERROR_MESSAGES)[keyof typeof AUTH_ERROR_MESSAGES]
+export type AuthSuccessMessage = typeof AUTH_SUCCESS_MESSAGES
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/index.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/index.ts
new file mode 100644
index 00000000..7f26f3c6
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/index.ts
@@ -0,0 +1,7 @@
+// ===================
+// © AngelaMos | 2025
+// index.ts
+// ===================
+
+export * from './auth.types'
+export * from './user.types'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/user.types.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/user.types.ts
new file mode 100644
index 00000000..2fd36234
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/api/types/user.types.ts
@@ -0,0 +1,127 @@
+// ===================
+// © AngelaMos | 2025
+// user.types.ts
+// ===================
+
+import { z } from 'zod'
+import { PASSWORD_CONSTRAINTS } from '@/config'
+import { UserRole, userResponseSchema } from './auth.types'
+
+export { type UserResponse, UserRole, userResponseSchema } from './auth.types'
+
+export const userListResponseSchema = z.object({
+ items: z.array(userResponseSchema),
+ total: z.number(),
+ page: z.number(),
+ size: z.number(),
+})
+
+export const userCreateRequestSchema = z.object({
+ email: z.string().email(),
+ password: z
+ .string()
+ .min(PASSWORD_CONSTRAINTS.MIN_LENGTH)
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+ full_name: z.string().max(255).nullable().optional(),
+})
+
+export const userUpdateRequestSchema = z.object({
+ full_name: z.string().max(255).nullable().optional(),
+})
+
+export const adminUserCreateRequestSchema = z.object({
+ email: z.string().email(),
+ password: z
+ .string()
+ .min(PASSWORD_CONSTRAINTS.MIN_LENGTH)
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+ full_name: z.string().max(255).nullable().optional(),
+ role: z.nativeEnum(UserRole).optional(),
+ is_active: z.boolean().optional(),
+ is_verified: z.boolean().optional(),
+})
+
+export const adminUserUpdateRequestSchema = z.object({
+ email: z.string().email().optional(),
+ full_name: z.string().max(255).nullable().optional(),
+ role: z.nativeEnum(UserRole).optional(),
+ is_active: z.boolean().optional(),
+ is_verified: z.boolean().optional(),
+})
+
+export const paginationParamsSchema = z.object({
+ page: z.number().min(1),
+ size: z.number().min(1).max(100),
+})
+
+export type UserListResponse = z.infer
+export type UserCreateRequest = z.infer
+export type UserUpdateRequest = z.infer
+export type AdminUserCreateRequest = z.infer
+export type AdminUserUpdateRequest = z.infer
+export type PaginationParams = z.infer
+
+export const isValidUserListResponse = (
+ data: unknown
+): data is UserListResponse => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = userListResponseSchema.safeParse(data)
+ return result.success
+}
+
+export const isValidUserCreateRequest = (
+ data: unknown
+): data is UserCreateRequest => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = userCreateRequestSchema.safeParse(data)
+ return result.success
+}
+
+export const isValidAdminUserCreateRequest = (
+ data: unknown
+): data is AdminUserCreateRequest => {
+ if (data === null || data === undefined) return false
+ if (typeof data !== 'object') return false
+
+ const result = adminUserCreateRequestSchema.safeParse(data)
+ return result.success
+}
+
+export class UserResponseError extends Error {
+ readonly endpoint?: string
+
+ constructor(message: string, endpoint?: string) {
+ super(message)
+ this.name = 'UserResponseError'
+ this.endpoint = endpoint
+ Object.setPrototypeOf(this, UserResponseError.prototype)
+ }
+}
+
+export const USER_ERROR_MESSAGES = {
+ INVALID_USER_RESPONSE: 'Invalid user data from server',
+ INVALID_USER_LIST_RESPONSE: 'Invalid user list from server',
+ USER_NOT_FOUND: 'User not found',
+ EMAIL_ALREADY_EXISTS: 'Email already exists',
+ FAILED_TO_CREATE: 'Failed to create user',
+ FAILED_TO_UPDATE: 'Failed to update user',
+ FAILED_TO_DELETE: 'Failed to delete user',
+} as const
+
+export const USER_SUCCESS_MESSAGES = {
+ CREATED: 'User created successfully',
+ UPDATED: 'User updated successfully',
+ DELETED: 'User deleted successfully',
+ PROFILE_UPDATED: 'Profile updated successfully',
+ REGISTERED:
+ 'Registration successful! Please check your email to verify your account.',
+} as const
+
+export type UserErrorMessage =
+ (typeof USER_ERROR_MESSAGES)[keyof typeof USER_ERROR_MESSAGES]
+export type UserSuccessMessage =
+ (typeof USER_SUCCESS_MESSAGES)[keyof typeof USER_SUCCESS_MESSAGES]
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/components/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/components/index.tsx
new file mode 100644
index 00000000..6b0a7329
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/components/index.tsx
@@ -0,0 +1,4 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/config.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/config.ts
new file mode 100644
index 00000000..8c0d2244
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/config.ts
@@ -0,0 +1,116 @@
+// ===================
+// © AngelaMos | 2026
+// config.ts
+// ===================
+const API_VERSION = 'v1'
+
+export const API_ENDPOINTS = {
+ AUTH: {
+ LOGIN: `/${API_VERSION}/auth/login`,
+ REFRESH: `/${API_VERSION}/auth/refresh`,
+ LOGOUT: `/${API_VERSION}/auth/logout`,
+ LOGOUT_ALL: `/${API_VERSION}/auth/logout-all`,
+ ME: `/${API_VERSION}/auth/me`,
+ CHANGE_PASSWORD: `/${API_VERSION}/auth/change-password`,
+ },
+ USERS: {
+ BASE: `/${API_VERSION}/users`,
+ BY_ID: (id: string) => `/${API_VERSION}/users/${id}`,
+ ME: `/${API_VERSION}/users/me`,
+ REGISTER: `/${API_VERSION}/users`,
+ },
+ ADMIN: {
+ USERS: {
+ LIST: `/${API_VERSION}/admin/users`,
+ CREATE: `/${API_VERSION}/admin/users`,
+ BY_ID: (id: string) => `/${API_VERSION}/admin/users/${id}`,
+ UPDATE: (id: string) => `/${API_VERSION}/admin/users/${id}`,
+ DELETE: (id: string) => `/${API_VERSION}/admin/users/${id}`,
+ },
+ },
+} as const
+
+export const QUERY_KEYS = {
+ AUTH: {
+ ALL: ['auth'] as const,
+ ME: () => [...QUERY_KEYS.AUTH.ALL, 'me'] as const,
+ },
+ USERS: {
+ ALL: ['users'] as const,
+ BY_ID: (id: string) => [...QUERY_KEYS.USERS.ALL, 'detail', id] as const,
+ ME: () => [...QUERY_KEYS.USERS.ALL, 'me'] as const,
+ },
+ ADMIN: {
+ ALL: ['admin'] as const,
+ USERS: {
+ ALL: () => [...QUERY_KEYS.ADMIN.ALL, 'users'] as const,
+ LIST: (page: number, size: number) =>
+ [...QUERY_KEYS.ADMIN.USERS.ALL(), 'list', { page, size }] as const,
+ BY_ID: (id: string) =>
+ [...QUERY_KEYS.ADMIN.USERS.ALL(), 'detail', id] as const,
+ },
+ },
+} as const
+
+export const ROUTES = {
+ HOME: '/',
+ LOGIN: '/login',
+ REGISTER: '/register',
+ DASHBOARD: '/dashboard',
+ SETTINGS: '/settings',
+ UNAUTHORIZED: '/unauthorized',
+ ADMIN: {
+ DASHBOARD: '/admin',
+ USERS: '/admin/users',
+ USER_DETAIL: (id: string) => `/admin/users/${id}`,
+ },
+} as const
+
+export const STORAGE_KEYS = {
+ AUTH: 'auth-storage',
+ UI: 'ui-storage',
+} as const
+
+export const QUERY_CONFIG = {
+ STALE_TIME: {
+ USER: 1000 * 60 * 5,
+ STATIC: Infinity,
+ FREQUENT: 1000 * 30,
+ },
+ GC_TIME: {
+ DEFAULT: 1000 * 60 * 30,
+ LONG: 1000 * 60 * 60,
+ },
+ RETRY: {
+ DEFAULT: 3,
+ NONE: 0,
+ },
+} as const
+
+export const HTTP_STATUS = {
+ OK: 200,
+ CREATED: 201,
+ NO_CONTENT: 204,
+ BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
+ FORBIDDEN: 403,
+ NOT_FOUND: 404,
+ CONFLICT: 409,
+ TOO_MANY_REQUESTS: 429,
+ INTERNAL_SERVER: 500,
+} as const
+
+export const PASSWORD_CONSTRAINTS = {
+ MIN_LENGTH: 8,
+ MAX_LENGTH: 128,
+} as const
+
+export const PAGINATION = {
+ DEFAULT_PAGE: 1,
+ DEFAULT_SIZE: 20,
+ MAX_SIZE: 100,
+} as const
+
+export type ApiEndpoint = typeof API_ENDPOINTS
+export type QueryKey = typeof QUERY_KEYS
+export type Route = typeof ROUTES
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/api.config.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/api.config.ts
new file mode 100644
index 00000000..8347fa00
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/api.config.ts
@@ -0,0 +1,160 @@
+// ===================
+// © AngelaMos | 2025
+// api.config.ts
+// ===================
+
+import axios, {
+ type AxiosError,
+ type AxiosInstance,
+ type InternalAxiosRequestConfig,
+} from 'axios'
+import { API_ENDPOINTS, HTTP_STATUS } from '@/config'
+import { useAuthStore } from '@/core/lib'
+import { ApiError, ApiErrorCode, transformAxiosError } from './errors'
+
+interface RequestConfig extends InternalAxiosRequestConfig {
+ _retry?: boolean
+}
+
+interface RefreshSubscriber {
+ resolve: (token: string) => void
+ reject: (error: Error) => void
+}
+
+const getBaseURL = (): string => {
+ return import.meta.env.VITE_API_URL ?? '/api'
+}
+
+export const apiClient: AxiosInstance = axios.create({
+ baseURL: getBaseURL(),
+ timeout: 15000,
+ headers: { 'Content-Type': 'application/json' },
+ withCredentials: true,
+})
+
+let isRefreshing = false
+let refreshSubscribers: RefreshSubscriber[] = []
+
+const processRefreshQueue = (error: Error | null, token: string | null): void => {
+ refreshSubscribers.forEach((subscriber) => {
+ if (error !== null) {
+ subscriber.reject(error)
+ } else if (token !== null) {
+ subscriber.resolve(token)
+ }
+ })
+ refreshSubscribers = []
+}
+
+const addRefreshSubscriber = (
+ resolve: (token: string) => void,
+ reject: (error: Error) => void
+): void => {
+ refreshSubscribers.push({ resolve, reject })
+}
+
+const handleTokenRefresh = async (): Promise => {
+ const response = await apiClient.post<{ access_token: string }>(
+ API_ENDPOINTS.AUTH.REFRESH
+ )
+
+ if (
+ response.data === null ||
+ response.data === undefined ||
+ typeof response.data !== 'object'
+ ) {
+ throw new ApiError(
+ 'Invalid refresh response',
+ ApiErrorCode.AUTHENTICATION_ERROR,
+ HTTP_STATUS.UNAUTHORIZED
+ )
+ }
+
+ const accessToken = response.data.access_token
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
+ throw new ApiError(
+ 'Invalid access token',
+ ApiErrorCode.AUTHENTICATION_ERROR,
+ HTTP_STATUS.UNAUTHORIZED
+ )
+ }
+
+ return accessToken
+}
+
+const handleAuthFailure = (): void => {
+ useAuthStore.getState().logout()
+ window.location.href = '/login'
+}
+
+apiClient.interceptors.request.use(
+ (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
+ const token = useAuthStore.getState().accessToken
+ if (token !== null && token.length > 0) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error: unknown): Promise => {
+ return Promise.reject(error)
+ }
+)
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ async (error: AxiosError): Promise => {
+ const originalRequest = error.config as RequestConfig | undefined
+
+ if (originalRequest === undefined) {
+ return Promise.reject(transformAxiosError(error))
+ }
+
+ const isUnauthorized = error.response?.status === HTTP_STATUS.UNAUTHORIZED
+ const isNotRetried = originalRequest._retry !== true
+ const isNotRefreshEndpoint =
+ originalRequest.url?.includes(API_ENDPOINTS.AUTH.REFRESH) !== true
+
+ if (isUnauthorized && isNotRetried && isNotRefreshEndpoint) {
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ addRefreshSubscriber(
+ (newToken: string): void => {
+ originalRequest.headers.Authorization = `Bearer ${newToken}`
+ resolve(apiClient(originalRequest))
+ },
+ (refreshError: Error): void => {
+ reject(refreshError)
+ }
+ )
+ })
+ }
+
+ originalRequest._retry = true
+ isRefreshing = true
+
+ try {
+ const newToken = await handleTokenRefresh()
+ useAuthStore.getState().setAccessToken(newToken)
+ processRefreshQueue(null, newToken)
+ originalRequest.headers.Authorization = `Bearer ${newToken}`
+ return await apiClient(originalRequest)
+ } catch (refreshError: unknown) {
+ const apiError =
+ refreshError instanceof ApiError
+ ? refreshError
+ : new ApiError(
+ 'Session expired',
+ ApiErrorCode.AUTHENTICATION_ERROR,
+ HTTP_STATUS.UNAUTHORIZED
+ )
+ processRefreshQueue(apiError, null)
+ handleAuthFailure()
+ return Promise.reject(apiError)
+ } finally {
+ isRefreshing = false
+ }
+ }
+
+ return Promise.reject(transformAxiosError(error))
+ }
+)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/errors.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/errors.ts
new file mode 100644
index 00000000..fde5ec65
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/errors.ts
@@ -0,0 +1,114 @@
+/**
+ * ©AngelaMos | 2025
+ * errors.ts
+ */
+
+import type { AxiosError } from 'axios'
+
+export const ApiErrorCode = {
+ NETWORK_ERROR: 'NETWORK_ERROR',
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
+ AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
+ AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
+ NOT_FOUND: 'NOT_FOUND',
+ CONFLICT: 'CONFLICT',
+ RATE_LIMITED: 'RATE_LIMITED',
+ SERVER_ERROR: 'SERVER_ERROR',
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
+} as const
+
+export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode]
+
+export class ApiError extends Error {
+ readonly code: ApiErrorCode
+ readonly statusCode: number
+ readonly details?: Record
+
+ constructor(
+ message: string,
+ code: ApiErrorCode,
+ statusCode: number,
+ details?: Record
+ ) {
+ super(message)
+ this.name = 'ApiError'
+ this.code = code
+ this.statusCode = statusCode
+ this.details = details
+ }
+
+ getUserMessage(): string {
+ const messages: Record = {
+ [ApiErrorCode.NETWORK_ERROR]:
+ 'Unable to connect. Please check your internet connection.',
+ [ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.',
+ [ApiErrorCode.AUTHENTICATION_ERROR]:
+ 'Your session has expired. Please log in again.',
+ [ApiErrorCode.AUTHORIZATION_ERROR]:
+ 'You do not have permission to perform this action.',
+ [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.',
+ [ApiErrorCode.CONFLICT]:
+ 'This operation conflicts with an existing resource.',
+ [ApiErrorCode.RATE_LIMITED]:
+ 'Too many requests. Please wait a moment and try again.',
+ [ApiErrorCode.SERVER_ERROR]:
+ 'Something went wrong on our end. Please try again later.',
+ [ApiErrorCode.UNKNOWN_ERROR]:
+ 'An unexpected error occurred. Please try again.',
+ }
+ return messages[this.code]
+ }
+}
+
+interface ApiErrorResponse {
+ detail?: string | { msg: string; type: string }[]
+ message?: string
+}
+
+export function transformAxiosError(error: AxiosError): ApiError {
+ if (!error.response) {
+ return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0)
+ }
+
+ const { status } = error.response
+ const data = error.response.data as ApiErrorResponse | undefined
+ let message = 'An error occurred'
+ let details: Record | undefined
+
+ if (data?.detail) {
+ if (typeof data.detail === 'string') {
+ message = data.detail
+ } else if (Array.isArray(data.detail)) {
+ details = { validation: [] }
+ data.detail.forEach((err) => {
+ details?.validation.push(err.msg)
+ })
+ message = 'Validation error'
+ }
+ } else if (data?.message) {
+ message = data.message
+ }
+
+ const codeMap: Record = {
+ 400: ApiErrorCode.VALIDATION_ERROR,
+ 401: ApiErrorCode.AUTHENTICATION_ERROR,
+ 403: ApiErrorCode.AUTHORIZATION_ERROR,
+ 404: ApiErrorCode.NOT_FOUND,
+ 409: ApiErrorCode.CONFLICT,
+ 429: ApiErrorCode.RATE_LIMITED,
+ 500: ApiErrorCode.SERVER_ERROR,
+ 502: ApiErrorCode.SERVER_ERROR,
+ 503: ApiErrorCode.SERVER_ERROR,
+ 504: ApiErrorCode.SERVER_ERROR,
+ }
+
+ const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR
+
+ return new ApiError(message, code, status, details)
+}
+
+declare module '@tanstack/react-query' {
+ interface Register {
+ defaultError: ApiError
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/index.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/index.ts
new file mode 100644
index 00000000..1818cd7e
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/index.ts
@@ -0,0 +1,8 @@
+// ===================
+// © AngelaMos | 2025
+// index.ts
+// ===================
+
+export * from './api.config'
+export * from './errors'
+export * from './query.config'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/query.config.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/query.config.ts
new file mode 100644
index 00000000..8244b22d
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/api/query.config.ts
@@ -0,0 +1,105 @@
+// ===================
+// © AngelaMos | 2025
+// query.config.ts
+// ===================
+
+import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+import { QUERY_CONFIG } from '@/config'
+import { ApiError, ApiErrorCode } from './errors'
+
+const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [
+ ApiErrorCode.AUTHENTICATION_ERROR,
+ ApiErrorCode.AUTHORIZATION_ERROR,
+ ApiErrorCode.NOT_FOUND,
+ ApiErrorCode.VALIDATION_ERROR,
+] as const
+
+const shouldRetryQuery = (failureCount: number, error: Error): boolean => {
+ if (error instanceof ApiError) {
+ if (NO_RETRY_ERROR_CODES.includes(error.code)) {
+ return false
+ }
+ }
+ return failureCount < QUERY_CONFIG.RETRY.DEFAULT
+}
+
+const calculateRetryDelay = (attemptIndex: number): number => {
+ const baseDelay = 1000
+ const maxDelay = 30000
+ return Math.min(baseDelay * 2 ** attemptIndex, maxDelay)
+}
+
+const handleQueryCacheError = (
+ error: Error,
+ query: { state: { data: unknown } }
+): void => {
+ if (query.state.data !== undefined) {
+ const message =
+ error instanceof ApiError
+ ? error.getUserMessage()
+ : 'Background update failed'
+ toast.error(message)
+ }
+}
+
+const handleMutationCacheError = (
+ error: Error,
+ _variables: unknown,
+ _context: unknown,
+ mutation: { options: { onError?: unknown } }
+): void => {
+ if (mutation.options.onError === undefined) {
+ const message =
+ error instanceof ApiError ? error.getUserMessage() : 'Operation failed'
+ toast.error(message)
+ }
+}
+
+export const QUERY_STRATEGIES = {
+ standard: {
+ staleTime: QUERY_CONFIG.STALE_TIME.USER,
+ gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
+ },
+ frequent: {
+ staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT,
+ gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
+ refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT,
+ },
+ static: {
+ staleTime: QUERY_CONFIG.STALE_TIME.STATIC,
+ gcTime: QUERY_CONFIG.GC_TIME.LONG,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ },
+ auth: {
+ staleTime: QUERY_CONFIG.STALE_TIME.USER,
+ gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
+ retry: QUERY_CONFIG.RETRY.NONE,
+ },
+} as const
+
+export type QueryStrategy = keyof typeof QUERY_STRATEGIES
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: QUERY_CONFIG.STALE_TIME.USER,
+ gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
+ retry: shouldRetryQuery,
+ retryDelay: calculateRetryDelay,
+ refetchOnWindowFocus: true,
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ },
+ mutations: {
+ retry: QUERY_CONFIG.RETRY.NONE,
+ },
+ },
+ queryCache: new QueryCache({
+ onError: handleQueryCacheError,
+ }),
+ mutationCache: new MutationCache({
+ onError: handleMutationCacheError,
+ }),
+})
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/protected-route.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/protected-route.tsx
new file mode 100644
index 00000000..486eec78
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/protected-route.tsx
@@ -0,0 +1,47 @@
+// ===================
+// © AngelaMos | 2025
+// protected-route.tsx
+// ===================
+
+import { Navigate, Outlet, useLocation } from 'react-router-dom'
+import type { UserRole } from '@/api/types'
+import { ROUTES } from '@/config'
+import { useAuthStore } from '@/core/lib'
+
+interface ProtectedRouteProps {
+ allowedRoles?: UserRole[]
+ redirectTo?: string
+}
+
+export function ProtectedRoute({
+ allowedRoles,
+ redirectTo = ROUTES.LOGIN,
+}: ProtectedRouteProps): React.ReactElement {
+ const location = useLocation()
+ const { isAuthenticated, isLoading, user } = useAuthStore()
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+ )
+ }
+
+ if (
+ allowedRoles !== undefined &&
+ allowedRoles.length > 0 &&
+ user !== null &&
+ !allowedRoles.includes(user.role)
+ ) {
+ return
+ }
+
+ return
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/routers.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/routers.tsx
new file mode 100644
index 00000000..cb2473c9
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/routers.tsx
@@ -0,0 +1,67 @@
+// ===================
+// © AngelaMos | 2025
+// routers.tsx
+// ===================
+
+import { createBrowserRouter, type RouteObject } from 'react-router-dom'
+import { UserRole } from '@/api/types'
+import { ROUTES } from '@/config'
+import { ProtectedRoute } from './protected-route'
+import { Shell } from './shell'
+
+const routes: RouteObject[] = [
+ {
+ path: ROUTES.HOME,
+ lazy: () => import('@/pages/landing'),
+ },
+ {
+ path: ROUTES.LOGIN,
+ lazy: () => import('@/pages/login'),
+ },
+ {
+ path: ROUTES.REGISTER,
+ lazy: () => import('@/pages/register'),
+ },
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ path: ROUTES.DASHBOARD,
+ lazy: () => import('@/pages/dashboard'),
+ },
+ {
+ path: ROUTES.SETTINGS,
+ lazy: () => import('@/pages/settings'),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ path: ROUTES.ADMIN.USERS,
+ lazy: () => import('@/pages/admin'),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ path: ROUTES.UNAUTHORIZED,
+ lazy: () => import('@/pages/landing'),
+ },
+ {
+ path: '*',
+ lazy: () => import('@/pages/landing'),
+ },
+]
+
+export const router = createBrowserRouter(routes)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.module.scss
new file mode 100644
index 00000000..b57d7a96
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.module.scss
@@ -0,0 +1,331 @@
+// ===================
+// © AngelaMos | 2025
+// shell.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+$sidebar-width: 240px;
+$sidebar-collapsed-width: 64px;
+$header-height: 56px;
+
+.shell {
+ display: flex;
+ min-height: 100vh;
+ min-height: 100dvh;
+}
+
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: $sidebar-width;
+ background: $bg-surface-100;
+ border-right: 1px solid $border-default;
+ display: flex;
+ flex-direction: column;
+ z-index: $z-fixed;
+ @include transition-fast;
+
+ &.collapsed {
+ width: $sidebar-collapsed-width;
+ }
+
+ @include breakpoint-down('sm') {
+ transform: translateX(-100%);
+
+ &.open {
+ transform: translateX(0);
+ }
+
+ &.collapsed {
+ width: $sidebar-width;
+ }
+ }
+}
+
+.sidebarHeader {
+ height: $header-height;
+ padding: 0 $space-3;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid $border-default;
+
+ .sidebar.collapsed & {
+ justify-content: center;
+ padding: 0;
+ }
+}
+
+.logo {
+ font-size: $font-size-base;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+ @include transition-fast;
+
+ .sidebar.collapsed & {
+ display: none;
+ }
+}
+
+.nav {
+ flex: 1;
+ padding: $space-3;
+ display: flex;
+ flex-direction: column;
+ gap: $space-1;
+}
+
+.navItem {
+ display: flex;
+ align-items: center;
+ gap: $space-3;
+ padding: $space-2 $space-3;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ color: $text-light;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ &.active {
+ background: $bg-selection;
+ color: $text-default;
+ }
+
+ .sidebar.collapsed & {
+ justify-content: center;
+ }
+}
+
+.navIcon {
+ width: 17px;
+ height: 17px;
+ flex-shrink: 0;
+}
+
+.navLabel {
+ @include transition-fast;
+
+ .sidebar.collapsed & {
+ display: none;
+ }
+}
+
+.adminItem {
+ margin-top: auto;
+ border-top: 1px solid $border-default;
+ padding-top: $space-3;
+}
+
+.collapseBtn {
+ width: 45px;
+ height: 45px;
+ border-radius: $radius-md;
+ color: $text-light;
+ @include flex-center;
+ @include transition-fast;
+
+ svg {
+ width: 23.5px;
+ height: 23.5px;
+ }
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ @include breakpoint-down('sm') {
+ display: none;
+ }
+}
+
+.sidebarFooter {
+ padding: $space-3;
+ border-top: 1px solid $border-default;
+}
+
+.logoutBtn {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: $space-3;
+ padding: $space-3;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ color: $text-default;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ }
+
+ .sidebar.collapsed & {
+ justify-content: center;
+
+ .logoutText {
+ display: none;
+ }
+ }
+}
+
+.logoutIcon {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+}
+
+.logoutText {
+ font-weight: $font-weight-medium;
+ @include transition-fast;
+}
+
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: rgb(0, 0, 0, 50%);
+ z-index: calc($z-fixed - 1);
+ display: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+
+ @include breakpoint-down('sm') {
+ display: block;
+ }
+}
+
+.main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-left: $sidebar-width;
+ min-width: 0;
+ @include transition-fast;
+
+ &.collapsed {
+ margin-left: $sidebar-collapsed-width;
+ }
+
+ @include breakpoint-down('sm') {
+ margin-left: 0;
+
+ &.collapsed {
+ margin-left: 0;
+ }
+ }
+}
+
+.header {
+ position: sticky;
+ top: 0;
+ height: $header-height;
+ background: $bg-surface-100;
+ border-bottom: 1px solid $border-default;
+ z-index: $z-sticky;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 $space-4;
+}
+
+.headerLeft {
+ display: flex;
+ align-items: center;
+ gap: $space-3;
+}
+
+.menuBtn {
+ display: none;
+ width: 36px;
+ height: 36px;
+ border-radius: $radius-md;
+ color: $text-light;
+ align-items: center;
+ justify-content: center;
+ @include transition-fast;
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ @media (width <= 479px) {
+ display: flex;
+ }
+}
+
+.pageTitle {
+ font-size: $font-size-base;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+ margin-left: 7px;
+}
+
+.headerRight {
+ display: flex;
+ align-items: center;
+ gap: $space-3;
+}
+
+.avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: $radius-full;
+ background: $bg-surface-300;
+ color: $text-light;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ @include flex-center;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(1.2);
+ }
+}
+
+.content {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.loading {
+ @include flex-center;
+ height: 100%;
+ color: $text-muted;
+}
+
+.error {
+ @include flex-column-center;
+ height: 100%;
+ gap: $space-4;
+ padding: $space-6;
+ color: $error-default;
+
+ h2 {
+ font-size: $font-size-xl;
+ font-weight: $font-weight-semibold;
+ }
+
+ pre {
+ font-family: $font-mono;
+ font-size: $font-size-sm;
+ padding: $space-4;
+ background: $bg-surface-200;
+ border-radius: $radius-lg;
+ overflow-x: auto;
+ max-width: 100%;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.tsx
new file mode 100644
index 00000000..65fe7f37
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/shell.tsx
@@ -0,0 +1,160 @@
+/**
+ * ©AngelaMos | 2025
+ * shell.tsx
+ */
+
+import { Suspense } from 'react'
+import { ErrorBoundary } from 'react-error-boundary'
+import { GiCardAceClubs, GiCardJoker, GiExitDoor } from 'react-icons/gi'
+import { LuChevronLeft, LuChevronRight, LuMenu, LuShield } from 'react-icons/lu'
+import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
+import { useLogout } from '@/api/hooks'
+import { ROUTES } from '@/config'
+import { useIsAdmin, useUIStore, useUser } from '@/core/lib'
+import styles from './shell.module.scss'
+
+const NAV_ITEMS = [
+ { path: ROUTES.DASHBOARD, label: 'Dashboard', icon: GiCardJoker },
+ { path: ROUTES.SETTINGS, label: 'Settings', icon: GiCardAceClubs },
+]
+
+const ADMIN_NAV_ITEM = {
+ path: ROUTES.ADMIN.USERS,
+ label: 'Admin',
+ icon: LuShield,
+}
+
+function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
+ return (
+
+
Something went wrong
+
{error.message}
+
+ )
+}
+
+function ShellLoading(): React.ReactElement {
+ return Loading...
+}
+
+function getPageTitle(pathname: string, isAdmin: boolean): string {
+ if (isAdmin && pathname === ADMIN_NAV_ITEM.path) {
+ return ADMIN_NAV_ITEM.label
+ }
+ const item = NAV_ITEMS.find((i) => i.path === pathname)
+ return item?.label ?? 'Dashboard'
+}
+
+export function Shell(): React.ReactElement {
+ const location = useLocation()
+ const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } =
+ useUIStore()
+ const { mutate: logout } = useLogout()
+ const isAdmin = useIsAdmin()
+ const user = useUser()
+
+ const pageTitle = getPageTitle(location.pathname, isAdmin)
+ const avatarLetter =
+ user?.full_name?.[0]?.toUpperCase() ?? user?.email?.[0]?.toUpperCase() ?? 'U'
+
+ return (
+
+
+
+ {sidebarOpen && (
+
+ )
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/toast.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/toast.module.scss
new file mode 100644
index 00000000..d50ab7fb
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/app/toast.module.scss
@@ -0,0 +1,67 @@
+// ===================
+// © AngelaMos | 2025
+// toast.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+:global {
+ [data-sonner-toaster] {
+ --normal-bg: #{$bg-surface-100};
+ --normal-border: #{$border-default};
+ --normal-text: #{$text-default};
+
+ --success-bg: #{$bg-surface-100};
+ --success-border: #{$border-default};
+ --success-text: #{$text-default};
+
+ --error-bg: #{$bg-surface-100};
+ --error-border: #{$error-default};
+ --error-text: #{$text-default};
+
+ --warning-bg: #{$bg-surface-100};
+ --warning-border: #{$border-default};
+ --warning-text: #{$text-default};
+
+ --info-bg: #{$bg-surface-100};
+ --info-border: #{$border-default};
+ --info-text: #{$text-default};
+
+ font-family: $font-sans;
+ }
+
+ [data-sonner-toast] {
+ border-radius: $radius-md;
+ padding: $space-3 $space-4;
+ font-size: $font-size-sm;
+ border: 1px solid $border-default;
+ background: $bg-surface-100;
+ color: $text-default;
+
+ [data-title] {
+ font-weight: $font-weight-medium;
+ }
+
+ [data-description] {
+ color: $text-light;
+ font-size: $font-size-xs;
+ }
+
+ [data-close-button] {
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ color: $text-muted;
+ @include transition-fast;
+
+ @include hover {
+ color: $text-default;
+ }
+ }
+ }
+
+ [data-sonner-toast][data-type='error'] {
+ border-color: $error-default;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.form.store.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.form.store.ts
new file mode 100644
index 00000000..22c422c5
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.form.store.ts
@@ -0,0 +1,43 @@
+/**
+ * ©AngelaMos | 2025
+ * auth.form.store.ts
+ */
+
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+
+interface AuthFormState {
+ loginEmail: string
+ registerEmail: string
+ setLoginEmail: (email: string) => void
+ setRegisterEmail: (email: string) => void
+ clearLoginForm: () => void
+ clearRegisterForm: () => void
+}
+
+export const useAuthFormStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ loginEmail: '',
+ registerEmail: '',
+
+ setLoginEmail: (email) =>
+ set({ loginEmail: email }, false, 'authForm/setLoginEmail'),
+
+ setRegisterEmail: (email) =>
+ set({ registerEmail: email }, false, 'authForm/setRegisterEmail'),
+
+ clearLoginForm: () =>
+ set({ loginEmail: '' }, false, 'authForm/clearLoginForm'),
+
+ clearRegisterForm: () =>
+ set({ registerEmail: '' }, false, 'authForm/clearRegisterForm'),
+ }),
+ {
+ name: 'auth-form-storage',
+ }
+ ),
+ { name: 'AuthFormStore' }
+ )
+)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.store.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.store.ts
new file mode 100644
index 00000000..a02cd0c9
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/auth.store.ts
@@ -0,0 +1,105 @@
+// ===================
+// © AngelaMos | 2025
+// auth.store.ts
+// ===================
+
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+import { type UserResponse, UserRole } from '@/api/types'
+import { STORAGE_KEYS } from '@/config'
+
+interface AuthState {
+ user: UserResponse | null
+ accessToken: string | null
+ isAuthenticated: boolean
+ isLoading: boolean
+}
+
+interface AuthActions {
+ login: (user: UserResponse, accessToken: string) => void
+ logout: () => void
+ setLoading: (loading: boolean) => void
+ setAccessToken: (token: string | null) => void
+ updateUser: (updates: Partial) => void
+}
+
+type AuthStore = AuthState & AuthActions
+
+export const useAuthStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ user: null,
+ accessToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+
+ login: (user, accessToken) =>
+ set(
+ {
+ user,
+ accessToken,
+ isAuthenticated: true,
+ isLoading: false,
+ },
+ false,
+ 'auth/login'
+ ),
+
+ logout: () =>
+ set(
+ {
+ user: null,
+ accessToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+ },
+ false,
+ 'auth/logout'
+ ),
+
+ setLoading: (loading) =>
+ set({ isLoading: loading }, false, 'auth/setLoading'),
+
+ setAccessToken: (token) =>
+ set({ accessToken: token }, false, 'auth/setAccessToken'),
+
+ updateUser: (updates) =>
+ set(
+ (state) => ({
+ user: state.user !== null ? { ...state.user, ...updates } : null,
+ }),
+ false,
+ 'auth/updateUser'
+ ),
+ }),
+ {
+ name: STORAGE_KEYS.AUTH,
+ partialize: (state) => ({
+ user: state.user,
+ isAuthenticated: state.isAuthenticated,
+ }),
+ }
+ ),
+ { name: 'AuthStore' }
+ )
+)
+
+export const useUser = (): UserResponse | null => useAuthStore((s) => s.user)
+export const useIsAuthenticated = (): boolean =>
+ useAuthStore((s) => s.isAuthenticated)
+export const useIsAuthLoading = (): boolean => useAuthStore((s) => s.isLoading)
+export const useAccessToken = (): string | null =>
+ useAuthStore((s) => s.accessToken)
+
+export const useHasRole = (role: UserRole): boolean => {
+ const user = useAuthStore((s) => s.user)
+ return user !== null && user.role === role
+}
+
+export const useIsAdmin = (): boolean => {
+ const user = useAuthStore((s) => s.user)
+ return user !== null && user.role === UserRole.ADMIN
+}
+
+export { UserRole }
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/index.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/index.ts
new file mode 100644
index 00000000..191757f4
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/index.ts
@@ -0,0 +1,8 @@
+// ===================
+// © AngelaMos | 2025
+// index.ts
+// ===================
+
+export * from './auth.form.store'
+export * from './auth.store'
+export * from './shell.ui.store'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/shell.ui.store.ts
new file mode 100644
index 00000000..d601a53b
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/core/lib/shell.ui.store.ts
@@ -0,0 +1,63 @@
+/**
+ * ©AngelaMos | 2025
+ * ui.store.ts
+ */
+
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+
+type Theme = 'light' | 'dark' | 'system'
+
+interface UIState {
+ theme: Theme
+ sidebarOpen: boolean
+ sidebarCollapsed: boolean
+ setTheme: (theme: Theme) => void
+ toggleSidebar: () => void
+ setSidebarOpen: (open: boolean) => void
+ toggleSidebarCollapsed: () => void
+}
+
+export const useUIStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ theme: 'dark',
+ sidebarOpen: false,
+ sidebarCollapsed: false,
+
+ setTheme: (theme) => set({ theme }, false, 'ui/setTheme'),
+
+ toggleSidebar: () =>
+ set(
+ (state) => ({ sidebarOpen: !state.sidebarOpen }),
+ false,
+ 'ui/toggleSidebar'
+ ),
+
+ setSidebarOpen: (open) =>
+ set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'),
+
+ toggleSidebarCollapsed: () =>
+ set(
+ (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }),
+ false,
+ 'ui/toggleSidebarCollapsed'
+ ),
+ }),
+ {
+ name: 'ui-storage',
+ partialize: (state) => ({
+ theme: state.theme,
+ sidebarCollapsed: state.sidebarCollapsed,
+ }),
+ }
+ ),
+ { name: 'UIStore' }
+ )
+)
+
+export const useTheme = (): Theme => useUIStore((s) => s.theme)
+export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen)
+export const useSidebarCollapsed = (): boolean =>
+ useUIStore((s) => s.sidebarCollapsed)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/main.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/main.tsx
new file mode 100644
index 00000000..1b1f2c8a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/main.tsx
@@ -0,0 +1,15 @@
+// ===========================
+// ©AngelaMos | 2025
+// main.tsx
+// ===========================
+
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import App from './App'
+import './styles.scss'
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+)
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/admin.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/admin.module.scss
new file mode 100644
index 00000000..ef515000
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/admin.module.scss
@@ -0,0 +1,468 @@
+// ===================
+// © AngelaMos | 2025
+// admin.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ padding: $space-6;
+ min-height: calc(100vh - 56px);
+ background-color: $bg-default;
+}
+
+.header {
+ @include flex-between;
+ margin-bottom: $space-6;
+}
+
+.title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+}
+
+.createBtn {
+ display: flex;
+ align-items: center;
+ gap: $space-2;
+ padding: $space-2 $space-4;
+ background-color: $white;
+ border: none;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $black;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(0.9);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+}
+
+.table {
+ width: 100%;
+ background: $bg-surface-100;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+ overflow: hidden;
+}
+
+.tableHeader {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 100px;
+ gap: $space-4;
+ padding: $space-3 $space-4;
+ background: $bg-surface-200;
+ border-bottom: 1px solid $border-default;
+
+ @include breakpoint-down('md') {
+ display: none;
+ }
+}
+
+.tableHeaderCell {
+ font-size: $font-size-xs;
+ font-weight: $font-weight-medium;
+ color: $text-lighter;
+ text-transform: uppercase;
+ letter-spacing: $tracking-wide;
+}
+
+.tableBody {
+ @include flex-column;
+}
+
+.tableRow {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 100px;
+ gap: $space-4;
+ padding: $space-3 $space-4;
+ border-bottom: 1px solid $border-default;
+ @include transition-fast;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ @include hover {
+ background: $bg-surface-75;
+ }
+
+ @include breakpoint-down('md') {
+ grid-template-columns: 1fr;
+ gap: $space-2;
+ }
+}
+
+.tableCell {
+ display: flex;
+ align-items: center;
+ font-size: $font-size-sm;
+ color: $text-default;
+ min-width: 0;
+
+ @include breakpoint-down('md') {
+ &::before {
+ content: attr(data-label);
+ font-size: $font-size-xs;
+ color: $text-lighter;
+ margin-right: $space-2;
+ min-width: 80px;
+ }
+ }
+}
+
+.email {
+ @include truncate;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: $space-1 $space-2;
+ border-radius: $radius-full;
+ font-size: $font-size-xs;
+ font-weight: $font-weight-medium;
+
+ &.admin {
+ background: $bg-selection;
+ color: $text-default;
+ }
+
+ &.user {
+ background: $bg-surface-200;
+ color: $text-light;
+ }
+
+ &.active {
+ background: hsl(142 76% 36% / 20%);
+ color: hsl(142, 76%, 46%);
+ }
+
+ &.inactive {
+ background: hsl(0 72% 51% / 20%);
+ color: $error-light;
+ }
+}
+
+.actions {
+ display: flex;
+ gap: $space-2;
+ justify-content: flex-end;
+
+ @include breakpoint-down('md') {
+ justify-content: flex-start;
+ }
+}
+
+.actionBtn {
+ width: 32px;
+ height: 32px;
+ @include flex-center;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ background: transparent;
+ color: $text-light;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ &.delete {
+ @include hover {
+ border-color: $error-default;
+ color: $error-default;
+ }
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+}
+
+.pagination {
+ @include flex-between;
+ padding: $space-4;
+ border-top: 1px solid $border-default;
+}
+
+.paginationInfo {
+ font-size: $font-size-sm;
+ color: $text-lighter;
+}
+
+.paginationBtns {
+ display: flex;
+ gap: $space-2;
+}
+
+.paginationBtn {
+ padding: $space-2 $space-3;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ background: transparent;
+ font-size: $font-size-sm;
+ color: $text-light;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.empty {
+ @include flex-column-center;
+ padding: $space-12;
+ color: $text-muted;
+ font-size: $font-size-sm;
+}
+
+.loading {
+ @include flex-center;
+ padding: $space-12;
+ color: $text-muted;
+}
+
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: $z-modal;
+ @include flex-center;
+}
+
+.modalOverlay {
+ @include absolute-fill;
+ background: rgb(0, 0, 0, 70%);
+}
+
+.modalContent {
+ position: relative;
+ width: 100%;
+ max-width: 400px;
+ margin: $space-4;
+ background: $bg-surface-100;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+ padding: $space-6;
+}
+
+.modalHeader {
+ @include flex-between;
+ margin-bottom: $space-5;
+}
+
+.modalTitle {
+ font-size: $font-size-lg;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+}
+
+.modalClose {
+ width: 32px;
+ height: 32px;
+ @include flex-center;
+ border: none;
+ border-radius: $radius-md;
+ background: transparent;
+ color: $text-light;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.form {
+ @include flex-column;
+ gap: $space-4;
+}
+
+.field {
+ @include flex-column;
+ gap: $space-2;
+}
+
+.label {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+}
+
+.input {
+ width: 100%;
+ height: 44px;
+ padding: 0 $space-3;
+ background: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ color: $text-default;
+ @include transition-fast;
+
+ &::placeholder {
+ color: $text-muted;
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $border-strong;
+ }
+}
+
+.select {
+ width: 100%;
+ height: 44px;
+ padding: 0 $space-3;
+ background: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ color: $text-default;
+ cursor: pointer;
+ @include transition-fast;
+
+ &:focus {
+ outline: none;
+ border-color: $border-strong;
+ }
+
+ option {
+ background: $bg-surface-100;
+ color: $text-default;
+ }
+}
+
+.checkbox {
+ display: flex;
+ align-items: center;
+ gap: $space-2;
+ cursor: pointer;
+
+ input {
+ width: 18px;
+ height: 18px;
+ accent-color: $white;
+ }
+
+ span {
+ font-size: $font-size-sm;
+ color: $text-light;
+ }
+}
+
+.formActions {
+ display: flex;
+ gap: $space-3;
+ margin-top: $space-2;
+}
+
+.submitBtn {
+ flex: 1;
+ height: 44px;
+ @include flex-center;
+ background: $white;
+ border: none;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $black;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(0.9);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.cancelBtn {
+ flex: 1;
+ height: 44px;
+ @include flex-center;
+ background: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-light;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ background: $bg-surface-200;
+ color: $text-default;
+ }
+}
+
+.deleteConfirm {
+ @include flex-column;
+ gap: $space-4;
+}
+
+.deleteText {
+ font-size: $font-size-sm;
+ color: $text-light;
+ line-height: $line-height-relaxed;
+}
+
+.deleteEmail {
+ font-weight: $font-weight-medium;
+ color: $text-default;
+}
+
+.deleteBtn {
+ flex: 1;
+ height: 44px;
+ @include flex-center;
+ background: $error-default;
+ border: none;
+ border-radius: $radius-md;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $white;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(0.9);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/index.tsx
new file mode 100644
index 00000000..53fa22a1
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/admin/index.tsx
@@ -0,0 +1,415 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
+
+import { useState } from 'react'
+import { LuPencil, LuPlus, LuTrash2, LuX } from 'react-icons/lu'
+import {
+ useAdminCreateUser,
+ useAdminDeleteUser,
+ useAdminUpdateUser,
+ useAdminUsers,
+} from '@/api/hooks'
+import type { UserResponse } from '@/api/types'
+import { UserRole } from '@/api/types'
+import { PAGINATION } from '@/config'
+import styles from './admin.module.scss'
+
+type ModalState =
+ | { type: 'closed' }
+ | { type: 'create' }
+ | { type: 'edit'; user: UserResponse }
+ | { type: 'delete'; user: UserResponse }
+
+export function Component(): React.ReactElement {
+ const [page, setPage] = useState(PAGINATION.DEFAULT_PAGE)
+ const [modal, setModal] = useState({ type: 'closed' })
+
+ const { data, isLoading } = useAdminUsers({
+ page,
+ size: PAGINATION.DEFAULT_SIZE,
+ })
+ const createUser = useAdminCreateUser()
+ const updateUser = useAdminUpdateUser()
+ const deleteUser = useAdminDeleteUser()
+
+ const handleCreate = (formData: FormData): void => {
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+ const fullName = (formData.get('fullName') as string) || undefined
+ const role = formData.get('role') as UserRole
+ const isActive = formData.get('isActive') === 'on'
+
+ createUser.mutate(
+ { email, password, full_name: fullName, role, is_active: isActive },
+ { onSuccess: () => setModal({ type: 'closed' }) }
+ )
+ }
+
+ const handleUpdate = (userId: string, formData: FormData): void => {
+ const email = formData.get('email') as string
+ const fullName = (formData.get('fullName') as string) || undefined
+ const role = formData.get('role') as UserRole
+ const isActive = formData.get('isActive') === 'on'
+
+ updateUser.mutate(
+ {
+ id: userId,
+ data: { email, full_name: fullName, role, is_active: isActive },
+ },
+ { onSuccess: () => setModal({ type: 'closed' }) }
+ )
+ }
+
+ const handleDelete = (userId: string): void => {
+ deleteUser.mutate(userId, { onSuccess: () => setModal({ type: 'closed' }) })
+ }
+
+ const totalPages = data ? Math.ceil(data.total / PAGINATION.DEFAULT_SIZE) : 0
+
+ return (
+
+
+
Users
+ setModal({ type: 'create' })}
+ >
+
+ Create User
+
+
+
+
+
+
Email
+
Name
+
Role
+
Status
+
Actions
+
+
+
+ {isLoading &&
Loading...
}
+
+ {!isLoading && data?.items.length === 0 && (
+
No users found
+ )}
+
+ {data?.items.map((user) => (
+
+
+ {user.email}
+
+
+ {user.full_name ?? '—'}
+
+
+
+ {user.role}
+
+
+
+
+ {user.is_active ? 'Active' : 'Inactive'}
+
+
+
+ setModal({ type: 'edit', user })}
+ aria-label="Edit user"
+ >
+
+
+ setModal({ type: 'delete', user })}
+ aria-label="Delete user"
+ >
+
+
+
+
+ ))}
+
+
+ {data && data.total > PAGINATION.DEFAULT_SIZE && (
+
+
+ Page {page} of {totalPages} ({data.total} users)
+
+
+ setPage((p) => p - 1)}
+ disabled={page <= 1}
+ >
+ Previous
+
+ setPage((p) => p + 1)}
+ disabled={page >= totalPages}
+ >
+ Next
+
+
+
+ )}
+
+
+ {modal.type === 'create' && (
+
+
setModal({ type: 'closed' })}
+ onKeyDown={(e) => e.key === 'Escape' && setModal({ type: 'closed' })}
+ aria-label="Close modal"
+ />
+
+
+
Create User
+ setModal({ type: 'closed' })}
+ >
+
+
+
+
+
+
+ )}
+
+ {modal.type === 'edit' && (
+
+
setModal({ type: 'closed' })}
+ onKeyDown={(e) => e.key === 'Escape' && setModal({ type: 'closed' })}
+ aria-label="Close modal"
+ />
+
+
+
Edit User
+ setModal({ type: 'closed' })}
+ >
+
+
+
+
+
+
+ )}
+
+ {modal.type === 'delete' && (
+
+
setModal({ type: 'closed' })}
+ onKeyDown={(e) => e.key === 'Escape' && setModal({ type: 'closed' })}
+ aria-label="Close modal"
+ />
+
+
+
Delete User
+ setModal({ type: 'closed' })}
+ >
+
+
+
+
+
+ Are you sure you want to delete{' '}
+ {modal.user.email}?
+ This action cannot be undone.
+
+
+ setModal({ type: 'closed' })}
+ >
+ Cancel
+
+ handleDelete(modal.user.id)}
+ disabled={deleteUser.isPending}
+ >
+ {deleteUser.isPending ? 'Deleting...' : 'Delete'}
+
+
+
+
+
+ )}
+
+ )
+}
+
+Component.displayName = 'AdminUsers'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/dashboard.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/dashboard.module.scss
new file mode 100644
index 00000000..ecac295d
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/dashboard.module.scss
@@ -0,0 +1,152 @@
+// ===================
+// © AngelaMos | 2025
+// dashboard.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ min-height: calc(100vh - 56px);
+ padding: $space-6;
+ background-color: $bg-default;
+}
+
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.header {
+ margin-bottom: $space-6;
+}
+
+.title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+ margin-bottom: $space-2;
+}
+
+.subtitle {
+ font-size: $font-size-sm;
+ color: $text-lighter;
+}
+
+.userCard {
+ display: flex;
+ align-items: center;
+ gap: $space-4;
+ padding: $space-5;
+ background: $bg-surface-100;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+ margin-bottom: $space-8;
+}
+
+.avatar {
+ width: 56px;
+ height: 56px;
+ border-radius: $radius-full;
+ background: $bg-surface-300;
+ color: $text-default;
+ font-size: $font-size-xl;
+ font-weight: $font-weight-semibold;
+ @include flex-center;
+ flex-shrink: 0;
+}
+
+.userInfo {
+ @include flex-column;
+ gap: $space-1;
+ min-width: 0;
+}
+
+.userName {
+ font-size: $font-size-base;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+ @include truncate;
+}
+
+.userEmail {
+ font-size: $font-size-sm;
+ color: $text-light;
+ @include truncate;
+}
+
+.userRole {
+ display: inline-flex;
+ align-self: flex-start;
+ padding: $space-0-5 $space-2;
+ background: $bg-surface-200;
+ border-radius: $radius-full;
+ font-size: $font-size-xs;
+ color: $text-lighter;
+ text-transform: uppercase;
+ letter-spacing: $tracking-wide;
+}
+
+.section {
+ margin-bottom: $space-8;
+}
+
+.sectionTitle {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-light;
+ text-transform: uppercase;
+ letter-spacing: $tracking-wide;
+ margin-bottom: $space-4;
+}
+
+.grid {
+ display: grid;
+ gap: $space-4;
+
+ @include breakpoint-up('md') {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+.card {
+ padding: $space-4;
+ background: $bg-surface-100;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+}
+
+.hookName {
+ display: inline-block;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+ background: $bg-surface-200;
+ padding: $space-1 $space-2;
+ border-radius: $radius-sm;
+ margin-bottom: $space-2;
+}
+
+.description {
+ font-size: $font-size-sm;
+ color: $text-light;
+ margin-bottom: $space-2;
+ line-height: $line-height-relaxed;
+}
+
+.file {
+ font-size: $font-size-xs;
+ color: $text-muted;
+ font-family: $font-mono;
+}
+
+.list {
+ @include flex-column;
+ gap: $space-2;
+ padding-left: $space-5;
+ list-style: disc;
+
+ li {
+ font-size: $font-size-sm;
+ color: $text-light;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/index.tsx
new file mode 100644
index 00000000..5d3f97ec
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/index.tsx
@@ -0,0 +1,90 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
+
+import { useUser } from '@/core/lib'
+import styles from './dashboard.module.scss'
+
+const AVAILABLE_STORES = [
+ {
+ name: 'useUser()',
+ file: 'core/lib/auth.store.ts',
+ description: 'Get current authenticated user',
+ },
+ {
+ name: 'useIsAuthenticated()',
+ file: 'core/lib/auth.store.ts',
+ description: 'Check if user is logged in',
+ },
+ {
+ name: 'useIsAdmin()',
+ file: 'core/lib/auth.store.ts',
+ description: 'Check if user has admin role',
+ },
+]
+
+const SUGGESTED_FEATURES = [
+ 'User stats and metrics',
+ 'Recent activity feed',
+ 'Quick actions',
+ 'Charts and analytics',
+ 'Notifications overview',
+ 'Task/project summary',
+]
+
+export function Component(): React.ReactElement {
+ const user = useUser()
+
+ return (
+
+
+
+
+ Welcome{user?.full_name ? `, ${user.full_name}` : ''}
+
+
+ Template page — build your dashboard here
+
+
+
+
+
+ {user?.full_name?.[0]?.toUpperCase() ??
+ user?.email?.[0]?.toUpperCase() ??
+ 'U'}
+
+
+ {user?.full_name ?? 'User'}
+ {user?.email}
+ {user?.role}
+
+
+
+
+ Available Stores
+
+ {AVAILABLE_STORES.map((store) => (
+
+
{store.name}
+
{store.description}
+
{store.file}
+
+ ))}
+
+
+
+
+ Suggested Features
+
+ {SUGGESTED_FEATURES.map((feature) => (
+ - {feature}
+ ))}
+
+
+
+
+ )
+}
+
+Component.displayName = 'Dashboard'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/index.tsx
new file mode 100644
index 00000000..48729f0b
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/index.tsx
@@ -0,0 +1,97 @@
+/**
+ * ©AngelaMos | 2026
+ * index.tsx
+ */
+
+import { FiGithub } from 'react-icons/fi'
+import { Link } from 'react-router-dom'
+import { ROUTES } from '@/config'
+import styles from './landing.module.scss'
+
+export function Component(): React.ReactElement {
+ return (
+
+
+ Full Stack Template
+ by Carter Perez
+
+
+
+
+
+
+
+ Boilerplate for medium-large scale applications. Built with modern
+ patterns, strict typing, and security best practices.
+
+
+
+
+ Frontend
+
+ - React 19 + TypeScript with strict mode
+ - TanStack Query for server state caching
+ - Zustand stores with persistence
+ - Axios interceptors with auto token refresh
+ - Zod runtime validation on API responses
+ - SCSS modules with design tokens
+
+
+
+
+ Backend
+
+ - DDD + DI Architecture
+ - FastAPI with async/await throughout
+ - SQLAlchemy 2.0+ async with connection pooling
+ - JWT auth with token rotation and replay detection
+ - Argon2id hashing with timing safe verification
+ - Pydantic v2 strict validation
+
+
+
+
+ Infrastructure
+
+ - Docker Compose with dev/prod configs
+ - Nginx reverse proxy with rate limiting
+ - PostgreSQL 18 + Redis 7
+ - Health checks and graceful shutdown
+
+
+
+
+ DevOps
+
+ - GitHub Actions CI (Ruff, Pylint, Mypy, Biome)
+ - Strict type checking on both ends
+ - Alembic async migrations
+
+
+
+
+
+
+
+ )
+}
+
+Component.displayName = 'Landing'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/landing.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/landing.module.scss
new file mode 100644
index 00000000..3013e87a
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/landing/landing.module.scss
@@ -0,0 +1,161 @@
+// ===================
+// © AngelaMos | 2025
+// landing.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ min-height: 100vh;
+ min-height: 100dvh;
+ @include flex-column-center;
+ background-color: $bg-default;
+ background-image: radial-gradient(
+ circle,
+ $bg-landing 1px,
+ transparent 1px
+ );
+ background-size: 20px 20px;
+ padding: $space-8;
+}
+
+.header {
+ text-align: center;
+ margin-bottom: $space-5;
+}
+
+.title {
+ font-size: $font-size-4xl;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+ letter-spacing: $tracking-tight;
+ margin-bottom: $space-2;
+}
+
+.subtitle {
+ font-size: $font-size-lg;
+ color: $text-light;
+ margin-bottom: $space-3;
+}
+
+.github {
+ display: inline-flex;
+ color: $text-muted;
+ font-size: $font-size-2xl;
+ @include transition-fast;
+
+ @include hover {
+ &:hover {
+ color: $text-default;
+ }
+ }
+}
+
+.content {
+ max-width: 800px;
+ text-align: center;
+}
+
+.description {
+ font-size: $font-size-base;
+ color: $text-light;
+ line-height: $line-height-relaxed;
+ margin-bottom: $space-8;
+}
+
+.sections {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: $space-6;
+ margin-bottom: $space-10;
+ text-align: left;
+
+ @include breakpoint-down(md) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.section {
+ padding: $space-5;
+ background-color: $bg-surface-75;
+ border: 1px solid $border-muted;
+ border-radius: $radius-lg;
+}
+
+.sectionTitle {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+ margin-bottom: $space-3;
+ letter-spacing: $tracking-wide;
+ text-transform: uppercase;
+}
+
+.features {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: $space-2;
+
+ li {
+ font-size: $font-size-sm;
+ color: $text-muted;
+
+ &::before {
+ content: '→';
+ margin-right: $space-2;
+ color: $text-lighter;
+ }
+ }
+}
+
+.actions {
+ display: flex;
+ gap: $space-3;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: $space-3 $space-5;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $bg-default;
+ background-color: $text-default;
+ border-radius: $radius-md;
+ text-decoration: none;
+ @include transition-fast;
+
+ @include hover {
+ &:hover {
+ filter: brightness(0.9);
+ }
+ }
+}
+
+.buttonOutline {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: $space-3 $space-5;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+ background-color: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ text-decoration: none;
+ @include transition-fast;
+
+ @include hover {
+ &:hover {
+ border-color: $border-strong;
+ background-color: $bg-surface-75;
+ }
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/index.tsx
new file mode 100644
index 00000000..13caa91c
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/index.tsx
@@ -0,0 +1,114 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
+
+import { useState } from 'react'
+import { LuEye, LuEyeOff } from 'react-icons/lu'
+import { Link, useNavigate } from 'react-router-dom'
+import { toast } from 'sonner'
+import { useLogin } from '@/api/hooks'
+import { loginRequestSchema } from '@/api/types'
+import { ROUTES } from '@/config'
+import { useAuthFormStore } from '@/core/lib'
+import styles from './login.module.scss'
+
+export function Component(): React.ReactElement {
+ const navigate = useNavigate()
+ const login = useLogin()
+
+ const { loginEmail, setLoginEmail, clearLoginForm } = useAuthFormStore()
+ const [password, setPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+
+ const handleSubmit = (e: React.FormEvent): void => {
+ e.preventDefault()
+
+ const result = loginRequestSchema.safeParse({
+ username: loginEmail,
+ password,
+ })
+
+ if (!result.success) {
+ const firstError = result.error.issues[0]
+ toast.error(firstError.message)
+ return
+ }
+
+ login.mutate(result.data, {
+ onSuccess: () => {
+ clearLoginForm()
+ navigate(ROUTES.DASHBOARD)
+ },
+ })
+ }
+
+ return (
+
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+ )
+}
+
+Component.displayName = 'Login'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/login.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/login.module.scss
new file mode 100644
index 00000000..6d01ed24
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/login/login.module.scss
@@ -0,0 +1,171 @@
+// ===================
+// © AngelaMos | 2025
+// login.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ min-height: 100vh;
+ min-height: 100dvh;
+ @include flex-center;
+ background-color: $bg-default;
+ background-image: radial-gradient(
+ circle,
+ $bg-page 1px,
+ transparent 1px
+ );
+ background-size: 22px 22px;
+ padding: $space-4;
+}
+
+.card {
+ width: 100%;
+ max-width: 400px;
+ background-color: $black;
+ background-image: radial-gradient(
+ circle,
+ $bg-card 1px,
+ transparent 1px
+ );
+ background-size: 20px 20px;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+ padding: $space-8;
+}
+
+.header {
+ margin-bottom: $space-6;
+}
+
+.title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ color: $white;
+ margin-bottom: $space-2;
+}
+
+.subtitle {
+ font-size: $font-size-sm;
+ color: $text-light;
+}
+
+.form {
+ @include flex-column;
+ gap: $space-5;
+}
+
+.field {
+ @include flex-column;
+ gap: $space-2;
+}
+
+.label {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $white;
+}
+
+.input {
+ width: 100%;
+ height: 48px;
+ padding: 0 $space-4;
+ background-color: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ font-size: $font-size-base;
+ color: $white;
+ @include transition-fast;
+
+ &::placeholder {
+ color: $text-muted;
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $border-strong;
+ }
+
+ &[aria-invalid='true'] {
+ border-color: $error-default;
+ }
+}
+
+.inputWrapper {
+ position: relative;
+ width: 100%;
+}
+
+.eyeButton {
+ position: absolute;
+ right: $space-3;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ color: $text-muted;
+ cursor: pointer;
+ padding: $space-1;
+ @include transition-fast;
+
+ &:hover {
+ color: $white;
+ }
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.error {
+ font-size: $font-size-xs;
+ color: $error-default;
+}
+
+.submit {
+ width: 100%;
+ height: 48px;
+ margin-top: $space-2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $white;
+ border: none;
+ border-radius: $radius-md;
+ font-size: $font-size-base;
+ font-weight: $font-weight-medium;
+ color: $black;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(0.9);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.footer {
+ margin-top: $space-6;
+ text-align: center;
+ font-size: $font-size-sm;
+ color: $text-light;
+}
+
+.link {
+ color: $text-default;
+ text-decoration: underline;
+ text-underline-offset: 4px;
+ @include transition-fast;
+
+ @include hover {
+ color: $text-light;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/index.tsx
new file mode 100644
index 00000000..f0f003f7
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/index.tsx
@@ -0,0 +1,164 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
+
+import { useState } from 'react'
+import { LuEye, LuEyeOff } from 'react-icons/lu'
+import { Link, useNavigate } from 'react-router-dom'
+import { toast } from 'sonner'
+import { z } from 'zod'
+import { useRegister } from '@/api/hooks'
+import { userCreateRequestSchema } from '@/api/types'
+import { PASSWORD_CONSTRAINTS, ROUTES } from '@/config'
+import { useAuthFormStore } from '@/core/lib'
+import styles from './register.module.scss'
+
+const registerFormSchema = userCreateRequestSchema
+ .extend({
+ confirmPassword: z
+ .string()
+ .min(
+ PASSWORD_CONSTRAINTS.MIN_LENGTH,
+ `Password must be at least ${PASSWORD_CONSTRAINTS.MIN_LENGTH} characters`
+ )
+ .max(PASSWORD_CONSTRAINTS.MAX_LENGTH),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: 'Passwords do not match',
+ path: ['confirmPassword'],
+ })
+
+export function Component(): React.ReactElement {
+ const navigate = useNavigate()
+ const register = useRegister()
+
+ const { registerEmail, setRegisterEmail, clearRegisterForm } =
+ useAuthFormStore()
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+ const handleSubmit = (e: React.FormEvent): void => {
+ e.preventDefault()
+
+ const result = registerFormSchema.safeParse({
+ email: registerEmail,
+ password,
+ confirmPassword,
+ })
+
+ if (!result.success) {
+ const firstError = result.error.issues[0]
+ toast.error(firstError.message)
+ return
+ }
+
+ register.mutate(
+ { email: result.data.email, password: result.data.password },
+ {
+ onSuccess: () => {
+ clearRegisterForm()
+ toast.success('Account created successfully')
+ navigate(ROUTES.LOGIN)
+ },
+ }
+ )
+ }
+
+ return (
+
+
+
+
Sign up
+
Create a new account
+
+
+
+
+
+ Already have an account?{' '}
+
+ Login
+
+
+
+
+ )
+}
+
+Component.displayName = 'Register'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/register.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/register.module.scss
new file mode 100644
index 00000000..55ad5717
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/register/register.module.scss
@@ -0,0 +1,171 @@
+// ===================
+// © AngelaMos | 2025
+// register.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ min-height: 100vh;
+ min-height: 100dvh;
+ @include flex-center;
+ background-color: $bg-default;
+ background-image: radial-gradient(
+ circle,
+ $bg-page 1px,
+ transparent 1px
+ );
+ background-size: 22px 22px;
+ padding: $space-4;
+}
+
+.card {
+ width: 100%;
+ max-width: 400px;
+ background-color: $black;
+ background-image: radial-gradient(
+ circle,
+ $bg-card 1px,
+ transparent 1px
+ );
+ background-size: 20px 20px;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+ padding: $space-8;
+}
+
+.header {
+ margin-bottom: $space-6;
+}
+
+.title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ color: $white;
+ margin-bottom: $space-2;
+}
+
+.subtitle {
+ font-size: $font-size-sm;
+ color: $text-light;
+}
+
+.form {
+ @include flex-column;
+ gap: $space-5;
+}
+
+.field {
+ @include flex-column;
+ gap: $space-2;
+}
+
+.label {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $white;
+}
+
+.input {
+ width: 100%;
+ height: 48px;
+ padding: 0 $space-4;
+ background-color: transparent;
+ border: 1px solid $border-default;
+ border-radius: $radius-md;
+ font-size: $font-size-base;
+ color: $white;
+ @include transition-fast;
+
+ &::placeholder {
+ color: $text-muted;
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $border-strong;
+ }
+
+ &[aria-invalid='true'] {
+ border-color: $error-default;
+ }
+}
+
+.inputWrapper {
+ position: relative;
+ width: 100%;
+}
+
+.eyeButton {
+ position: absolute;
+ right: $space-3;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ color: $text-muted;
+ cursor: pointer;
+ padding: $space-1;
+ @include transition-fast;
+
+ &:hover {
+ color: $white;
+ }
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.error {
+ font-size: $font-size-xs;
+ color: $error-default;
+}
+
+.submit {
+ width: 100%;
+ height: 48px;
+ margin-top: $space-2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $white;
+ border: none;
+ border-radius: $radius-md;
+ font-size: $font-size-base;
+ font-weight: $font-weight-medium;
+ color: $black;
+ cursor: pointer;
+ @include transition-fast;
+
+ @include hover {
+ filter: brightness(0.9);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.footer {
+ margin-top: $space-6;
+ text-align: center;
+ font-size: $font-size-sm;
+ color: $text-light;
+}
+
+.link {
+ color: $text-default;
+ text-decoration: underline;
+ text-underline-offset: 4px;
+ @include transition-fast;
+
+ @include hover {
+ color: $text-light;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/index.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/index.tsx
new file mode 100644
index 00000000..8a22eb65
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/index.tsx
@@ -0,0 +1,95 @@
+/**
+ * ©AngelaMos | 2025
+ * index.tsx
+ */
+
+import styles from './settings.module.scss'
+
+const AVAILABLE_HOOKS = [
+ {
+ name: 'useUpdateProfile()',
+ file: 'api/hooks/useUsers.ts',
+ description: 'Update user profile (full_name)',
+ endpoint: 'PATCH /api/v1/users/me',
+ },
+ {
+ name: 'useChangePassword()',
+ file: 'api/hooks/useAuth.ts',
+ description: 'Change password (current + new)',
+ endpoint: 'POST /api/v1/auth/change-password',
+ },
+]
+
+const AVAILABLE_STORES = [
+ {
+ name: 'useAuthStore()',
+ file: 'core/lib/auth.store.ts',
+ description: 'Access user state, logout, updateUser',
+ },
+ {
+ name: 'useUser()',
+ file: 'core/lib/auth.store.ts',
+ description: 'Get current user from store',
+ },
+]
+
+export function Component(): React.ReactElement {
+ return (
+
+
+
+
Settings
+
+ Template page — available hooks and stores for building your settings
+ UI
+
+
+
+
+ Available Hooks
+
+ {AVAILABLE_HOOKS.map((hook) => (
+
+
{hook.name}
+
{hook.description}
+
+ {hook.file}
+ {hook.endpoint}
+
+
+ ))}
+
+
+
+
+ Available Stores
+
+ {AVAILABLE_STORES.map((store) => (
+
+
{store.name}
+
{store.description}
+
+ {store.file}
+
+
+ ))}
+
+
+
+
+ Suggested Features
+
+ - Profile form (full name, avatar)
+ - Change password form
+ - Email preferences
+ - Theme toggle (dark/light)
+ - Notification settings
+ - Delete account
+
+
+
+
+ )
+}
+
+Component.displayName = 'Settings'
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/settings.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/settings.module.scss
new file mode 100644
index 00000000..16d85855
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/settings/settings.module.scss
@@ -0,0 +1,109 @@
+// ===================
+// © AngelaMos | 2025
+// settings.module.scss
+// ===================
+
+@use '@/styles' as *;
+
+.page {
+ min-height: calc(100vh - 56px);
+ padding: $space-6;
+ background-color: $bg-default;
+}
+
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.header {
+ margin-bottom: $space-8;
+}
+
+.title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ color: $text-default;
+ margin-bottom: $space-2;
+}
+
+.subtitle {
+ font-size: $font-size-sm;
+ color: $text-lighter;
+}
+
+.section {
+ margin-bottom: $space-8;
+}
+
+.sectionTitle {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-light;
+ text-transform: uppercase;
+ letter-spacing: $tracking-wide;
+ margin-bottom: $space-4;
+}
+
+.grid {
+ display: grid;
+ gap: $space-4;
+
+ @include breakpoint-up('md') {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+.card {
+ padding: $space-4;
+ background: $bg-surface-100;
+ border: 1px solid $border-default;
+ border-radius: $radius-lg;
+}
+
+.hookName {
+ display: inline-block;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-medium;
+ color: $text-default;
+ background: $bg-surface-200;
+ padding: $space-1 $space-2;
+ border-radius: $radius-sm;
+ margin-bottom: $space-2;
+}
+
+.description {
+ font-size: $font-size-sm;
+ color: $text-light;
+ margin-bottom: $space-3;
+ line-height: $line-height-relaxed;
+}
+
+.meta {
+ @include flex-column;
+ gap: $space-1;
+}
+
+.file {
+ font-size: $font-size-xs;
+ color: $text-lighter;
+ font-family: $font-mono;
+}
+
+.endpoint {
+ font-size: $font-size-xs;
+ color: $text-muted;
+ font-family: $font-mono;
+}
+
+.list {
+ @include flex-column;
+ gap: $space-2;
+ padding-left: $space-5;
+ list-style: disc;
+
+ li {
+ font-size: $font-size-sm;
+ color: $text-light;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles.scss
new file mode 100644
index 00000000..4ac95fe0
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles.scss
@@ -0,0 +1,29 @@
+// ===================
+// © AngelaMos | 2025
+// styles.scss
+// ===================
+
+@forward 'styles/tokens';
+@forward 'styles/fonts';
+@forward 'styles/mixins';
+
+@use 'styles/reset';
+@use 'styles/tokens' as *;
+@use 'styles/fonts' as *;
+
+#root {
+ min-height: 100vh;
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ background: $bg-default;
+}
+
+.app {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: $bg-default;
+ color: $text-default;
+ font-family: $font-sans;
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_fonts.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_fonts.scss
new file mode 100644
index 00000000..4f517884
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_fonts.scss
@@ -0,0 +1,12 @@
+// ===================
+// © AngelaMos | 2025
+// _fonts.scss
+// ===================
+
+@use 'tokens' as *;
+
+$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto,
+ 'Helvetica Neue', Arial, sans-serif;
+
+$font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
+ 'Liberation Mono', monospace;
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_index.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_index.scss
new file mode 100644
index 00000000..ac5a8266
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_index.scss
@@ -0,0 +1,8 @@
+// ===================
+// © AngelaMos | 2025
+// _index.scss
+// ===================
+
+@forward 'tokens';
+@forward 'fonts';
+@forward 'mixins';
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_mixins.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_mixins.scss
new file mode 100644
index 00000000..6b5b1ba9
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_mixins.scss
@@ -0,0 +1,120 @@
+// ===================
+// © AngelaMos | 2025
+// _mixins.scss
+// ===================
+
+@use 'sass:map';
+@use 'sass:list';
+@use 'tokens' as *;
+
+$breakpoints: (
+ 'xs': $breakpoint-xs,
+ 'sm': $breakpoint-sm,
+ 'md': $breakpoint-md,
+ 'lg': $breakpoint-lg,
+ 'xl': $breakpoint-xl,
+ '2xl': $breakpoint-2xl,
+);
+
+@mixin breakpoint-up($size) {
+ @media (min-width: map.get($breakpoints, $size)) {
+ @content;
+ }
+}
+
+@mixin breakpoint-down($size) {
+ @media (width < map.get($breakpoints, $size)) {
+ @content;
+ }
+}
+
+@mixin flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@mixin flex-between {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+@mixin flex-column {
+ display: flex;
+ flex-direction: column;
+}
+
+@mixin flex-column-center {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+@mixin sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
+@mixin truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@mixin line-clamp($lines: 2) {
+ display: -webkit-box;
+ -webkit-line-clamp: #{$lines};
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+@mixin transition-fast {
+ transition-property: background-color, border-color, color, opacity;
+ transition-duration: $duration-fast;
+ transition-timing-function: $ease-out;
+}
+
+@mixin transition-normal {
+ transition-property: background-color, border-color, color, opacity;
+ transition-duration: $duration-normal;
+ transition-timing-function: $ease-out;
+}
+
+@mixin absolute-fill {
+ position: absolute;
+ inset: 0;
+}
+
+@mixin absolute-center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+@mixin hover {
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ @content;
+ }
+ }
+}
+
+@mixin hide-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_reset.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_reset.scss
new file mode 100644
index 00000000..b6af56b5
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_reset.scss
@@ -0,0 +1,198 @@
+// ===================
+// © AngelaMos | 2025
+// _reset.scss
+// ===================
+
+@use 'tokens' as *;
+@use 'fonts' as *;
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ -webkit-tap-highlight-color: transparent;
+}
+
+html {
+ font-size: 16px;
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+ overflow-x: hidden;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ html {
+ interpolate-size: allow-keywords;
+ scroll-behavior: smooth;
+ }
+}
+
+body {
+ min-height: 100vh;
+ min-height: 100dvh;
+ line-height: $line-height-normal;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow-x: hidden;
+ background-color: $bg-default;
+ color: $text-default;
+ font-family: $font-sans;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ line-height: $line-height-tight;
+ text-wrap: balance;
+ overflow-wrap: break-word;
+ font-weight: $font-weight-semibold;
+}
+
+p {
+ text-wrap: pretty;
+ overflow-wrap: break-word;
+}
+
+ul,
+ol {
+ list-style: none;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+ color: inherit;
+}
+
+input[type='text'],
+input[type='email'],
+input[type='password'],
+input[type='search'],
+input[type='number'],
+input[type='tel'],
+input[type='url'],
+textarea,
+select {
+ font-size: $font-size-sm;
+ appearance: none;
+}
+
+button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-align: inherit;
+ font-family: inherit;
+}
+
+fieldset {
+ border: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+textarea:not([rows]) {
+ min-height: 10em;
+}
+
+:target {
+ scroll-margin-block: 5ex;
+}
+
+:focus-visible {
+ outline: 2px solid $border-strong;
+ outline-offset: 2px;
+}
+
+:focus:not(:focus-visible) {
+ outline: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+dialog {
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+
+summary {
+ cursor: pointer;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
+
+@supports (padding: max(0px)) {
+ body {
+ padding-left: max(0px, env(safe-area-inset-left));
+ padding-right: max(0px, env(safe-area-inset-right));
+ }
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: $border-default;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: $border-strong;
+}
+
+::selection {
+ background-color: $bg-selection;
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_tokens.scss
new file mode 100644
index 00000000..be1f6dd6
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/styles/_tokens.scss
@@ -0,0 +1,168 @@
+// ===================
+// © AngelaMos | 2025
+// _tokens.scss
+// ===================
+
+// ============================================================================
+// SPACING (8px base system)
+// ============================================================================
+$space-0: 0;
+$space-px: 1px;
+$space-0-5: 0.125rem;
+$space-1: 0.25rem;
+$space-1-5: 0.375rem;
+$space-2: 0.5rem;
+$space-2-5: 0.625rem;
+$space-3: 0.75rem;
+$space-3-5: 0.875rem;
+$space-4: 1rem;
+$space-5: 1.25rem;
+$space-6: 1.5rem;
+$space-7: 1.75rem;
+$space-8: 2rem;
+$space-9: 2.25rem;
+$space-10: 2.5rem;
+$space-11: 2.75rem;
+$space-12: 3rem;
+$space-14: 3.5rem;
+$space-16: 4rem;
+$space-20: 5rem;
+$space-24: 6rem;
+$space-28: 7rem;
+$space-32: 8rem;
+
+// ============================================================================
+// TYPOGRAPHY SCALE
+// ============================================================================
+$font-size-3xs: 0.625rem;
+$font-size-2xs: 0.6875rem;
+$font-size-xs: 0.75rem;
+$font-size-sm: 0.875rem;
+$font-size-base: 1rem;
+$font-size-lg: 1.125rem;
+$font-size-xl: 1.25rem;
+$font-size-2xl: 1.5rem;
+$font-size-3xl: 1.875rem;
+$font-size-4xl: 2.25rem;
+$font-size-5xl: 3rem;
+
+// ============================================================================
+// FONT WEIGHTS
+// ============================================================================
+$font-weight-regular: 400;
+$font-weight-medium: 500;
+$font-weight-semibold: 600;
+
+// ============================================================================
+// LINE HEIGHTS
+// ============================================================================
+$line-height-none: 1;
+$line-height-tight: 1.2;
+$line-height-snug: 1.375;
+$line-height-normal: 1.5;
+$line-height-relaxed: 1.625;
+
+// ============================================================================
+// LETTER SPACING
+// ============================================================================
+$tracking-tighter: -0.05em;
+$tracking-tight: -0.025em;
+$tracking-normal: 0;
+$tracking-wide: 0.025em;
+$tracking-wider: 0.05em;
+
+// ============================================================================
+// COLORS
+// ============================================================================
+$white: hsl(0, 0%, 100%);
+$black: hsl(0, 0%, 0%);
+
+// Auth
+$bg-page: hsl(0, 0%, 10.5%);
+$bg-card: hsl(0, 0%, 6.2%);
+
+// Home/landing
+$bg-landing: hsl(0, 0%, 10.8%);
+
+$bg-default: hsl(0, 0%, 7.1%);
+$bg-alternative: hsl(0, 0%, 5.9%);
+$bg-surface-75: hsl(0, 0%, 9%);
+$bg-surface-100: hsl(0, 0%, 12.2%);
+$bg-surface-200: hsl(0, 0%, 14.1%);
+$bg-surface-300: hsl(0, 0%, 16.1%);
+$bg-control: hsl(0, 0%, 10%);
+$bg-selection: hsl(0, 0%, 19.2%);
+$bg-overlay: hsl(0, 0%, 14.1%);
+$bg-overlay-hover: hsl(0, 0%, 18%);
+
+$border-muted: hsl(0, 0%, 11.1%);
+$border-default: hsl(0, 0%, 18%);
+$border-strong: hsl(0, 0%, 22.4%);
+$border-stronger: hsl(0, 0%, 27.1%);
+$border-control: hsl(0, 0%, 22.4%);
+
+$text-default: hsl(0, 0%, 98%);
+$text-light: hsl(0, 0%, 70.6%);
+$text-lighter: hsl(0, 0%, 53.7%);
+$text-muted: hsl(0, 0%, 30.2%);
+
+$error-default: hsl(0, 72%, 51%);
+$error-light: hsl(0, 72%, 65%);
+
+// ============================================================================
+// BORDER RADIUS
+// ============================================================================
+$radius-none: 0;
+$radius-xs: 2px;
+$radius-sm: 4px;
+$radius-md: 6px;
+$radius-lg: 8px;
+$radius-xl: 12px;
+$radius-full: 9999px;
+
+// ============================================================================
+// Z-INDEX SCALE
+// ============================================================================
+$z-hide: -1;
+$z-base: 0;
+$z-dropdown: 100;
+$z-sticky: 200;
+$z-fixed: 300;
+$z-overlay: 400;
+$z-modal: 500;
+$z-popover: 600;
+$z-tooltip: 700;
+$z-toast: 800;
+$z-max: 9999;
+
+// ============================================================================
+// TRANSITIONS
+// ============================================================================
+$duration-instant: 0ms;
+$duration-fast: 100ms;
+$duration-normal: 150ms;
+$duration-slow: 200ms;
+
+$ease-out: cubic-bezier(0, 0, 0.2, 1);
+$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+
+// ============================================================================
+// BREAKPOINTS
+// ============================================================================
+$breakpoint-xs: 360px;
+$breakpoint-sm: 480px;
+$breakpoint-md: 768px;
+$breakpoint-lg: 1024px;
+$breakpoint-xl: 1280px;
+$breakpoint-2xl: 1536px;
+
+// ============================================================================
+// CONTAINER WIDTHS
+// ============================================================================
+$container-xs: 20rem;
+$container-sm: 24rem;
+$container-md: 28rem;
+$container-lg: 32rem;
+$container-xl: 36rem;
+$container-2xl: 42rem;
+$container-full: 100%;
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/stylelint.config.js b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/stylelint.config.js
new file mode 100644
index 00000000..56ebd7bc
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/stylelint.config.js
@@ -0,0 +1,107 @@
+// ©AngelaMos | 2025
+// stylelint.config.js
+
+/** @type {import('stylelint').Config} */
+export default {
+ extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'],
+ rules: {
+ 'block-no-empty': true,
+ 'declaration-no-important': true,
+ 'color-no-invalid-hex': true,
+ 'property-no-unknown': true,
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['global'],
+ },
+ ],
+
+ 'selector-class-pattern': [
+ '^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$',
+ {
+ message:
+ 'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)',
+ },
+ ],
+
+ 'value-keyword-case': [
+ 'lower',
+ {
+ camelCaseSvgKeywords: true,
+ ignoreKeywords: [
+ 'BlinkMacSystemFont',
+ 'SFMono-Regular',
+ 'Menlo',
+ 'Monaco',
+ 'Consolas',
+ 'Roboto',
+ 'Arial',
+ 'Helvetica',
+ 'Times',
+ 'Georgia',
+ 'Verdana',
+ 'Tahoma',
+ 'Trebuchet',
+ 'Impact',
+ 'Comic',
+ ],
+ },
+ ],
+
+ 'property-no-vendor-prefix': [
+ true,
+ {
+ ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'],
+ },
+ ],
+ 'value-no-vendor-prefix': true,
+ 'selector-no-vendor-prefix': true,
+
+ 'property-no-deprecated': [
+ true,
+ {
+ ignoreProperties: ['clip'],
+ },
+ ],
+
+ 'container-name-pattern': null,
+ 'layer-name-pattern': null,
+
+ 'scss/at-rule-no-unknown': true,
+ 'scss/declaration-nested-properties-no-divided-groups': true,
+ 'scss/dollar-variable-no-missing-interpolation': true,
+ 'scss/dollar-variable-empty-line-before': null,
+
+ 'declaration-empty-line-before': null,
+ 'custom-property-empty-line-before': null,
+
+ 'no-descending-specificity': null,
+
+ 'media-feature-name-no-unknown': [
+ true,
+ {
+ ignoreMediaFeatureNames: ['map'],
+ },
+ ],
+
+ 'color-function-notation': null,
+ 'hue-degree-notation': null,
+ },
+ ignoreFiles: [
+ 'node_modules/**',
+ 'dist/**',
+ 'build/**',
+ '**/*.js',
+ '**/*.ts',
+ '**/*.tsx',
+ ],
+ overrides: [
+ {
+ files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'],
+ rules: {
+ 'declaration-no-important': null,
+ 'scss/comment-no-empty': null,
+ },
+ },
+ ],
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.app.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.app.json
new file mode 100644
index 00000000..afe17f60
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.app.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.node.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.node.json
new file mode 100644
index 00000000..a96b3e59
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/vite.config.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/vite.config.ts
new file mode 100644
index 00000000..a8d5d478
--- /dev/null
+++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/vite.config.ts
@@ -0,0 +1,71 @@
+/**
+ * ©AngelaMos | 2025
+ * vite.config.ts
+ */
+
+import path from 'node:path'
+import react from '@vitejs/plugin-react'
+import { defineConfig, loadEnv } from 'vite'
+import tsconfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
+ const isDev = mode === 'development'
+
+ return {
+ plugins: [react(), tsconfigPaths()],
+
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+
+ css: {
+ preprocessorOptions: {
+ scss: {},
+ },
+ },
+
+ server: {
+ port: 5173,
+ host: '0.0.0.0',
+ proxy: {
+ '/api': {
+ target: env.VITE_API_TARGET || 'http://localhost:8000',
+ changeOrigin: true,
+ rewrite: (p) => p.replace(/^\/api/, ''),
+ },
+ },
+ },
+
+ build: {
+ target: 'esnext',
+ cssTarget: 'chrome100',
+ sourcemap: isDev ? true : 'hidden',
+ minify: 'oxc',
+ rollupOptions: {
+ output: {
+ manualChunks(id: string): string | undefined {
+ if (id.includes('node_modules')) {
+ if (id.includes('react-dom') || id.includes('react-router')) {
+ return 'vendor-react'
+ }
+ if (id.includes('@tanstack/react-query')) {
+ return 'vendor-query'
+ }
+ if (id.includes('zustand')) {
+ return 'vendor-state'
+ }
+ }
+ return undefined
+ },
+ },
+ },
+ },
+
+ preview: {
+ port: 4173,
+ },
+ }
+})