initial commit
This commit is contained in:
parent
ebd5bb09fd
commit
3046604b64
26 changed files with 1535 additions and 82 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ dist/
|
|||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
/bin/
|
||||
.idea
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
25
Makefile
25
Makefile
|
|
@ -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"
|
||||
BIN
bin/go-mad
BIN
bin/go-mad
Binary file not shown.
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
6
helm/go-mad/Chart.lock
Normal 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"
|
||||
|
|
@ -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
|
||||
BIN
helm/go-mad/charts/postgresql-16.7.27.tgz
Normal file
BIN
helm/go-mad/charts/postgresql-16.7.27.tgz
Normal file
Binary file not shown.
|
|
@ -3,5 +3,9 @@ go-mad
|
|||
{{- end -}}
|
||||
|
||||
{{- define "go-mad.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride -}}
|
||||
{{- else -}}
|
||||
{{- .Release.Name -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
38
helm/go-mad/templates/ingress.yaml
Normal file
38
helm/go-mad/templates/ingress.yaml
Normal 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 }}
|
||||
61
helm/go-mad/values-dev.yaml
Normal file
61
helm/go-mad/values-dev.yaml
Normal 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
|
||||
60
helm/go-mad/values-lcl.yaml
Normal file
60
helm/go-mad/values-lcl.yaml
Normal 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
|
||||
68
helm/go-mad/values-prd.yaml
Normal file
68
helm/go-mad/values-prd.yaml
Normal 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
|
||||
61
helm/go-mad/values-tst.yaml
Normal file
61
helm/go-mad/values-tst.yaml
Normal 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
|
||||
|
|
@ -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
|
||||
postgresql:
|
||||
# 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
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
34
internal/handler/handler.go
Normal file
34
internal/handler/handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
951
web/package-lock.json
generated
Normal 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
25
web/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { escapeHTML } from './App.utils';
|
||||
|
||||
const state = {
|
||||
tenants: [],
|
||||
applications: [],
|
||||
|
|
@ -36,7 +33,7 @@ async function api(path, options = {}) {
|
|||
const response = await fetch(path, {
|
||||
credentials: 'same-origin',
|
||||
...options,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }
|
||||
headers: {'Content-Type': 'application/json', ...(options.headers || {})}
|
||||
});
|
||||
if (response.status === 401) {
|
||||
location.href = '/login';
|
||||
|
|
@ -139,7 +136,7 @@ $$('.tab').forEach(button => button.addEventListener('click', () => {
|
|||
$('#tenant-form').addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api('/api/v1/tenants', { method: 'POST', body: JSON.stringify(formJSON(event.target)) });
|
||||
await api('/api/v1/tenants', {method: 'POST', body: JSON.stringify(formJSON(event.target))});
|
||||
event.target.closest('dialog').close();
|
||||
event.target.reset();
|
||||
notice('Tenant created');
|
||||
|
|
@ -152,7 +149,7 @@ $('#tenant-form').addEventListener('submit', async event => {
|
|||
$('#app-form').addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api('/api/v1/applications', { method: 'POST', body: JSON.stringify(formJSON(event.target)) });
|
||||
await api('/api/v1/applications', {method: 'POST', body: JSON.stringify(formJSON(event.target))});
|
||||
event.target.closest('dialog').close();
|
||||
event.target.reset();
|
||||
notice('Application registered');
|
||||
|
|
@ -165,7 +162,7 @@ $('#app-form').addEventListener('submit', async event => {
|
|||
$('#db-form').addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api('/api/v1/databases', { method: 'POST', body: JSON.stringify(formJSON(event.target)) });
|
||||
await api('/api/v1/databases', {method: 'POST', body: JSON.stringify(formJSON(event.target))});
|
||||
event.target.closest('dialog').close();
|
||||
event.target.reset();
|
||||
notice('Database connection added');
|
||||
|
|
@ -197,7 +194,7 @@ document.addEventListener('click', async event => {
|
|||
if (dbID) {
|
||||
if (confirm('Run migrations for this database?')) {
|
||||
try {
|
||||
await api(`/api/v1/databases/${dbID}/migrations`, { method: 'POST' });
|
||||
await api(`/api/v1/databases/${dbID}/migrations`, {method: 'POST'});
|
||||
notice('Migrations executed');
|
||||
await load();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -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
110
web/src/main.tsx
Normal 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
6
web/vite.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: '',
|
||||
publicDir: 'src',
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue