initial commit

This commit is contained in:
Daniel Noulet 2026-10-07 15:17:40 +02:00
parent ebd5bb09fd
commit 3046604b64
26 changed files with 1535 additions and 82 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ dist/
*.pem
*.key
*.cert
/bin/
.idea

View file

@ -8,13 +8,15 @@ RUN go mod download
COPY cmd/ /src/cmd/
COPY internal/ /src/internal/
COPY web/ /src/web/
COPY internal/migration/ /src/internal/migration/
COPY internal/migration/ /internal/migration/
RUN go build -o /out/go-mad ./cmd/go-mad
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/go-mad /go-mad
COPY --from=builder /src/web /src/web
COPY --from=builder /internal/migration /internal/migration
EXPOSE 8080

View file

@ -1,4 +1,4 @@
.PHONY: all build test vet fmt compose-up compose-down logs clean help
.PHONY: all build test vet fmt compose-up compose-down logs clean help web-dev
GO_VERSION ?= 1.27.1
BINARY_NAME ?= go-mad
@ -19,7 +19,7 @@ build: fmt vet ## Build the Go binary
## ---- Development ----
run: build ## Run the Go binary (requires postgres running)
@$(BINARY_NAME)
@$(BINARY_DIR)/$(BINARY_NAME)
compose-up: ## Start docker compose (postgres + go-mad)
docker compose up --build -d
@ -48,12 +48,10 @@ integration-test: ## Run integration tests (requires test database)
## ---- Web Frontend ----
web-build: ## Build React frontend
npm install
npm run build
cd web && npm install && npm run build
web-dev: ## Run React dev server
npm install
npm run dev
web-dev: ## Start the go-mad application with Docker Compose
@docker compose up --build -d
## ---- Docker ----
@ -75,24 +73,19 @@ help: ## Show this help message
@echo ""
@echo "Build targets:"
@echo " build Build the Go binary"
@echo " run Run the Go binary"
@echo " run Run the Go binary (requires postgres running)"
@echo " compose-up Start docker compose"
@echo " compose-down Stop docker compose"
@echo " compose-logs View docker compose logs"
@echo ""
@echo "Code quality:"
@echo " fmt Format Go code"
@echo " vet Vet Go code"
@echo " test Run Go unit tests"
@echo "Web frontend:"
@echo " web-build Build React frontend"
@echo " web-dev Start go-mad with docker compose"
@echo ""
@echo "Docker:"
@echo " docker-build Build docker image"
@echo " docker-push Push docker image"
@echo ""
@echo "Web frontend:"
@echo " web-build Build React frontend"
@echo " web-dev Run React dev server"
@echo ""
@echo "Other:"
@echo " clean Remove build artifacts"
@echo " help Show this help message"

Binary file not shown.

View file

@ -1,13 +1,16 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/dnoulet/go-mad/internal/config"
"github.com/dnoulet/go-mad/internal/database"
"github.com/dnoulet/go-mad/internal/handler"
)
func main() {
@ -22,21 +25,39 @@ func main() {
}
defer db.Close()
if err := database.Migrate(db, "internal/migration"); err != nil {
if err := database.Migrate(db, "/internal/migration"); err != nil {
log.Fatalf("Running migrations: %v", err)
}
h := handler.New(db)
if err != nil {
log.Fatalf(" initializing handler: %v", err)
}
httpServer := &http.Server{
Addr: cfg.Server.Host + ":" + cfg.Server.Port,
Handler: h,
}
// Set up signal handling for graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// Start server
// Start HTTP server
go func() {
log.Printf("Starting server on %s:%s", cfg.Server.Host, cfg.Server.Port)
// TODO: start HTTP server
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server failed: %v", err)
}
}()
// Wait for interrupt
<-stop
log.Println("Shutting down gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*60)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown failed: %v", err)
}
log.Println("Server stopped")
}

View file

@ -1,5 +1,5 @@
services:
go-mad-database:
postgres:
image: postgres:15-alpine
container_name: go-mad-postgres
environment:
@ -20,15 +20,13 @@ services:
dockerfile: Dockerfile
container_name: go-mad
depends_on:
go-mad-database:
postgres:
condition: service_healthy
extra_hosts:
- "go-mad-database:host-gateway"
environment:
GO_MAD_DATABASE_URL: postgres://mad:mad@go-mad-database:5432/mad?sslmode=disable
GO_MAD_DATABASE_URL: postgres://mad:mad@postgres:5432/mad?sslmode=disable
GO_MAD_SERVER_PORT: "8080"
GO_MAD_JWT_SECRET: change-me-to-a-32-plus-char-secret
ports:
- "8081:8080"
- "8080:8080"
volumes:
postgres_data:

6
helm/go-mad/Chart.lock Normal file
View file

@ -0,0 +1,6 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 16.7.27
digest: sha256:e8b999a3aaad6328a13d0c666207650897b69c61367c947d363b2acbe0ea1b42
generated: "2026-10-04T17:23:41.006015619+02:00"

View file

@ -4,3 +4,8 @@ description: A web application for managing tenant database connections and migr
type: application
version: 0.1.0
appVersion: "1.0"
dependencies:
- name: postgresql
version: "16.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled

Binary file not shown.

View file

@ -3,5 +3,9 @@ go-mad
{{- end -}}
{{- define "go-mad.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride -}}
{{- else -}}
{{- .Release.Name -}}
{{- end -}}
{{- end -}}

View file

@ -1,5 +1,6 @@
{{- $fullName := include "go-mad.fullname" . }}
{{- $values := index .Values "go-mad" }}
{{- $pgName := printf "%s-postgresql" .Release.Name }}
apiVersion: apps/v1
kind: Deployment
@ -29,7 +30,7 @@ spec:
- containerPort: {{ $values.serverPort | int }}
env:
- name: GO_MAD_DATABASE_URL
value: "{{ $values.databaseURL }}"
value: "postgres://mad:{{ .Values.postgresql.auth.password }}@{{ $pgName }}:5432/mad?sslmode=disable"
- name: GO_MAD_SERVER_PORT
value: "{{ $values.serverPort }}"
- name: GO_MAD_JWT_SECRET

View file

@ -0,0 +1,38 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "go-mad.fullname" . }}
{{- $firstHost := first .Values.ingress.hosts -}}
{{- $firstPath := first $firstHost.paths -}}
{{- $ingressPath := $firstPath.path -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
app.kubernetes.io/name: {{ include "go-mad.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ $fullName }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,61 @@
# Development values for go-mad
replicaCount: 1
image:
repository: batty1039.startdedicated.net:5000/go-mad
pullPolicy: Always
tag: "latest"
service:
type: ClusterIP
port: 8080
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
postgresql:
enabled: true
image:
registry: docker.io
repository: bitnami/postgresql
tag: latest
pullPolicy: IfNotPresent
auth:
username: mad
password: mad
database: mad
primary:
persistence:
enabled: true
size: 2Gi
go-mad:
serverPort: 8080
jwtSecret: "dev-jwt-secret-change-me-to-32-plus-chars"
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: mad-dev.urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-mad-tls
hosts:
- mad-dev.urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer

View file

@ -0,0 +1,60 @@
# Local (LCL) values for go-mad.
# Deployed to namespace go-insane-lcl, served at https://mad-lcl.intern-urpi.be
# by the docker TLS edge (dev-infrastructure/config/traefik/tls.yaml).
replicaCount: 1
image:
repository: localhost:5000/go-mad
pullPolicy: IfNotPresent
tag: "0.0.1"
service:
type: ClusterIP
port: 8080
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
postgresql:
enabled: true
image:
registry: docker.io
repository: bitnami/postgresql
tag: latest
pullPolicy: IfNotPresent
auth:
username: mad
password: mad
database: mad
primary:
persistence:
enabled: true
size: 2Gi
go-mad:
serverPort: 8080
jwtSecret: "lcl-jwt-secret-change-me-to-32-plus-chars"
ingress:
enabled: true
className: traefik
annotations: {}
hosts:
- host: mad-lcl.intern-urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: local-dev-cert
hosts:
- mad-lcl.intern-urpi.be
# No cert-manager in the local cluster. The local CA certificate is provided
# through the local-dev-cert secret (dev-infrastructure/k8s).
certManager:
enabled: false

View file

@ -0,0 +1,68 @@
# Production values for go-mad
replicaCount: 2
image:
repository: batty1039.startdedicated.net:5000/go-mad
pullPolicy: Always
tag: "latest"
service:
type: ClusterIP
port: 8080
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
postgresql:
enabled: true
image:
registry: docker.io
repository: bitnami/postgresql
tag: latest
pullPolicy: IfNotPresent
auth:
username: mad
password: mad
database: mad
primary:
persistence:
enabled: true
size: 20Gi
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 512Mi
go-mad:
serverPort: 8080
jwtSecret: "" # Set via --set or secret
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: mad.urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-mad-tls
hosts:
- mad.urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer

View file

@ -0,0 +1,61 @@
# TST values for go-mad
replicaCount: 1
image:
repository: batty1039.startdedicated.net:5000/go-mad
pullPolicy: Always
tag: "latest"
service:
type: ClusterIP
port: 8080
resources:
limits:
cpu: 750m
memory: 384Mi
requests:
cpu: 150m
memory: 256Mi
postgresql:
enabled: true
image:
registry: docker.io
repository: bitnami/postgresql
tag: latest
pullPolicy: IfNotPresent
auth:
username: mad
password: mad
database: mad
primary:
persistence:
enabled: true
size: 5Gi
go-mad:
serverPort: 8080
jwtSecret: "tst-jwt-secret-change-me"
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: mad-tst.intern-urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-mad-tls
hosts:
- mad-tst.intern-urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer

View file

@ -1,8 +1,11 @@
# Default values for go-mad
# This is a base configuration - use values-dev.yaml or values-prd.yaml for deployment
replicaCount: 1
image:
repository: batty1039.startdedicated.net:5000/go-mad
pullPolicy: IfNotPresent
pullPolicy: Always
tag: "latest"
service:
@ -17,37 +20,48 @@ resources:
cpu: 100m
memory: 128Mi
database:
enabled: true
# Database configuration (uses bitnami/postgresql subchart)
postgresql:
enabled: true
image:
registry: docker.io
repository: bitnami/postgresql
tag: latest
pullPolicy: IfNotPresent
auth:
username: mad
password: mad
database: mad
primary:
persistence:
enabled: true
size: 8Gi
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-related: ...
hosts:
- host: go-mad.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: go-mad-tls
# hosts:
# - go-mad.local
# Application configuration
go-mad:
databaseURL: "postgres://mad:mad@go-guess-postgresql:5432/mad?sslmode=disable"
serverPort: 8080
jwtSecret: "change-me-to-a-32-plus-char-secret"
web:
# Ingress configuration
ingress:
enabled: true
type: serving
port: 8080
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: mad.urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-mad-tls
hosts:
- mad.urpi.be
# Cert-manager configuration
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer

View file

@ -57,14 +57,14 @@ func FindMigrations(migrationPath string) ([]Migration, error) {
}
contentStr := string(content)
migration := parseMigration(contentStr)
migration := parseMigration(contentStr, entry.Name())
migrations = append(migrations, migration)
}
return migrations, nil
}
func parseMigration(content string) Migration {
func parseMigration(content string, filename string) Migration {
// Parse goose-style migration: -- +goose Up / -- +goose Down
lines := strings.Split(content, "\n")
var upSQL strings.Builder
@ -90,9 +90,14 @@ func parseMigration(content string) Migration {
// Extract version from filename (first part before _)
version := ""
desc := ""
if idx := strings.Index(content, "_"); idx >= 0 {
version = content[:idx]
desc = content[idx+1 : strings.Index(content, "_")] // simplistic
if idx := strings.Index(filename, "_"); idx >= 0 {
version = filename[:idx]
remaining := filename[idx+1:]
if idx2 := strings.Index(remaining, "_"); idx2 >= 0 {
desc = remaining[:idx2]
} else {
desc = strings.TrimSuffix(remaining, ".sql")
}
}
return Migration{

View file

@ -0,0 +1,34 @@
package handler
import (
"database/sql"
"log"
"net/http"
)
type Handler struct {
db *sql.DB
}
func New(db *sql.DB) *Handler {
log.Println("Handler routes registered")
return &Handler{
db: db,
}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"status":"ok"}`))
case "/api/v1/dashboard":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"tenants":[],"applications":[],"databases":[],"is_system_administrator":false,"selected_tenant":""}`))
default:
http.NotFound(w, r)
}
}

View file

@ -4,10 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go-mad - Tenant Database Manager</title>
<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/static/index.jsx"></script>
<script type="module" src="/main.tsx"></script>
</body>
</html>

951
web/package-lock.json generated Normal file
View file

@ -0,0 +1,951 @@
{
"name": "web",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"zustand": "^5.0.15"
},
"devDependencies": {
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"vite": "^8.3.2"
}
},
"node_modules/@oxc-project/types": {
"version": "0.152.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz",
"integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz",
"integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz",
"integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz",
"integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz",
"integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz",
"integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz",
"integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz",
"integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz",
"integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz",
"integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz",
"integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz",
"integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz",
"integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz",
"integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz",
"integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz",
"integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.3.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
"integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"oxc-transform-react": "^0.145.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"oxc-transform-react": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT",
"peer": true
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.19",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
"integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.18",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.28.0"
},
"peerDependencies": {
"react": "^19.3.0"
}
},
"node_modules/rolldown": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz",
"integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.152.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.12",
"@rolldown/binding-android-arm64": "1.2.12",
"@rolldown/binding-darwin-arm64": "1.2.12",
"@rolldown/binding-darwin-x64": "1.2.12",
"@rolldown/binding-freebsd-x64": "1.2.12",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.12",
"@rolldown/binding-linux-arm64-gnu": "1.2.12",
"@rolldown/binding-linux-arm64-musl": "1.2.12",
"@rolldown/binding-linux-ppc64-gnu": "1.2.12",
"@rolldown/binding-linux-s390x-gnu": "1.2.12",
"@rolldown/binding-linux-x64-gnu": "1.2.12",
"@rolldown/binding-linux-x64-musl": "1.2.12",
"@rolldown/binding-openharmony-arm64": "1.2.12",
"@rolldown/binding-win32-arm64-msvc": "1.2.12",
"@rolldown/binding-win32-x64-msvc": "1.2.12"
}
},
"node_modules/scheduler": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vite": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz",
"integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.28",
"rolldown": "~1.2.11",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.7.1",
"esbuild": "^0.27.0 || ^0.28.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
},
"@vitejs/devtools": {
"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
}
}
},
"node_modules/zustand": {
"version": "5.0.15",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
"integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
"license": "MIT",
"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
}
}
}
}
}

25
web/package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "web",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"zustand": "^5.0.15"
},
"devDependencies": {
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"vite": "^8.3.2"
}
}

View file

@ -1,6 +1,3 @@
import React, { useState, useEffect } from 'react';
import { escapeHTML } from './App.utils';
const state = {
tenants: [],
applications: [],

View file

@ -1,9 +0,0 @@
import './index.css';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

110
web/src/main.tsx Normal file
View file

@ -0,0 +1,110 @@
import { create } from 'zustand'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
const root = document.getElementById('root')
if (!root) {
console.error('Root element not found')
process.exit(1)
}
const client = ReactDOM.createRoot(root)
// Define types for the dashboard data
interface DashboardData {
tenants: Array<{ id: string; name: string; role: string; slug: string }>
applications: Array<{ id: string; name: string; description: string; tenant_id: string; client_ready: boolean; client_id: string }>
databases: Array<{ id: string; name: string; connection_string: string; tenant_id: string; migration_status: string }>
is_system_administrator: boolean
selected_tenant: string
}
interface State {
tenants: DashboardData['tenants']
applications: DashboardData['applications']
databases: DashboardData['databases']
selectedTenant: string
isSystemAdministrator: boolean
setState: (partial: Partial<DashboardData>) => void
}
const useStore = create<State>((set) => ({
tenants: [],
applications: [],
databases: [],
selectedTenant: '',
isSystemAdministrator: false,
setState: (partial) => set(partial),
}))
// Load dashboard data
fetch('/api/v1/dashboard')
.then((res) => {
if (!res.ok) throw new Error('API error: ' + res.status)
return res.json()
})
.then((data: DashboardData) => {
useStore.getState().setState(data)
})
.catch((err) => {
console.error('Failed to load dashboard:', err)
})
// Render the React app
const App = () => {
const { tenants, applications, databases, selectedTenant, isSystemAdministrator, setState } = useStore.getState()
const render = () => {
const tenantCount = document.getElementById('tenant-count')
const appCount = document.getElementById('app-count')
const dbCount = document.getElementById('db-count')
if (tenantCount) tenantCount.textContent = tenants.length
if (appCount) appCount.textContent = applications.length
if (dbCount) dbCount.textContent = databases.length
const tenantList = document.getElementById('tenant-list')
if (tenantList) {
tenantList.innerHTML = tenants
.map(
(i) =>
`<div class="card"><span class="badge">${i.role || ''}</span><h3>${i.name || ''}</h3><p>${i.slug || ''}</p></div>`
)
.join('') || '<p>No tenants yet</p>'
}
const appList = document.getElementById('app-list')
if (appList) {
const filteredApps = applications.filter(
(a) => a.tenant_id === selectedTenant
)
appList.innerHTML = filteredApps
.map(
(i) =>
`<div class="card"><span class="badge">${i.tenant_slug || ''}</span><h3>${i.name || ''}</h3><p>${i.description || 'No description'}</p></div>`
)
.join('') || '<p>No applications yet</p>'
}
const dbList = document.getElementById('db-list')
if (dbList) {
const filteredDBs = databases.filter((d) => d.tenant_id === selectedTenant)
dbList.innerHTML = filteredDBs
.map(
(i) =>
`<div class="card"><h3>${i.name || ''}</h3><p>${i.connection_string ? 'Connected' : 'Not connected'}</p></div>`
)
.join('') || '<p>No databases yet</p>'
}
}
// Initial render
render()
// Set up event listeners for any interactive elements
return null
}
ReactDOM.createRoot(root).render(<App />)

6
web/vite.config.js Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
export default defineConfig({
base: '',
publicDir: 'src',
})