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
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
*.cert
|
*.cert
|
||||||
|
/bin/
|
||||||
|
.idea
|
||||||
|
|
@ -8,13 +8,15 @@ RUN go mod download
|
||||||
COPY cmd/ /src/cmd/
|
COPY cmd/ /src/cmd/
|
||||||
COPY internal/ /src/internal/
|
COPY internal/ /src/internal/
|
||||||
COPY web/ /src/web/
|
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
|
RUN go build -o /out/go-mad ./cmd/go-mad
|
||||||
|
|
||||||
FROM gcr.io/distroless/static-debian12:nonroot
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
COPY --from=builder /out/go-mad /go-mad
|
COPY --from=builder /out/go-mad /go-mad
|
||||||
|
COPY --from=builder /src/web /src/web
|
||||||
|
COPY --from=builder /internal/migration /internal/migration
|
||||||
|
|
||||||
EXPOSE 8080
|
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
|
GO_VERSION ?= 1.27.1
|
||||||
BINARY_NAME ?= go-mad
|
BINARY_NAME ?= go-mad
|
||||||
|
|
@ -19,7 +19,7 @@ build: fmt vet ## Build the Go binary
|
||||||
## ---- Development ----
|
## ---- Development ----
|
||||||
|
|
||||||
run: build ## Run the Go binary (requires postgres running)
|
run: build ## Run the Go binary (requires postgres running)
|
||||||
@$(BINARY_NAME)
|
@$(BINARY_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
compose-up: ## Start docker compose (postgres + go-mad)
|
compose-up: ## Start docker compose (postgres + go-mad)
|
||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
|
|
@ -48,12 +48,10 @@ integration-test: ## Run integration tests (requires test database)
|
||||||
## ---- Web Frontend ----
|
## ---- Web Frontend ----
|
||||||
|
|
||||||
web-build: ## Build React frontend
|
web-build: ## Build React frontend
|
||||||
npm install
|
cd web && npm install && npm run build
|
||||||
npm run build
|
|
||||||
|
|
||||||
web-dev: ## Run React dev server
|
web-dev: ## Start the go-mad application with Docker Compose
|
||||||
npm install
|
@docker compose up --build -d
|
||||||
npm run dev
|
|
||||||
|
|
||||||
## ---- Docker ----
|
## ---- Docker ----
|
||||||
|
|
||||||
|
|
@ -75,24 +73,19 @@ help: ## Show this help message
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Build targets:"
|
@echo "Build targets:"
|
||||||
@echo " build Build the Go binary"
|
@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-up Start docker compose"
|
||||||
@echo " compose-down Stop docker compose"
|
@echo " compose-down Stop docker compose"
|
||||||
@echo " compose-logs View docker compose logs"
|
@echo " compose-logs View docker compose logs"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Code quality:"
|
@echo "Web frontend:"
|
||||||
@echo " fmt Format Go code"
|
@echo " web-build Build React frontend"
|
||||||
@echo " vet Vet Go code"
|
@echo " web-dev Start go-mad with docker compose"
|
||||||
@echo " test Run Go unit tests"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker:"
|
@echo "Docker:"
|
||||||
@echo " docker-build Build docker image"
|
@echo " docker-build Build docker image"
|
||||||
@echo " docker-push Push docker image"
|
@echo " docker-push Push docker image"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Web frontend:"
|
|
||||||
@echo " web-build Build React frontend"
|
|
||||||
@echo " web-dev Run React dev server"
|
|
||||||
@echo ""
|
|
||||||
@echo "Other:"
|
@echo "Other:"
|
||||||
@echo " clean Remove build artifacts"
|
@echo " clean Remove build artifacts"
|
||||||
@echo " help Show this help message"
|
@echo " help Show this help message"
|
||||||
BIN
bin/go-mad
BIN
bin/go-mad
Binary file not shown.
|
|
@ -1,13 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/dnoulet/go-mad/internal/config"
|
"github.com/dnoulet/go-mad/internal/config"
|
||||||
"github.com/dnoulet/go-mad/internal/database"
|
"github.com/dnoulet/go-mad/internal/database"
|
||||||
|
"github.com/dnoulet/go-mad/internal/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -22,21 +25,39 @@ func main() {
|
||||||
}
|
}
|
||||||
defer db.Close()
|
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)
|
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
|
// Set up signal handling for graceful shutdown
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
// Start server
|
// Start HTTP server
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("Starting server on %s:%s", cfg.Server.Host, cfg.Server.Port)
|
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
|
<-stop
|
||||||
log.Println("Shutting down gracefully...")
|
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:
|
services:
|
||||||
go-mad-database:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: go-mad-postgres
|
container_name: go-mad-postgres
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -20,15 +20,13 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: go-mad
|
container_name: go-mad
|
||||||
depends_on:
|
depends_on:
|
||||||
go-mad-database:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
extra_hosts:
|
|
||||||
- "go-mad-database:host-gateway"
|
|
||||||
environment:
|
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_SERVER_PORT: "8080"
|
||||||
GO_MAD_JWT_SECRET: change-me-to-a-32-plus-char-secret
|
GO_MAD_JWT_SECRET: change-me-to-a-32-plus-char-secret
|
||||||
ports:
|
ports:
|
||||||
- "8081:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
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
|
type: application
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
appVersion: "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 -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "go-mad.fullname" -}}
|
{{- define "go-mad.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride -}}
|
||||||
|
{{- .Values.fullnameOverride -}}
|
||||||
|
{{- else -}}
|
||||||
{{- .Release.Name -}}
|
{{- .Release.Name -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{{- $fullName := include "go-mad.fullname" . }}
|
{{- $fullName := include "go-mad.fullname" . }}
|
||||||
{{- $values := index .Values "go-mad" }}
|
{{- $values := index .Values "go-mad" }}
|
||||||
|
{{- $pgName := printf "%s-postgresql" .Release.Name }}
|
||||||
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
|
|
@ -29,7 +30,7 @@ spec:
|
||||||
- containerPort: {{ $values.serverPort | int }}
|
- containerPort: {{ $values.serverPort | int }}
|
||||||
env:
|
env:
|
||||||
- name: GO_MAD_DATABASE_URL
|
- 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
|
- name: GO_MAD_SERVER_PORT
|
||||||
value: "{{ $values.serverPort }}"
|
value: "{{ $values.serverPort }}"
|
||||||
- name: GO_MAD_JWT_SECRET
|
- 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
|
replicaCount: 1
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: batty1039.startdedicated.net:5000/go-mad
|
repository: batty1039.startdedicated.net:5000/go-mad
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: Always
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
|
|
||||||
service:
|
service:
|
||||||
|
|
@ -17,37 +20,48 @@ resources:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 128Mi
|
memory: 128Mi
|
||||||
|
|
||||||
database:
|
# Database configuration (uses bitnami/postgresql subchart)
|
||||||
enabled: true
|
postgresql:
|
||||||
postgresql:
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
image:
|
||||||
|
registry: docker.io
|
||||||
|
repository: bitnami/postgresql
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
auth:
|
auth:
|
||||||
username: mad
|
username: mad
|
||||||
password: mad
|
password: mad
|
||||||
database: mad
|
database: mad
|
||||||
|
primary:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 8Gi
|
||||||
|
|
||||||
ingress:
|
# Application configuration
|
||||||
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
|
|
||||||
|
|
||||||
go-mad:
|
go-mad:
|
||||||
databaseURL: "postgres://mad:mad@go-guess-postgresql:5432/mad?sslmode=disable"
|
|
||||||
serverPort: 8080
|
serverPort: 8080
|
||||||
jwtSecret: "change-me-to-a-32-plus-char-secret"
|
jwtSecret: "change-me-to-a-32-plus-char-secret"
|
||||||
|
|
||||||
web:
|
# Ingress configuration
|
||||||
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
type: serving
|
className: nginx
|
||||||
port: 8080
|
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)
|
contentStr := string(content)
|
||||||
migration := parseMigration(contentStr)
|
migration := parseMigration(contentStr, entry.Name())
|
||||||
migrations = append(migrations, migration)
|
migrations = append(migrations, migration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrations, nil
|
return migrations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMigration(content string) Migration {
|
func parseMigration(content string, filename string) Migration {
|
||||||
// Parse goose-style migration: -- +goose Up / -- +goose Down
|
// Parse goose-style migration: -- +goose Up / -- +goose Down
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
var upSQL strings.Builder
|
var upSQL strings.Builder
|
||||||
|
|
@ -90,9 +90,14 @@ func parseMigration(content string) Migration {
|
||||||
// Extract version from filename (first part before _)
|
// Extract version from filename (first part before _)
|
||||||
version := ""
|
version := ""
|
||||||
desc := ""
|
desc := ""
|
||||||
if idx := strings.Index(content, "_"); idx >= 0 {
|
if idx := strings.Index(filename, "_"); idx >= 0 {
|
||||||
version = content[:idx]
|
version = filename[:idx]
|
||||||
desc = content[idx+1 : strings.Index(content, "_")] // simplistic
|
remaining := filename[idx+1:]
|
||||||
|
if idx2 := strings.Index(remaining, "_"); idx2 >= 0 {
|
||||||
|
desc = remaining[:idx2]
|
||||||
|
} else {
|
||||||
|
desc = strings.TrimSuffix(remaining, ".sql")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Migration{
|
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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>go-mad - Tenant Database Manager</title>
|
<title>go-mad - Tenant Database Manager</title>
|
||||||
<link rel="stylesheet" href="/static/index.css">
|
<link rel="stylesheet" href="/index.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/static/index.jsx"></script>
|
<script type="module" src="/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 = {
|
const state = {
|
||||||
tenants: [],
|
tenants: [],
|
||||||
applications: [],
|
applications: [],
|
||||||
|
|
@ -36,7 +33,7 @@ async function api(path, options = {}) {
|
||||||
const response = await fetch(path, {
|
const response = await fetch(path, {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
...options,
|
...options,
|
||||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }
|
headers: {'Content-Type': 'application/json', ...(options.headers || {})}
|
||||||
});
|
});
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
location.href = '/login';
|
location.href = '/login';
|
||||||
|
|
@ -139,7 +136,7 @@ $$('.tab').forEach(button => button.addEventListener('click', () => {
|
||||||
$('#tenant-form').addEventListener('submit', async event => {
|
$('#tenant-form').addEventListener('submit', async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
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.closest('dialog').close();
|
||||||
event.target.reset();
|
event.target.reset();
|
||||||
notice('Tenant created');
|
notice('Tenant created');
|
||||||
|
|
@ -152,7 +149,7 @@ $('#tenant-form').addEventListener('submit', async event => {
|
||||||
$('#app-form').addEventListener('submit', async event => {
|
$('#app-form').addEventListener('submit', async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
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.closest('dialog').close();
|
||||||
event.target.reset();
|
event.target.reset();
|
||||||
notice('Application registered');
|
notice('Application registered');
|
||||||
|
|
@ -165,7 +162,7 @@ $('#app-form').addEventListener('submit', async event => {
|
||||||
$('#db-form').addEventListener('submit', async event => {
|
$('#db-form').addEventListener('submit', async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
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.closest('dialog').close();
|
||||||
event.target.reset();
|
event.target.reset();
|
||||||
notice('Database connection added');
|
notice('Database connection added');
|
||||||
|
|
@ -197,7 +194,7 @@ document.addEventListener('click', async event => {
|
||||||
if (dbID) {
|
if (dbID) {
|
||||||
if (confirm('Run migrations for this database?')) {
|
if (confirm('Run migrations for this database?')) {
|
||||||
try {
|
try {
|
||||||
await api(`/api/v1/databases/${dbID}/migrations`, { method: 'POST' });
|
await api(`/api/v1/databases/${dbID}/migrations`, {method: 'POST'});
|
||||||
notice('Migrations executed');
|
notice('Migrations executed');
|
||||||
await load();
|
await load();
|
||||||
} catch (error) {
|
} 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