diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..dc424f3 --- /dev/null +++ b/.air.toml @@ -0,0 +1,68 @@ +#:schema https://json.schemastore.org/any.json + +env_files = [] +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/red" + delay = 1000 + entrypoint = ["./tmp/main"] + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + ignore_dangerous_root_dir = false + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + + [build.windows] + args_bin = [] + bin = "tmp\\main.exe" + cmd = "go build -o ./tmp/main.exe ./cmd/red" + entrypoint = ["tmp\\main.exe"] + full_bin = "" + post_cmd = [] + pre_cmd = [] + +[color] + app = "" + build = "yellow" + main = "magenta" + mode = "" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + app_start_timeout = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore index 0ce0318..b2f2ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ Thumbs.db .vscode/ .idea/ + +# remove py venv +venv-podman + +tmp \ No newline at end of file diff --git a/cmd/red/main.go b/cmd/red/main.go index 9e71f4f..2dc494a 100644 --- a/cmd/red/main.go +++ b/cmd/red/main.go @@ -30,7 +30,6 @@ func main() { cfg = loaded } - // --- NEW: Security Token Warning --- if cfg.AdminToken == "" || cfg.AdminToken == "secret123" { log.Println("=================================================================") log.Println("⚠️ SECURITY WARNING: Using default or missing Admin Token! ⚠️") @@ -41,7 +40,6 @@ func main() { log.Println("Windows: .\\manage-token.ps1") log.Println("=================================================================") } - // ----------------------------------- // 1. Core Knowledge Base Pulling if *pull && cfg.SourceURL != "" { @@ -52,11 +50,11 @@ func main() { log.Println("fetch complete") } - // 2. Startup Sync (Ported from Legacy Gateway) + // 2. Startup Sync if len(cfg.StartupSync) > 0 { + // Calculate the absolute path based on the config (e.g., /app/data/remote) remoteDir := filepath.Join(cfg.DataDir, "remote") - // Check for permission errors right here before proceeding if err := os.MkdirAll(remoteDir, 0755); err != nil { log.Fatalf("CRITICAL: Failed to create remote directory. Check volume permissions: %v", err) } @@ -65,7 +63,16 @@ func main() { for _, sync := range cfg.StartupSync { log.Printf("Startup Sync: Fetching %s...", sync.Filename) - if err := executeSync(client, sync.URL, filepath.Join(remoteDir, sync.Filename)); err != nil { + + // Auto-convert awesome-markdown shortcut to raw GitHub URL + downloadURL := sync.URL + if downloadURL == "https://github.com/mundimark/awesome-markdown" { + downloadURL = "https://raw.githubusercontent.com/mundimark/awesome-markdown/master/README.md" + } + + // Pass the absolute target path directly to avoid double-appending "data" + targetPath := filepath.Join(remoteDir, sync.Filename) + if err := executeSync(client, downloadURL, targetPath); err != nil { log.Printf("Startup Sync Error (%s): %v", sync.Filename, err) } else { log.Printf("Startup Sync: Successfully downloaded %s", sync.Filename) @@ -79,17 +86,18 @@ func main() { log.Fatalf("store: %v", err) } - // 4. Start HTTP Server with the Refactored Router + // Start Hot Reload Watcher + if err := s.Watch(); err != nil { + log.Printf("⚠️ Warning: Could not start hot reloader: %v", err) + } + + // 4. Start HTTP Server h := router.New(s, &cfg, *cfgPath) log.Printf("RED listening on %s", cfg.Addr) log.Fatal(http.ListenAndServe(cfg.Addr, h)) } -func executeSync(client *http.Client, targetURL, destSubPath string) error { - // Reconstruct target file paths relative to data root directory - // Note: main.go has context of cfg.DataDir - - // Let's resolve the path correctly depending on the initialization parameters +func executeSync(client *http.Client, targetURL, destPath string) error { lowerURL := strings.ToLower(targetURL) if strings.HasSuffix(lowerURL, ".tar.gz") || strings.HasSuffix(lowerURL, ".zip") { @@ -97,11 +105,9 @@ func executeSync(client *http.Client, targetURL, destSubPath string) error { if strings.HasSuffix(lowerURL, ".zip") { srcType = "zip" } - // Pull the dynamic folder contents using the internal archive worker - return fetch.Pull(targetURL, srcType, filepath.Join("data", destSubPath)) + return fetch.Pull(targetURL, srcType, destPath) } - // Otherwise, proceed with single file retrieval flow req, err := http.NewRequest(http.MethodGet, targetURL, nil) if err != nil { return err @@ -118,12 +124,12 @@ func executeSync(client *http.Client, targetURL, destSubPath string) error { return os.ErrPermission } - fullFilePath := filepath.Join("data", destSubPath) - if err := os.MkdirAll(filepath.Dir(fullFilePath), 0755); err != nil { + // Use destPath exactly as provided, removing the hardcoded "data" string + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { return err } - outFile, err := os.Create(fullFilePath) + outFile, err := os.Create(destPath) if err != nil { return err } diff --git a/docker-compose.yml b/docker-compose.yml index eafe97d..d2a5afc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ networks: services: red_engine: + build: . image: red-engine-image container_name: red_engine_node restart: unless-stopped @@ -20,8 +21,8 @@ services: container_name: caddy_proxy restart: unless-stopped ports: - - "80:80" - - "443:443" + - "8080:80" + - "8443:443" networks: - clearnet-tier volumes: diff --git a/go.mod b/go.mod index 7183ad8..7474651 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,18 @@ module github.com/RED-Collective/red-engine go 1.26.2 -require github.com/yuin/goldmark v1.8.2 +require ( + github.com/microcosm-cc/bluemonday v1.0.27 + github.com/yuin/goldmark v1.8.2 +) + +require ( + github.com/fsnotify/fsnotify v1.10.1 // direct + golang.org/x/sys v0.45.0 // indirect +) require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // direct - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.55.0 // indirect ) diff --git a/go.sum b/go.sum index 884e507..fc77dd8 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/install-red-engine.ps1 b/install-red-engine.ps1 index 9ca1088..88e4276 100644 --- a/install-red-engine.ps1 +++ b/install-red-engine.ps1 @@ -1,5 +1,5 @@ Write-Host "========================================" -ForegroundColor Cyan -Write-Host "🚀 Installing RED Engine..." -ForegroundColor Cyan +Write-Host "🚀 Installing RED Engine (Production Mode)..." -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan if (-Not (Test-Path "docker-compose.yml")) { diff --git a/install-red-engine.sh b/install-red-engine.sh index 34b7887..3541a4c 100755 --- a/install-red-engine.sh +++ b/install-red-engine.sh @@ -1,27 +1,12 @@ #!/bin/bash echo "========================================" -echo "🚀 Installing RED Engine..." +echo "🚀 Installing RED Engine (Production Mode)..." echo "========================================" if [ ! -f "docker-compose.yml" ]; then - echo "[*] Repository not detected in current directory." - - if ! command -v git &> /dev/null; then - echo "❌ Error: 'git' is not installed. Please install git to continue." - exit 1 - fi - echo "[*] Cloning RED Engine repository..." git clone https://github.com/RED-Collective/red-engine.git - if [ $? -ne 0 ]; then - echo "❌ Error: Failed to clone repository." - exit 1 - fi - - echo "[*] Navigating into red-engine directory..." cd red-engine || exit 1 -else - echo "[*] Running from inside existing repository." fi if [ ! -d "./data" ]; then @@ -34,7 +19,6 @@ fi if [ ! -f "config.json" ]; then echo "[*] Generating default config.json..." NEW_TOKEN=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1) - cat < config.json { "addr": ":8080", @@ -44,19 +28,16 @@ if [ ! -f "config.json" ]; then "startupSync": [] } EOF - echo "[*] Generated secure Admin Token: $NEW_TOKEN" - echo "⚠️ PLEASE SAVE THIS TOKEN! You will need it to log in to the admin panel." -else - echo "[*] config.json already exists. Skipping default generation." + echo "[*] Generated Admin Token: $NEW_TOKEN" fi if [ ! -f "contributors.json" ]; then - echo "[*] Generating default contributors.json..." echo "[]" > contributors.json else echo "[*] contributors.json already exists." fi +# 5. Detect the container engine if command -v podman-compose &> /dev/null; then COMPOSE_CMD="podman-compose up --build -d" elif command -v docker-compose &> /dev/null; then @@ -72,23 +53,6 @@ fi echo "[*] Starting RED Engine using container engine..." $COMPOSE_CMD -if [ $? -ne 0 ]; then - echo "❌ Error: Failed to start containers." - exit 1 -fi - -CONFIG_PORT=$(grep '"addr"' config.json | sed -E 's/.*:([0-9]+).*/\1/') - -if [ -z "$CONFIG_PORT" ]; then - CONFIG_PORT="8080" -fi - -HOST_IP="localhost" -if command -v hostname &> /dev/null; then - HOST_IP=$(hostname -I | awk '{print $1}') - [ -z "$HOST_IP" ] && HOST_IP="localhost" -fi - echo "========================================" echo "✅ Installation Complete!" echo "🌐 Your node is running at: http://${HOST_IP}:${CONFIG_PORT}" diff --git a/internal/router/.gitignore b/internal/router/.gitignore new file mode 100644 index 0000000..a1b6b7b --- /dev/null +++ b/internal/router/.gitignore @@ -0,0 +1,3 @@ +# For tailwind + +node_modules \ No newline at end of file diff --git a/internal/router/package-lock.json b/internal/router/package-lock.json new file mode 100644 index 0000000..aa4eb3e --- /dev/null +++ b/internal/router/package-lock.json @@ -0,0 +1,1041 @@ +{ + "name": "router", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", + "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "enhanced-resolve": "^5.21.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.0" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", + "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/internal/router/package.json b/internal/router/package.json new file mode 100644 index 0000000..56a64f7 --- /dev/null +++ b/internal/router/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19" + } +} diff --git a/internal/router/running_tailwind.md b/internal/router/running_tailwind.md new file mode 100644 index 0000000..d5610af --- /dev/null +++ b/internal/router/running_tailwind.md @@ -0,0 +1,21 @@ +# How to run tailwind and update other files here + +_(for contributors)_ + +See: https://tailwindcss.com/docs/installation/tailwind-cli + +Setup: +Get node and npm + +``` +npm install +``` + +The above will install all the packages for tailwind and tailwind typography. +Make sure that node is on your system's path if you're on Windows. + +Run the following in the router folder: + +``` +npx @tailwindcss/cli -i ./static/main.css -o ./static/render.css --watch +``` diff --git a/internal/router/serve.go b/internal/router/serve.go index d2556cd..36072c0 100644 --- a/internal/router/serve.go +++ b/internal/router/serve.go @@ -68,12 +68,22 @@ func (h *handler) serve(w http.ResponseWriter, r *http.Request) { d.Body = template.HTML(sectionHTML(sec)) default: - art := h.store.Get(path) + // Clean the incoming URL + cleanPath := strings.TrimSuffix(path, ".md") + art := h.store.Get(cleanPath) + + // Fallback lookup if exact match failed + if art == nil { + art = h.store.Get(path) + } + if art == nil { http.NotFound(w, r) return } + d.Title = capitalize(parts[len(parts)-1]) + d.Title = strings.TrimSuffix(d.Title, ".md") // Remove .md from the UI title d.Crumb = buildCrumbs(parts) d.Body = art.Body d.Verified = art.Verified diff --git a/internal/router/static/article.css b/internal/router/static/article.css deleted file mode 100644 index da1c9c7..0000000 --- a/internal/router/static/article.css +++ /dev/null @@ -1,161 +0,0 @@ -.article-content { - font-size: 1rem; - line-height: 1.8; - color: #334155; -} - -.article-content h1 { - font-size: 2.25rem; - font-weight: 700; - color: #0f172a; - margin-bottom: 1rem; - letter-spacing: -0.02em; -} - -.article-content h2 { - font-size: 1.5rem; - font-weight: 600; - color: #0f172a; - margin-top: 2.5rem; - margin-bottom: 1rem; - border-bottom: 1px solid var(--border); - padding-bottom: 0.5rem; -} - -.article-content a { - color: var(--primary); - text-decoration: none; - font-weight: 500; -} - -.article-content a:hover { - text-decoration: underline; -} - -.article-content p { - margin-bottom: 1.25rem; -} - -.article-content ul, -.article-content ol { - margin-bottom: 1.25rem; - padding-left: 1.5rem; -} - -.article-content li { - margin-bottom: 0.5rem; -} - -/* Beautiful Code Blocks */ -.article-content pre { - border-radius: 8px; - margin: 1.5rem 0; - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); -} - -.article-content code { - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 0.875rem; -} - -.article-content p code, -.article-content li code { - background: #f1f5f9; - color: #c53030; - padding: 0.2em 0.4em; - border-radius: 4px; -} - -.article-content table { - width: 100%; - border-collapse: collapse; - margin: 2rem 0; -} - -.article-content pre { - border-radius: 8px; - margin: 1.5rem 0; - padding: 1.25rem; /* NEW: Adds breathing room inside the box */ - background: #f8fafc; /* NEW: Light background for code blocks */ - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); - overflow-x: auto; /* FIX: Contains long text to a scrollable box */ - max-width: 100%; /* FIX: Prevents breaking the page width */ -} - -.article-content td { - padding: 12px; - border-bottom: 1px solid var(--border); -} - -.article-content blockquote { - border-left: 4px solid var(--primary); - background: #f8fafc; - margin: 1.5rem 0; - padding: 1rem 1.5rem; - border-radius: 0 8px 8px 0; - font-style: italic; -} -/* Security Header Badges */ -.security-header { - margin-bottom: 2rem; - padding: 1rem 1.5rem; - border-bottom: 2px solid var(--border); - background: #fafbfc; - border-radius: 8px; -} - -.badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-radius: 8px; - font-size: 0.9rem; - font-weight: 600; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -.badge.verified { - background: linear-gradient(135deg, #dcfce7 0%, #d1fae5 100%); - color: #166534; - border: 1px solid #86efac; -} - -.badge.unverified { - background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); - color: #991b1b; - border: 1px solid #f87171; -} - -/* SHA-256 Footer */ -.hash-footer { - margin-top: 4rem; - padding: 1.5rem; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - border: 2px solid #e2e8f0; - border-radius: 12px; - display: flex; - flex-direction: column; - gap: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); -} - -.hash-label { - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - color: #64748b; - letter-spacing: 0.05em; -} - -.hash-string { - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 0.85rem; - color: #0f172a; - word-break: break-all; - background: white; - padding: 0.75rem; - border-radius: 6px; - border: 1px solid #e2e8f0; - user-select: all; -} diff --git a/internal/router/static/layout.css b/internal/router/static/layout.css deleted file mode 100644 index 1552896..0000000 --- a/internal/router/static/layout.css +++ /dev/null @@ -1,83 +0,0 @@ -:root { - --bg-main: #ffffff; - --bg-sidebar: #f8fafc; - --bg-topbar: #ffffff; - --text-main: #0f172a; - --text-muted: #64748b; - --border: #e2e8f0; - --primary: #ff4757; - --primary-hover: #ff6b81; -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - background: var(--bg-main); - color: var(--text-main); -} - -#topbar { - position: sticky; - top: 0; - z-index: 100; - height: 60px; - background: var(--bg-topbar); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 24px; -} - -#wordmark { - display: flex; - align-items: center; - font-size: 1.1rem; - font-weight: 700; - color: var(--text-main); - text-decoration: none; -} - -.search-placeholder { - background: #f1f5f9; - border: 1px solid var(--border); - color: var(--text-muted); - padding: 6px 16px; - border-radius: 6px; - font-size: 0.85rem; -} - -#wrap { - display: flex; - min-height: calc(100vh - 60px); -} - -#main { - flex: 1; - min-width: 0; - padding: 40px 48px; - max-width: 960px; - margin: 0 auto; -} - -.breadcrumb { - font-size: 0.85rem; - color: var(--text-muted); - margin-bottom: 24px; - display: flex; - align-items: center; - gap: 8px; -} - -.breadcrumb a { - color: var(--text-muted); - text-decoration: none; - font-weight: 500; -} - -.breadcrumb a:hover { - color: var(--primary); -} diff --git a/internal/router/static/main.css b/internal/router/static/main.css new file mode 100644 index 0000000..1c4d2a8 --- /dev/null +++ b/internal/router/static/main.css @@ -0,0 +1,2 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; diff --git a/internal/router/static/nav.css b/internal/router/static/nav.css deleted file mode 100644 index fc37a09..0000000 --- a/internal/router/static/nav.css +++ /dev/null @@ -1,52 +0,0 @@ -#sidebar { - width: 260px; - flex-shrink: 0; - background: var(--bg-sidebar); - border-right: 1px solid var(--border); - padding: 24px 16px; - position: sticky; - top: 60px; - height: calc(100vh - 60px); - overflow-y: auto; -} - -.nav-heading { - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - margin-top: 1.5rem; - margin-bottom: 0.5rem; - padding: 0 8px; -} - -.nav-heading:first-child { - margin-top: 0; -} - -.nav-sub-heading { - font-size: 0.8rem; - font-weight: 600; - color: var(--text-main); - margin: 12px 0 4px 16px; -} - -.nav-item { - display: block; - font-size: 0.9rem; - color: var(--text-muted); - padding: 6px 8px; - border-radius: 6px; - text-decoration: none; - transition: all 0.15s ease; -} - -.nav-item:hover { - background: #e2e8f0; - color: var(--text-main); -} - -.sub-item { - padding-left: 24px; -} diff --git a/internal/router/static/render.css b/internal/router/static/render.css new file mode 100644 index 0000000..fc5c8c1 --- /dev/null +++ b/internal/router/static/render.css @@ -0,0 +1,1335 @@ +/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-red-900: oklch(39.6% 0.141 25.723); + --color-red-950: oklch(25.8% 0.092 26.042); + --color-orange-100: oklch(95.4% 0.038 75.164); + --color-orange-300: oklch(83.7% 0.128 66.29); + --color-orange-400: oklch(75% 0.183 55.934); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-orange-600: oklch(64.6% 0.222 41.116); + --color-orange-700: oklch(55.3% 0.195 38.402); + --color-orange-800: oklch(47% 0.157 37.304); + --color-amber-300: oklch(87.9% 0.169 91.605); + --color-amber-700: oklch(55.5% 0.163 48.998); + --color-yellow-100: oklch(97.3% 0.071 103.193); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-800: oklch(47.6% 0.114 61.907); + --color-green-300: oklch(87.1% 0.15 154.449); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-300: oklch(84.5% 0.143 164.978); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-600: oklch(59.6% 0.145 163.225); + --color-emerald-700: oklch(50.8% 0.118 165.612); + --color-emerald-900: oklch(37.8% 0.077 168.94); + --color-emerald-950: oklch(26.2% 0.051 172.552); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-neutral-50: oklch(98.5% 0 0); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-950: oklch(14.5% 0 0); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --container-3xl: 48rem; + --font-weight-light: 300; + --font-weight-bold: 700; + --font-weight-black: 900; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-xl: 0.75rem; + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-2 { + top: calc(var(--spacing) * 2); + } + .top-4 { + top: calc(var(--spacing) * 4); + } + .top-10 { + top: calc(var(--spacing) * 10); + } + .right-0 { + right: calc(var(--spacing) * 0); + } + .right-2 { + right: calc(var(--spacing) * 2); + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .left-10 { + left: calc(var(--spacing) * 10); + } + .z-40 { + z-index: 40; + } + .m-2 { + margin: calc(var(--spacing) * 2); + } + .m-4 { + margin: calc(var(--spacing) * 4); + } + .m-8 { + margin: calc(var(--spacing) * 8); + } + .m-auto { + margin: auto; + } + .mx-8 { + margin-inline: calc(var(--spacing) * 8); + } + .mx-auto { + margin-inline: auto; + } + .my-auto { + margin-block: auto; + } + .prose { + color: var(--tw-prose-body); + max-width: 65ch; + :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-lead); + font-size: 1.25em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 1.2em; + } + :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-links); + text-decoration: underline; + font-weight: 500; + } + :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-bold); + font-weight: 600; + } + :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; + } + :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; + } + :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; + } + :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; + } + :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; + } + :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; + } + :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; + } + :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; + } + :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; + } + :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; + } + :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: disc; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; + } + :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + font-weight: 400; + color: var(--tw-prose-counters); + } + :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + color: var(--tw-prose-bullets); + } + :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.25em; + } + :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-color: var(--tw-prose-hr); + border-top-width: 1px; + margin-top: 3em; + margin-bottom: 3em; + } + :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-style: italic; + color: var(--tw-prose-quotes); + border-inline-start-width: 0.25rem; + border-inline-start-color: var(--tw-prose-quote-borders); + quotes: "\201C""\201D""\2018""\2019"; + margin-top: 1.6em; + margin-bottom: 1.6em; + padding-inline-start: 1em; + } + :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: open-quote; + } + :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: close-quote; + } + :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 800; + font-size: 2.25em; + margin-top: 0; + margin-bottom: 0.8888889em; + line-height: 1.1111111; + } + :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 900; + color: inherit; + } + :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 700; + font-size: 1.5em; + margin-top: 2em; + margin-bottom: 1em; + line-height: 1.3333333; + } + :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 800; + color: inherit; + } + :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; + } + :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; + } + :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; + } + :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; + } + :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + display: block; + margin-top: 2em; + margin-bottom: 2em; + } + :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-family: inherit; + color: var(--tw-prose-kbd); + box-shadow: 0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows); + font-size: 0.875em; + border-radius: 0.3125rem; + padding-top: 0.1875em; + padding-inline-end: 0.375em; + padding-bottom: 0.1875em; + padding-inline-start: 0.375em; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-code); + font-weight: 600; + font-size: 0.875em; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: "`"; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: "`"; + } + :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.875em; + } + :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.9em; + } + :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-pre-code); + background-color: var(--tw-prose-pre-bg); + overflow-x: auto; + font-weight: 400; + font-size: 0.875em; + line-height: 1.7142857; + margin-top: 1.7142857em; + margin-bottom: 1.7142857em; + border-radius: 0.375rem; + padding-top: 0.8571429em; + padding-inline-end: 1.1428571em; + padding-bottom: 0.8571429em; + padding-inline-start: 1.1428571em; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + background-color: transparent; + border-width: 0; + border-radius: 0; + padding: 0; + font-weight: inherit; + color: inherit; + font-size: inherit; + font-family: inherit; + line-height: inherit; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: none; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: none; + } + :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + width: 100%; + table-layout: auto; + margin-top: 2em; + margin-bottom: 2em; + font-size: 0.875em; + line-height: 1.7142857; + } + :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-th-borders); + } + :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + vertical-align: bottom; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; + } + :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-td-borders); + } + :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 0; + } + :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: baseline; + } + :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-top-width: 1px; + border-top-color: var(--tw-prose-th-borders); + } + :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: top; + } + :where(th, td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + text-align: start; + } + :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; + } + :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-captions); + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; + } + --tw-prose-body: oklch(37.3% 0.034 259.733); + --tw-prose-headings: oklch(21% 0.034 264.665); + --tw-prose-lead: oklch(44.6% 0.03 256.802); + --tw-prose-links: oklch(21% 0.034 264.665); + --tw-prose-bold: oklch(21% 0.034 264.665); + --tw-prose-counters: oklch(55.1% 0.027 264.364); + --tw-prose-bullets: oklch(87.2% 0.01 258.338); + --tw-prose-hr: oklch(92.8% 0.006 264.531); + --tw-prose-quotes: oklch(21% 0.034 264.665); + --tw-prose-quote-borders: oklch(92.8% 0.006 264.531); + --tw-prose-captions: oklch(55.1% 0.027 264.364); + --tw-prose-kbd: oklch(21% 0.034 264.665); + --tw-prose-kbd-shadows: color-mix(in oklab, oklch(21% 0.034 264.665) 10%, transparent); + --tw-prose-code: oklch(21% 0.034 264.665); + --tw-prose-pre-code: oklch(92.8% 0.006 264.531); + --tw-prose-pre-bg: oklch(27.8% 0.033 256.848); + --tw-prose-th-borders: oklch(87.2% 0.01 258.338); + --tw-prose-td-borders: oklch(92.8% 0.006 264.531); + --tw-prose-invert-body: oklch(87.2% 0.01 258.338); + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: oklch(70.7% 0.022 261.325); + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: oklch(70.7% 0.022 261.325); + --tw-prose-invert-bullets: oklch(44.6% 0.03 256.802); + --tw-prose-invert-hr: oklch(37.3% 0.034 259.733); + --tw-prose-invert-quotes: oklch(96.7% 0.003 264.542); + --tw-prose-invert-quote-borders: oklch(37.3% 0.034 259.733); + --tw-prose-invert-captions: oklch(70.7% 0.022 261.325); + --tw-prose-invert-kbd: #fff; + --tw-prose-invert-kbd-shadows: rgb(255 255 255 / 10%); + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: oklch(87.2% 0.01 258.338); + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: oklch(44.6% 0.03 256.802); + --tw-prose-invert-td-borders: oklch(37.3% 0.034 259.733); + font-size: 1rem; + line-height: 1.75; + :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; + } + :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; + } + :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; + } + :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + :where(.prose > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + } + :where(.prose > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; + } + :where(.prose > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + } + :where(.prose > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; + } + :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + padding-inline-start: 1.625em; + } + :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; + } + :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; + } + :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-top: 0.5714286em; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; + } + :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; + } + :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; + } + :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 0; + } + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-8 { + margin-top: calc(var(--spacing) * 8); + } + .mt-auto { + margin-top: auto; + } + .mb-0 { + margin-bottom: calc(var(--spacing) * 0); + } + .ml-3 { + margin-left: calc(var(--spacing) * 3); + } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-16 { + margin-left: calc(var(--spacing) * 16); + } + .ml-32 { + margin-left: calc(var(--spacing) * 32); + } + .ml-34 { + margin-left: calc(var(--spacing) * 34); + } + .ml-36 { + margin-left: calc(var(--spacing) * 36); + } + .ml-48 { + margin-left: calc(var(--spacing) * 48); + } + .ml-auto { + margin-left: auto; + } + .block { + display: block; + } + .flex { + display: flex; + } + .hidden { + display: none; + } + .table { + display: table; + } + .h-auto { + height: auto; + } + .h-full { + height: 100%; + } + .h-screen { + height: 100vh; + } + .min-h-auto { + min-height: auto; + } + .min-h-screen { + min-height: 100vh; + } + .w-16 { + width: calc(var(--spacing) * 16); + } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-32 { + width: calc(var(--spacing) * 32); + } + .w-48 { + width: calc(var(--spacing) * 48); + } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-auto { + width: auto; + } + .w-full { + width: 100%; + } + .max-w-3xl { + max-width: var(--container-3xl); + } + .max-w-none { + max-width: none; + } + .min-w-auto { + min-width: auto; + } + .border-collapse { + border-collapse: collapse; + } + .-translate-x-full { + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .rotate-180 { + rotate: 180deg; + } + .resize { + resize: both; + } + .flex-col { + flex-direction: column; + } + .flex-row { + flex-direction: row; + } + .justify-center { + justify-content: center; + } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-8 { + gap: calc(var(--spacing) * 8); + } + .overflow-hidden { + overflow: hidden; + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-sm { + border-radius: var(--radius-sm); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-2 { + border-style: var(--tw-border-style); + border-width: 2px; + } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-r-4 { + border-right-style: var(--tw-border-style); + border-right-width: 4px; + } + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + .border-black { + border-color: var(--color-black); + } + .border-emerald-600 { + border-color: var(--color-emerald-600); + } + .border-emerald-700 { + border-color: var(--color-emerald-700); + } + .border-emerald-950 { + border-color: var(--color-emerald-950); + } + .border-neutral-200 { + border-color: var(--color-neutral-200); + } + .border-neutral-950 { + border-color: var(--color-neutral-950); + } + .border-orange-600 { + border-color: var(--color-orange-600); + } + .border-red-950 { + border-color: var(--color-red-950); + } + .bg-amber-300 { + background-color: var(--color-amber-300); + } + .bg-amber-700 { + background-color: var(--color-amber-700); + } + .bg-black { + background-color: var(--color-black); + } + .bg-emerald-100 { + background-color: var(--color-emerald-100); + } + .bg-emerald-200 { + background-color: var(--color-emerald-200); + } + .bg-emerald-400 { + background-color: var(--color-emerald-400); + } + .bg-green-300 { + background-color: var(--color-green-300); + } + .bg-neutral-100 { + background-color: var(--color-neutral-100); + } + .bg-neutral-200 { + background-color: var(--color-neutral-200); + } + .bg-neutral-300 { + background-color: var(--color-neutral-300); + } + .bg-neutral-500 { + background-color: var(--color-neutral-500); + } + .bg-neutral-700 { + background-color: var(--color-neutral-700); + } + .bg-orange-100 { + background-color: var(--color-orange-100); + } + .bg-orange-300 { + background-color: var(--color-orange-300); + } + .bg-orange-400 { + background-color: var(--color-orange-400); + } + .bg-orange-500 { + background-color: var(--color-orange-500); + } + .bg-orange-700 { + background-color: var(--color-orange-700); + } + .bg-orange-800 { + background-color: var(--color-orange-800); + } + .bg-red-300 { + background-color: var(--color-red-300); + } + .bg-red-400 { + background-color: var(--color-red-400); + } + .bg-slate-700 { + background-color: var(--color-slate-700); + } + .bg-white { + background-color: var(--color-white); + } + .fill-yellow-100 { + fill: var(--color-yellow-100); + } + .stroke-3 { + stroke-width: 3; + } + .p-3 { + padding: calc(var(--spacing) * 3); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .align-middle { + vertical-align: middle; + } + .font-black { + --tw-font-weight: var(--font-weight-black); + font-weight: var(--font-weight-black); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-light { + --tw-font-weight: var(--font-weight-light); + font-weight: var(--font-weight-light); + } + .text-black { + color: var(--color-black); + } + .text-emerald-100 { + color: var(--color-emerald-100); + } + .text-emerald-300 { + color: var(--color-emerald-300); + } + .text-emerald-900 { + color: var(--color-emerald-900); + } + .text-emerald-950 { + color: var(--color-emerald-950); + } + .text-neutral-900 { + color: var(--color-neutral-900); + } + .text-neutral-950 { + color: var(--color-neutral-950); + } + .text-orange-100 { + color: var(--color-orange-100); + } + .text-orange-300 { + color: var(--color-orange-300); + } + .text-red-100 { + color: var(--color-red-100); + } + .text-red-700 { + color: var(--color-red-700); + } + .text-red-800 { + color: var(--color-red-800); + } + .text-red-950 { + color: var(--color-red-950); + } + .text-yellow-300 { + color: var(--color-yellow-300); + } + .text-yellow-800 { + color: var(--color-yellow-800); + } + .underline { + text-decoration-line: underline; + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .duration-400 { + --tw-duration: 400ms; + transition-duration: 400ms; + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .prose-neutral { + --tw-prose-body: oklch(37.1% 0 0); + --tw-prose-headings: oklch(20.5% 0 0); + --tw-prose-lead: oklch(43.9% 0 0); + --tw-prose-links: oklch(20.5% 0 0); + --tw-prose-bold: oklch(20.5% 0 0); + --tw-prose-counters: oklch(55.6% 0 0); + --tw-prose-bullets: oklch(87% 0 0); + --tw-prose-hr: oklch(92.2% 0 0); + --tw-prose-quotes: oklch(20.5% 0 0); + --tw-prose-quote-borders: oklch(92.2% 0 0); + --tw-prose-captions: oklch(55.6% 0 0); + --tw-prose-kbd: oklch(20.5% 0 0); + --tw-prose-kbd-shadows: color-mix(in oklab, oklch(20.5% 0 0) 10%, transparent); + --tw-prose-code: oklch(20.5% 0 0); + --tw-prose-pre-code: oklch(92.2% 0 0); + --tw-prose-pre-bg: oklch(26.9% 0 0); + --tw-prose-th-borders: oklch(87% 0 0); + --tw-prose-td-borders: oklch(92.2% 0 0); + --tw-prose-invert-body: oklch(87% 0 0); + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: oklch(70.8% 0 0); + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: oklch(70.8% 0 0); + --tw-prose-invert-bullets: oklch(43.9% 0 0); + --tw-prose-invert-hr: oklch(37.1% 0 0); + --tw-prose-invert-quotes: oklch(97% 0 0); + --tw-prose-invert-quote-borders: oklch(37.1% 0 0); + --tw-prose-invert-captions: oklch(70.8% 0 0); + --tw-prose-invert-kbd: #fff; + --tw-prose-invert-kbd-shadows: rgb(255 255 255 / 10%); + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: oklch(87% 0 0); + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: oklch(43.9% 0 0); + --tw-prose-invert-td-borders: oklch(37.1% 0 0); + } + .sm\:translate-x-0 { + @media (width >= 40rem) { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .sm\:px-6 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .dark\:border-neutral-800 { + @media (prefers-color-scheme: dark) { + border-color: var(--color-neutral-800); + } + } + .dark\:bg-black { + @media (prefers-color-scheme: dark) { + background-color: var(--color-black); + } + } + .dark\:bg-neutral-700 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-neutral-700); + } + } + .dark\:bg-neutral-800 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-neutral-800); + } + } + .dark\:bg-neutral-900 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-neutral-900); + } + } + .dark\:bg-neutral-950 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-neutral-950); + } + } + .dark\:bg-red-800 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-red-800); + } + } + .dark\:bg-red-900 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-red-900); + } + } + .dark\:bg-red-950 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-red-950); + } + } + .dark\:text-neutral-50 { + @media (prefers-color-scheme: dark) { + color: var(--color-neutral-50); + } + } + .dark\:text-neutral-100 { + @media (prefers-color-scheme: dark) { + color: var(--color-neutral-100); + } + } + .dark\:text-red-300 { + @media (prefers-color-scheme: dark) { + color: var(--color-red-300); + } + } + .dark\:text-red-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-red-400); + } + } + .dark\:prose-invert { + @media (prefers-color-scheme: dark) { + --tw-prose-body: var(--tw-prose-invert-body); + --tw-prose-headings: var(--tw-prose-invert-headings); + --tw-prose-lead: var(--tw-prose-invert-lead); + --tw-prose-links: var(--tw-prose-invert-links); + --tw-prose-bold: var(--tw-prose-invert-bold); + --tw-prose-counters: var(--tw-prose-invert-counters); + --tw-prose-bullets: var(--tw-prose-invert-bullets); + --tw-prose-hr: var(--tw-prose-invert-hr); + --tw-prose-quotes: var(--tw-prose-invert-quotes); + --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); + --tw-prose-captions: var(--tw-prose-invert-captions); + --tw-prose-kbd: var(--tw-prose-invert-kbd); + --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows); + --tw-prose-code: var(--tw-prose-invert-code); + --tw-prose-pre-code: var(--tw-prose-invert-pre-code); + --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); + --tw-prose-th-borders: var(--tw-prose-invert-th-borders); + --tw-prose-td-borders: var(--tw-prose-invert-td-borders); + } + } +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-duration: initial; + --tw-ease: initial; + } + } +} diff --git a/internal/router/static/reset.css b/internal/router/static/reset.css deleted file mode 100644 index b78b73d..0000000 --- a/internal/router/static/reset.css +++ /dev/null @@ -1,36 +0,0 @@ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 15px; - line-height: 1.6; - -webkit-text-size-adjust: 100%; -} - -body { - background: #f8f9fa; - color: #202122; - font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; - min-height: 100vh; -} - -a { - color: #3366cc; - text-decoration: none; -} - -a:visited { - color: #6b4ba1; -} - -a:hover { - text-decoration: underline; -} - -img { - max-width: 100%; - display: block; -} diff --git a/internal/router/templates/admin.html b/internal/router/templates/admin.html index 4fce225..50fafe8 100644 --- a/internal/router/templates/admin.html +++ b/internal/router/templates/admin.html @@ -1,296 +1,128 @@ - - - - RED Engine Admin - - - -
-

Node Administration

- -
- -
- - -
- -
-
- -
-

+ + RED Engine Admin + + + + +

+

Node Administration

+ +
+ +
+ + +
+ +
+
+ +
+

- ✓ Authenticated -

- -

Add Remote Base or Guide

- - - - - - -
- - -
- - -
- -

Currently Synced Sync Targets

-
    -
    -
    - - - - + list.appendChild(li); + }); + } + // Fire archive imports + async function syncGuide() { + const status = document.getElementById("status"); + const payload = { + url: document.getElementById("url").value, + filename: document.getElementById("filename").value, + saveToStartup: document.getElementById("saveState").checked, + }; + + status.textContent = "Syncing..."; + status.className = ""; + + try { + const response = await fetch("/-/import", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Admin-Token": sessionToken, + }, + body: JSON.stringify(payload), + }); + + const text = await response.text(); + if (response.ok) { + status.textContent = text; + status.className = "success"; + document.getElementById("url").value = ""; + document.getElementById("filename").value = ""; + refreshList(); + } else { + status.textContent = "Error: " + text; + status.className = "error"; + } + } catch (err) { + status.textContent = "Network error syncing target."; + status.className = "error"; + } + } + + // Drop remote data scopes dynamically + async function removeGuide(filename, escapedFilename) { + const deleteCheckbox = document.getElementById( + `delete-files-${escapedFilename}`, + ); + const deleteLocal = deleteCheckbox + ? deleteCheckbox.checked + : false; + + let msg = `Are you sure you want to stop tracking "${filename}"?`; + if (deleteLocal) { + msg += + " This will permanently delete its files from system storage!"; + } + + if (!confirm(msg)) return; + + try { + const response = await fetch("/-/admin/remove", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Admin-Token": sessionToken, + }, + body: JSON.stringify({ + filename: filename, + deleteLocalFiles: deleteLocal, // <-- PASSES STATE TO BACKEND + }), + }); + + if (response.ok) { + refreshList(); + } else { + alert( + "Prune operation failed: " + + (await response.text()), + ); + } + } catch (err) { + alert("Network communication fault."); + } + } + + // Silent status sync background refreshes + async function refreshList() { + const response = await fetch("/-/admin/config", { + headers: { "X-Admin-Token": sessionToken }, + }); + if (response.ok) { + const data = await response.json(); + renderList(data); + } + } + + + + \ No newline at end of file diff --git a/internal/router/templates/base.html b/internal/router/templates/base.html index 3d7603e..bae15db 100644 --- a/internal/router/templates/base.html +++ b/internal/router/templates/base.html @@ -1,79 +1,157 @@ - - - - {{ .Site }} - {{ .Title }} - - - - - - - -
    - - -
    - {{ if .Crumb }} - - {{ end }} - -
    - {{ if .Verified }} -
    - ✅ Verified Contributor: {{ .Author }} -
    - {{ else }} -
    - ⚠️ Unverified / Unknown Origin -
    - {{ end }} -
    - -
    {{ .Body }}
    - - -
    + + + + + + + {{ .Site }} - {{ .Title }} + + + + +
    + + + +
    + {{ if .Crumb }} + + {{ end }} + +
    + {{if .Verified}} +
    + + + + +

    Verified Contributor: {{ .Author }}

    +
    - - + {{else}} +
    + + + + +

    + Unverified / Unknown Origin +

    +
    + {{end}} +
    + + +
    +
    + {{ .Body }} +
    +
    + + + + + +
    +
    +

    This open source project was developed by people like you! If you are interested in contributing, please + consider hosting + a node or collaborating with us on GitHub.

    +
    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/internal/store/store.go b/internal/store/store.go index bcc0490..38d9ea9 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -14,15 +14,16 @@ import ( "sync" "github.com/RED-Collective/red-engine/internal/render" + "github.com/fsnotify/fsnotify" ) type Article struct { Path string Title string Body template.HTML - Hash string // SHA-256 Hash for UI Display - Verified bool // Ed25519 Verification Status - Author string // Name of the verified signer + Hash string + Verified bool + Author string } type Section struct { @@ -48,7 +49,6 @@ func (s *Store) DataDir() string { return s.dataDir } -// --- Cryptographic Structs matching the Obsidian Plugin --- type Contributor struct { Name string `json:"name"` PublicKey string `json:"public_key"` @@ -56,7 +56,7 @@ type Contributor struct { type ManifestEntry struct { FileHash string `json:"file_hash"` - Hash string `json:"hash"` // Fallback support + Hash string `json:"hash"` PublicKey string `json:"public_key"` Signature string `json:"signature"` } @@ -65,28 +65,62 @@ type Manifest struct { Files map[string]ManifestEntry `json:"files"` } -// Helper to unmarshal flexible manifest format func parseManifestJSON(data []byte) map[string]ManifestEntry { result := make(map[string]ManifestEntry) - - // Try wrapped format first var wrapped Manifest if err := json.Unmarshal(data, &wrapped); err == nil && len(wrapped.Files) > 0 { return wrapped.Files } - - // Try flat format: {filepath: entry, ...} if err := json.Unmarshal(data, &result); err == nil { return result } - return result } +func (s *Store) Watch() error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + absDataDir, _ := filepath.Abs(s.dataDir) + + filepath.WalkDir(absDataDir, func(path string, d fs.DirEntry, err error) error { + if err == nil && d.IsDir() { + watcher.Add(path) + } + return nil + }) + + go func() { + defer watcher.Close() + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 { + log.Printf("🔄 File change detected: %s. Reloading store...", event.Name) + s.Reload() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("⚠️ Watcher error:", err) + } + } + }() + + log.Printf("[DEBUG] File watcher started on %s", absDataDir) + return nil +} + func (s *Store) Reload() error { s.mu.Lock() defer s.mu.Unlock() - // 1. Load the Trusted Public Keys from contributors.json + trustedKeys := make(map[string]string) if trustData, err := os.ReadFile("contributors.json"); err == nil { var contributors []Contributor @@ -99,26 +133,19 @@ func (s *Store) Reload() error { log.Println("⚠️ Warning: contributors.json not found. Verification checks disabled.") } - // 2. Pre-load all signatures from any manifest.json in the data directory - // Map by filepath for easier lookup allSignatures := make(map[string]ManifestEntry) filepath.WalkDir(s.dataDir, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() || filepath.Base(path) != "manifest.json" { return nil } - manifestData, err := os.ReadFile(path) if err != nil { - log.Printf("Warning: cannot read manifest %s: %v", path, err) return nil } - manifest := parseManifestJSON(manifestData) if len(manifest) == 0 { return nil } - - // Get manifest directory relative to dataDir manifestDir := filepath.Dir(path) relManifestDir, err := filepath.Rel(s.dataDir, manifestDir) if err != nil { @@ -141,11 +168,6 @@ func (s *Store) Reload() error { return nil }) - log.Printf("[DEBUG] Loaded %d signature entries", len(allSignatures)) - for k := range allSignatures { - log.Printf("[DEBUG] %s", k) - } - newNav := make(map[string]*Section) err := filepath.WalkDir(s.dataDir, func(path string, d fs.DirEntry, err error) error { @@ -158,7 +180,6 @@ func (s *Store) Reload() error { return nil } - // 3. Calculate SHA-256 hashBytes := sha256.Sum256(content) fileHash := hex.EncodeToString(hashBytes[:]) @@ -167,64 +188,51 @@ func (s *Store) Reload() error { return nil } - // 4. Ed25519 Cryptographic Verification - isVerified := false - authorName := "Unverified / Unknown Origin" - - // Get relative path in the format used in manifest rel, _ := filepath.Rel(s.dataDir, path) relativePath := strings.TrimPrefix(filepath.ToSlash(rel), "/") - log.Printf("[DEBUG] Checking file: %s -> relativePath=%s", path, relativePath) - // Try to find manifest entry by filepath + // Clean the path for web URL building + cleanPath := strings.TrimSuffix(relativePath, ".md") + + isVerified := false + authorName := "Unverified / Unknown Origin" + if entry, exists := allSignatures[relativePath]; exists { - // Use file_hash if available, otherwise hash entryHash := entry.FileHash if entryHash == "" { entryHash = entry.Hash } - // Does the hash match? if entryHash == fileHash { - // Does the signature belong to a trusted public key? if trustedAuthor, isTrusted := trustedKeys[strings.ToLower(entry.PublicKey)]; isTrusted { pubBytes, err1 := hex.DecodeString(entry.PublicKey) sigBytes, err2 := hex.DecodeString(entry.Signature) if err1 == nil && err2 == nil && len(pubBytes) == ed25519.PublicKeySize { - // Check 1: Did the plugin sign the raw Markdown content? if ed25519.Verify(pubBytes, content, sigBytes) { isVerified = true authorName = trustedAuthor - // Check 2: Did the plugin sign the Hex string of the SHA256 hash? (Very common) } else if ed25519.Verify(pubBytes, []byte(fileHash), sigBytes) { isVerified = true authorName = trustedAuthor - // Check 3: Did the plugin sign the raw SHA256 bytes? } else if ed25519.Verify(pubBytes, hashBytes[:], sigBytes) { isVerified = true authorName = trustedAuthor - } else { - log.Printf("[DEBUG] Signature verification failed for %s (Tried Content, Hex Hash, and Byte Hash)", relativePath) } } - } else { - log.Printf("[DEBUG] Public key not trusted for %s: %s", relativePath, entry.PublicKey) } - } else { - log.Printf("[DEBUG] Hash mismatch for %s: stored=%s, actual=%s", relativePath, entryHash, fileHash) } } - // 5. Build Article Structure - parts := strings.Split(filepath.ToSlash(rel), "/") + // Use cleanPath (no .md) to build the tree and URLs + parts := strings.Split(filepath.ToSlash(cleanPath), "/") - title := strings.TrimSuffix(parts[len(parts)-1], ".md") + title := parts[len(parts)-1] title = strings.ReplaceAll(title, "-", " ") title = strings.Title(title) art := &Article{ - Path: "/" + filepath.ToSlash(rel), + Path: "/" + filepath.ToSlash(cleanPath), Title: title, Body: template.HTML(res.HTMLContent), Hash: fileHash, @@ -232,7 +240,6 @@ func (s *Store) Reload() error { Author: authorName, } - // Tree Building if len(parts) == 1 { if newNav["root"] == nil { newNav["root"] = &Section{Name: "root"} diff --git a/internal/store/watcher.go b/internal/store/watcher.go new file mode 100644 index 0000000..7cd39f7 --- /dev/null +++ b/internal/store/watcher.go @@ -0,0 +1,51 @@ +package store + +import ( + "io/fs" + "log" + "path/filepath" + + "github.com/fsnotify/fsnotify" +) + +func (s *Store) Watcher() error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + absDataDir, _ := filepath.Abs(s.dataDir) + + // Watch the root and all subdirectories + filepath.WalkDir(absDataDir, func(path string, d fs.DirEntry, err error) error { + if err == nil && d.IsDir() { + watcher.Add(path) + } + return nil + }) + + go func() { + defer watcher.Close() + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + // Trigger a reload if a file is written, created, removed, or renamed + if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename { + log.Printf("🔄 File change detected: %s. Reloading store...", event.Name) + s.Reload() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("⚠️ Watcher error:", err) + } + } + }() + + log.Printf("[DEBUG] File watcher started on %s", absDataDir) + return nil +} diff --git a/manage-token.ps1 b/manage-token.ps1 index 788c61f..b0aa33d 100644 --- a/manage-token.ps1 +++ b/manage-token.ps1 @@ -1,34 +1,48 @@ $ConfigFile = "config.json" -if (-Not (Test-Path $ConfigFile)) +# Helper function to generate a secure token +function New-SecureToken { - Write-Host "Error: $ConfigFile not found in the current directory!" -ForegroundColor Red - Exit + $Bytes = New-Object Byte[] 16 + [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Bytes) + return [BitConverter]::ToString($Bytes) -replace '-' } -# Parse the JSON file -$Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json - -$CurrentToken = $Config.adminToken - -Write-Host "----------------------------------------" -if ([string]::IsNullOrEmpty($CurrentToken)) -{ - Write-Host "Current Admin Token: [NONE / NOT SET]" -ForegroundColor Yellow -} else +# 1. Check/Create Config +if (-Not (Test-Path $ConfigFile)) { - Write-Host "Current Admin Token: $CurrentToken" -ForegroundColor Cyan + Write-Host "⚠️ $ConfigFile not found. Creating a default configuration..." -ForegroundColor Yellow + + $NewToken = New-SecureToken + $DefaultConfig = [PSCustomObject]@{ + addr = ":8080" + siteName = "RED Engine" + dataDir = "/app/data" + adminToken = $NewToken + startupSync = @( + @{ + url = "https://github.com/mundimark/awesome-markdown" + filename = "awesome-markdown.md" + } + ) + } + $DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile + Write-Host "✅ Created new config.json with secure token." -ForegroundColor Green } + +# 2. Parse and Display +$Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json +Write-Host "`n----------------------------------------" +Write-Host "Current Admin Token: $($Config.adminToken)" -ForegroundColor Cyan Write-Host "----------------------------------------`n" +# 3. Interactive Update $Choice = Read-Host "Would you like to generate and save a new secure token? (y/N)" if ($Choice -match "^[yY]") { - # Generate a cryptographically secure 32-character hexadecimal token - $Bytes = New-Object Byte[] 16 - [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Bytes) - $NewToken = [BitConverter]::ToString($Bytes) -replace '-' + $Config.adminToken = New-SecureToken + $Config | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile # Update the object and save it back to disk $Config.adminToken = $NewToken diff --git a/manage-token.sh b/manage-token.sh index ac053a0..ff07f2f 100755 --- a/manage-token.sh +++ b/manage-token.sh @@ -1,43 +1,49 @@ #!/bin/bash CONFIG_FILE="config.json" +# Helper function for token +generate_token() { + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 +} + +# 1. Check/Create Config if [ ! -f "$CONFIG_FILE" ]; then - echo "Error: $CONFIG_FILE not found in the current directory!" - exit 1 -fi + echo "⚠️ $CONFIG_FILE not found. Creating a default configuration..." + NEW_TOKEN=$(generate_token) -# Extract the current token safely -CURRENT_TOKEN=$(grep '"adminToken"' "$CONFIG_FILE" | sed -E 's/.*"adminToken"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/') + cat < "$CONFIG_FILE" +{ + "addr": ":8080", + "siteName": "RED Engine", + "dataDir": "/app/data", + "adminToken": "$NEW_TOKEN", + "startupSync": [ + { + "url": "https://github.com/mundimark/awesome-markdown", + "filename": "awesome-markdown.md" + } + ] +} +EOF + echo "✅ Created new config.json." +fi +# 2. Display Current Token +TOKEN=$(grep -oP '"adminToken": "\K[^"]+' "$CONFIG_FILE") echo "----------------------------------------" -if [ -z "$CURRENT_TOKEN" ]; then - echo "Current Admin Token: [NONE / NOT SET]" -else - echo "Current Admin Token: $CURRENT_TOKEN" -fi +echo "Current Admin Token: $TOKEN" echo "----------------------------------------" -echo "" +# 3. Interactive Update read -p "Would you like to generate and save a new secure token? (y/N): " choice -case "$choice" in - y|Y ) - # Generate a secure 24-character random string - NEW_TOKEN=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1) - - # Safely replace the token in the JSON file - if grep -q '"adminToken"' "$CONFIG_FILE"; then - sed -i.bak -E 's/("adminToken"[[:space:]]*:[[:space:]]*")[^"]*(")/\1'"$NEW_TOKEN"'\2/' "$CONFIG_FILE" - rm -f "$CONFIG_FILE.bak" - else - echo "Error: 'adminToken' key not found in $CONFIG_FILE. Please add it manually." - exit 1 - fi +if [[ "$choice" =~ ^[yY]$ ]]; then + NEW_TOKEN=$(generate_token) + # Use sed to replace the token safely + sed -i "s/\"adminToken\": \".*\"/\"adminToken\": \"$NEW_TOKEN\"/" "$CONFIG_FILE" echo "✅ Token updated successfully!" echo "Your new token is: $NEW_TOKEN" - echo "⚠️ Make sure to restart your node: podman-compose restart red_engine" - ;; - * ) + echo "⚠️ Restart your node: podman-compose restart red_engine" +else echo "Operation cancelled. Token unchanged." - ;; -esac +fi diff --git a/set-ports.sh b/set-ports.sh old mode 100755 new mode 100644