Import Labyricorn Control Plane
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/941c018f-56b5-4bf6-afbf-444c3f0a351f
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@@ -0,0 +1,661 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "react-example",
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"vite": "^6.2.3",
|
||||
"yaml": "^2.9.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/[email protected]", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/[email protected]", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/[email protected]", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/[email protected]", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
|
||||
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@google/genai": ["@google/[email protected]", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-GM7C8Kaomvjz05x5JEO6+l3d/pciL9LxAG9dUjJLD7nTPZ9X0Cfsf2Z7eET6UjgWyUmxXCHtYnQoQ77F9+ZIOQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/[email protected]", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/[email protected]", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/[email protected]", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/[email protected]", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/[email protected]", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/[email protected]", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/[email protected]", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/[email protected]", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/[email protected]", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/[email protected]", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/[email protected]", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/[email protected]", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||
|
||||
"@types/babel__core": ["@types/[email protected]", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/[email protected]", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/body-parser": ["@types/[email protected]", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/estree": ["@types/[email protected]", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/express": ["@types/[email protected]", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="],
|
||||
|
||||
"@types/http-errors": ["@types/[email protected]", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/mime": ["@types/[email protected]", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
|
||||
|
||||
"@types/qs": ["@types/[email protected]", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||
|
||||
"@types/range-parser": ["@types/[email protected]", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/retry": ["@types/[email protected]", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||
|
||||
"@types/send": ["@types/[email protected]", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"@types/serve-static": ["@types/[email protected]", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/[email protected]", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"accepts": ["[email protected]", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"array-flatten": ["[email protected]", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"autoprefixer": ["[email protected]", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="],
|
||||
|
||||
"base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A=="],
|
||||
|
||||
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"body-parser": ["[email protected]", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="],
|
||||
|
||||
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="],
|
||||
|
||||
"buffer-equal-constant-time": ["[email protected]", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["[email protected]", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
|
||||
|
||||
"content-disposition": ["[email protected]", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"content-type": ["[email protected]", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["[email protected]", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["[email protected]", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||
|
||||
"data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"destroy": ["[email protected]", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dotenv": ["[email protected]", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
||||
"dunder-proto": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["[email protected]", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["[email protected]", "", {}, "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA=="],
|
||||
|
||||
"encodeurl": ["[email protected]", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="],
|
||||
|
||||
"es-define-property": ["[email protected]", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["[email protected]", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["[email protected]", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["[email protected]", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
||||
|
||||
"extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["[email protected]", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"finalhandler": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||
|
||||
"formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["[email protected]", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
|
||||
|
||||
"fresh": ["[email protected]", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gaxios": ["[email protected]", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="],
|
||||
|
||||
"gcp-metadata": ["[email protected]", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||
|
||||
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-intrinsic": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["[email protected]", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"google-auth-library": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="],
|
||||
|
||||
"google-logging-utils": ["[email protected]", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"gopd": ["[email protected]", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["[email protected]", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"https-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["[email protected]", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jwa": ["[email protected]", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["[email protected]", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"long": ["[email protected]", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["[email protected]", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
||||
|
||||
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["[email protected]", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-descriptors": ["[email protected]", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||
|
||||
"methods": ["[email protected]", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="],
|
||||
|
||||
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
|
||||
|
||||
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||
|
||||
"ms": ["[email protected]", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||
|
||||
"negotiator": ["[email protected]", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"node-releases": ["[email protected]", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
|
||||
|
||||
"object-inspect": ["[email protected]", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["[email protected]", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"p-retry": ["[email protected]", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
"parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["[email protected]", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||
|
||||
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["[email protected]", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="],
|
||||
|
||||
"postcss-value-parser": ["[email protected]", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
||||
|
||||
"proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["[email protected]", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
||||
|
||||
"range-parser": ["[email protected]", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["[email protected]", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"react": ["[email protected]", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
|
||||
"react-refresh": ["[email protected]", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"retry": ["[email protected]", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
||||
|
||||
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"send": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||
|
||||
"serve-static": ["[email protected]", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"setprototypeof": ["[email protected]", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"statuses": ["[email protected]", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"tailwindcss": ["[email protected]", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["[email protected]", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["[email protected]", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="],
|
||||
|
||||
"type-is": ["[email protected]", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["[email protected]", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"utils-merge": ["[email protected]", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"vary": ["[email protected]", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
|
||||
|
||||
"web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
|
||||
|
||||
"yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"@babel/core/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@babel/traverse/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"send/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"@babel/core/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"@babel/traverse/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"https-proxy-agent/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Labyricorn Git Site Builder",
|
||||
"description": "Self-hosted Git site builder and control plane for assembling static websites from Git repositories with Markdown content, Nginx local hosting, staging, and remote push.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx server.ts",
|
||||
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
||||
"start": "node dist/server.cjs",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist server.js",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"vite": "^6.2.3",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"@types/express": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
id: article
|
||||
kind: content-model
|
||||
name: Article
|
||||
description: Longform articles and blog posts.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: true
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /articles/{slug}/
|
||||
presentation:
|
||||
default_style_config: article
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- rss
|
||||
- search
|
||||
@@ -0,0 +1,18 @@
|
||||
id: devlog
|
||||
kind: content-model
|
||||
name: Devlog
|
||||
description: Developer logs and updates.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: true
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /devlogs/{slug}/
|
||||
presentation:
|
||||
default_style_config: devlogs
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- rss
|
||||
- search
|
||||
@@ -0,0 +1,18 @@
|
||||
id: discussion
|
||||
kind: content-model
|
||||
name: Discussion
|
||||
description: Community forum topics.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: true
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /discussions/{slug}/
|
||||
presentation:
|
||||
default_style_config: knowledge-base
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- rss
|
||||
- search
|
||||
@@ -0,0 +1,17 @@
|
||||
id: documentation
|
||||
kind: content-model
|
||||
name: Documentation
|
||||
description: Technical guides and manuals.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: false
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /docs/{slug}/
|
||||
presentation:
|
||||
default_style_config: wiki
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- search
|
||||
@@ -0,0 +1,16 @@
|
||||
id: page
|
||||
kind: content-model
|
||||
name: Page
|
||||
description: Standalone generic pages.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: false
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /{slug}/
|
||||
presentation:
|
||||
default_style_config: article
|
||||
capabilities:
|
||||
- detail-page
|
||||
- search
|
||||
@@ -0,0 +1,17 @@
|
||||
id: project
|
||||
kind: content-model
|
||||
name: Project
|
||||
description: Project showcases and portfolios.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: false
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /projects/{slug}/
|
||||
presentation:
|
||||
default_style_config: project
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- search
|
||||
@@ -0,0 +1,18 @@
|
||||
id: release
|
||||
kind: content-model
|
||||
name: Release
|
||||
description: Software release notes.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: true
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /releases/{slug}/
|
||||
presentation:
|
||||
default_style_config: article
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- rss
|
||||
- search
|
||||
@@ -0,0 +1,17 @@
|
||||
id: wiki
|
||||
kind: content-model
|
||||
name: Wiki
|
||||
description: Community knowledge base pages.
|
||||
semantics:
|
||||
document: true
|
||||
chronological: false
|
||||
searchable: true
|
||||
indexable: true
|
||||
routing:
|
||||
detail: /wiki/{slug}/
|
||||
presentation:
|
||||
default_style_config: wiki
|
||||
capabilities:
|
||||
- detail-page
|
||||
- index-page
|
||||
- search
|
||||
@@ -0,0 +1,31 @@
|
||||
navigation:
|
||||
- id: "home"
|
||||
label: "Home"
|
||||
route: "/"
|
||||
iconName: "Globe"
|
||||
contentModel: "page"
|
||||
- id: "articles"
|
||||
label: "Articles"
|
||||
route: "/articles/"
|
||||
iconName: "FileText"
|
||||
contentModel: "article"
|
||||
- id: "projects"
|
||||
label: "Projects"
|
||||
route: "/projects/"
|
||||
iconName: "LayoutTemplate"
|
||||
contentModel: "project"
|
||||
- id: "documentation"
|
||||
label: "Documentation"
|
||||
route: "/docs/"
|
||||
iconName: "FolderOpen"
|
||||
contentModel: "documentation"
|
||||
- id: "community"
|
||||
label: "Community"
|
||||
route: "/community/"
|
||||
iconName: "MessageSquare"
|
||||
contentModel: "discussion"
|
||||
- id: "downloads"
|
||||
label: "Downloads"
|
||||
route: "/downloads/"
|
||||
iconName: "Download"
|
||||
contentModel: "release"
|
||||
@@ -0,0 +1,44 @@
|
||||
protocol: "labyricorn-site/v1"
|
||||
site:
|
||||
id: "labyricorn"
|
||||
title: "Labyricorn Engineering & Architecture"
|
||||
baseUrl: "https://labyricorn.com"
|
||||
language: "en-US"
|
||||
theme:
|
||||
source: "site-definition"
|
||||
path: "/.theme"
|
||||
markdown:
|
||||
dialect: "labyricorn-commonmark-v1"
|
||||
rawHtmlPolicy: "disabled"
|
||||
rawHtmlEnabled: false
|
||||
extensions:
|
||||
tables: true
|
||||
taskLists: true
|
||||
footnotes: true
|
||||
definitionLists: true
|
||||
headingAnchors: true
|
||||
fencedCode: true
|
||||
syntaxHighlighting: true
|
||||
callouts: true
|
||||
youtube: true
|
||||
wikipediaLinks: true
|
||||
hosting:
|
||||
engine: "nginx"
|
||||
production:
|
||||
enabled: true
|
||||
hostname: "labyricorn.local"
|
||||
listen: 80
|
||||
staging:
|
||||
enabled: true
|
||||
hostname: "preview.labyricorn.local"
|
||||
listen: 8080
|
||||
releases:
|
||||
retainCount: 10
|
||||
buildPolicy:
|
||||
staging:
|
||||
enabled: true
|
||||
requireApproval: true
|
||||
localActivation:
|
||||
automatic: false
|
||||
push:
|
||||
automatic: false
|
||||
@@ -0,0 +1,16 @@
|
||||
id: "article"
|
||||
name: "Longform Article"
|
||||
description: "In-depth engineering guides, whitepapers, and technical analysis"
|
||||
defaultPath: "/.articles"
|
||||
views:
|
||||
standalone:
|
||||
template: "article/page.html"
|
||||
description: "Full article view"
|
||||
listing:
|
||||
template: "article/summary.html"
|
||||
description: "Article index summary"
|
||||
features:
|
||||
tableOfContents: true
|
||||
revisionInfo: true
|
||||
repositoryLinks: true
|
||||
breadcrumbs: true
|
||||
@@ -0,0 +1,16 @@
|
||||
id: "blog"
|
||||
name: "Blog Package"
|
||||
description: "Editorial blog posts with minimal summary listings"
|
||||
defaultPath: "/.blog"
|
||||
views:
|
||||
standalone:
|
||||
template: "blog/post.html"
|
||||
description: "Full blog post view"
|
||||
listing:
|
||||
template: "blog/minimal-summary.html"
|
||||
description: "Minimal text listing"
|
||||
features:
|
||||
tableOfContents: true
|
||||
revisionInfo: false
|
||||
repositoryLinks: false
|
||||
breadcrumbs: true
|
||||
@@ -0,0 +1,22 @@
|
||||
id: "devlogs"
|
||||
name: "Devlog Stream"
|
||||
description: "Detailed developer logs & release summaries with repo defaults & project metadata"
|
||||
defaultPath: "/.devlogs"
|
||||
views:
|
||||
standalone:
|
||||
template: "devlogs/entry.html"
|
||||
description: "Full devlog post"
|
||||
detailed-summary:
|
||||
template: "devlogs/detailed-summary.html"
|
||||
description: "Rich timeline summary"
|
||||
compact:
|
||||
template: "devlogs/compact.html"
|
||||
description: "Compact list item"
|
||||
card:
|
||||
template: "devlogs/card.html"
|
||||
description: "Visual grid card"
|
||||
features:
|
||||
tableOfContents: true
|
||||
revisionInfo: true
|
||||
repositoryLinks: true
|
||||
breadcrumbs: true
|
||||
@@ -0,0 +1,16 @@
|
||||
id: "knowledge-base"
|
||||
name: "Knowledge Base"
|
||||
description: "Structured Q&A, troubleshooting articles, and reference cards"
|
||||
defaultPath: "/.knowledge"
|
||||
views:
|
||||
standalone:
|
||||
template: "kb/article.html"
|
||||
description: "Knowledge base entry"
|
||||
compact:
|
||||
template: "kb/compact.html"
|
||||
description: "Search list view"
|
||||
features:
|
||||
tableOfContents: false
|
||||
revisionInfo: false
|
||||
repositoryLinks: false
|
||||
breadcrumbs: true
|
||||
@@ -0,0 +1,16 @@
|
||||
id: "project"
|
||||
name: "Project Showcase"
|
||||
description: "Software repository overview page linking to associated devlogs"
|
||||
defaultPath: "/.project"
|
||||
views:
|
||||
standalone:
|
||||
template: "project/index.html"
|
||||
description: "Project overview landing"
|
||||
embedded:
|
||||
template: "project/badge.html"
|
||||
description: "Project badge embed"
|
||||
features:
|
||||
tableOfContents: true
|
||||
revisionInfo: true
|
||||
repositoryLinks: true
|
||||
breadcrumbs: false
|
||||
@@ -0,0 +1,13 @@
|
||||
id: "wiki"
|
||||
name: "Wiki Hierarchy"
|
||||
description: "Hierarchical documentation system with sidebar navigation and backlinks"
|
||||
defaultPath: "/.wiki"
|
||||
views:
|
||||
standalone:
|
||||
template: "wiki/page.html"
|
||||
description: "Wiki article page"
|
||||
features:
|
||||
tableOfContents: true
|
||||
revisionInfo: true
|
||||
repositoryLinks: true
|
||||
breadcrumbs: true
|
||||
@@ -0,0 +1,788 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { store } from "./src/backend/store";
|
||||
import { CredentialRef } from "./src/types";
|
||||
import YAML from "yaml";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// --- API ROUTES ---
|
||||
|
||||
// Health
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.json({ status: "ok", service: "labyricorn-site-builder" });
|
||||
});
|
||||
|
||||
// Site Config
|
||||
app.get("/api/site-config", (req, res) => {
|
||||
res.json(store.siteConfig);
|
||||
});
|
||||
|
||||
app.put("/api/site-config", (req, res) => {
|
||||
try {
|
||||
const { rawYaml } = req.body;
|
||||
if (rawYaml) {
|
||||
const parsed = YAML.parse(rawYaml);
|
||||
store.siteConfig.rawYaml = rawYaml;
|
||||
if (parsed.site) store.siteConfig.site = parsed.site;
|
||||
if (parsed.markdown) store.siteConfig.markdown = parsed.markdown;
|
||||
if (parsed.hosting) store.siteConfig.hosting = parsed.hosting;
|
||||
if (parsed.buildPolicy)
|
||||
store.siteConfig.buildPolicy = parsed.buildPolicy;
|
||||
store.addAudit(
|
||||
"Update Site Configuration",
|
||||
"config",
|
||||
"Updated site.yaml configuration from admin UI",
|
||||
);
|
||||
}
|
||||
res.json({ success: true, siteConfig: store.siteConfig });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: `YAML parse error: ${err.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// --- GITEA DISCOVERY ---
|
||||
app.post("/api/gitea/discover", async (req, res) => {
|
||||
store.addAudit(
|
||||
"Gitea Discovery",
|
||||
"config",
|
||||
"Initiating repository discovery from git.labyricorn.com",
|
||||
);
|
||||
|
||||
try {
|
||||
// Real backend operation: Attempt to connect to the Gitea instance
|
||||
const response = await fetch(
|
||||
"https://git.labyricorn.com/api/v1/repos/search",
|
||||
{
|
||||
timeout: 5000,
|
||||
} as any,
|
||||
).catch((e) => {
|
||||
throw new Error(
|
||||
`Connection to git.labyricorn.com failed: ${e.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea returned HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repos = data.data || [];
|
||||
|
||||
let added = 0;
|
||||
for (const repo of repos) {
|
||||
if (
|
||||
!store.gitSources.find(
|
||||
(s) =>
|
||||
s.repository === repo.clone_url || s.repository === repo.ssh_url,
|
||||
)
|
||||
) {
|
||||
store.gitSources.push({
|
||||
id: repo.name,
|
||||
repository: repo.clone_url,
|
||||
ref: repo.default_branch || "main",
|
||||
branch: repo.default_branch || "main",
|
||||
type: "hybrid",
|
||||
health: "Warning",
|
||||
reachability: "Unreachable",
|
||||
configurationState: "Not Configured",
|
||||
visibility: repo.private ? "Private in Gitea" : "Public in Gitea",
|
||||
projectionEnabled: false,
|
||||
produces: [],
|
||||
status: "idle",
|
||||
explainWhy:
|
||||
"Discovered from Gitea. Awaiting initial synchronization.",
|
||||
required: false,
|
||||
credential: "none",
|
||||
});
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
discoveredCount: repos.length,
|
||||
addedCount: added,
|
||||
message: `Successfully discovered ${repos.length} repositories from Gitea.`,
|
||||
});
|
||||
} catch (e: any) {
|
||||
store.addAudit("Gitea Discovery Failed", "config", e.message, "failure");
|
||||
res.status(502).json({ error: e.message, success: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Git Sources
|
||||
app.get("/api/git-sources", (req, res) => {
|
||||
res.json(store.gitSources);
|
||||
});
|
||||
|
||||
app.post("/api/git-sources", async (req, res) => {
|
||||
const newSource = req.body;
|
||||
|
||||
// Phase 2 Defaults
|
||||
newSource.type = newSource.type || "hybrid";
|
||||
newSource.health = "Warning";
|
||||
newSource.reachability = "Unreachable";
|
||||
newSource.configurationState = "Not Configured";
|
||||
newSource.visibility = "Private in Gitea";
|
||||
newSource.projectionEnabled = false;
|
||||
newSource.produces = [];
|
||||
newSource.explainWhy = "Awaiting initial synchronization.";
|
||||
newSource.status = "syncing";
|
||||
|
||||
try {
|
||||
if (
|
||||
newSource.repository.startsWith("http") ||
|
||||
newSource.repository.startsWith("git@") ||
|
||||
newSource.repository.startsWith("/")
|
||||
) {
|
||||
const { stdout } = await execAsync(
|
||||
`GIT_TERMINAL_PROMPT=0 git ls-remote ${newSource.repository} ${newSource.ref || "HEAD"}`,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const match = stdout.trim().split("\\t")[0];
|
||||
if (match) {
|
||||
newSource.status = "connected";
|
||||
newSource.reachability = "Healthy";
|
||||
newSource.lastResolvedCommit = match;
|
||||
newSource.lastSyncedAt = new Date().toISOString();
|
||||
newSource.explainWhy =
|
||||
"Connected to repository, but missing .labyricorn/site.yml metadata.";
|
||||
} else {
|
||||
newSource.status = "error";
|
||||
newSource.explainWhy = "Ref not found in repository.";
|
||||
}
|
||||
} else {
|
||||
newSource.status = "offline";
|
||||
newSource.explainWhy =
|
||||
"Backend Not Yet Implemented for non-http/git repositories. (Development Mode)";
|
||||
}
|
||||
} catch (e: any) {
|
||||
newSource.status = "error";
|
||||
newSource.explainWhy = `Git ls-remote failed: ${e.message.split("\\n")[0]}`;
|
||||
}
|
||||
|
||||
store.gitSources.push(newSource);
|
||||
store.addAudit(
|
||||
"Add Git Source",
|
||||
"source",
|
||||
`Added Git source ${newSource.id} (${newSource.repository})`,
|
||||
);
|
||||
res.json({ success: true, source: newSource });
|
||||
});
|
||||
|
||||
app.put("/api/git-sources/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const idx = store.gitSources.findIndex((s) => s.id === id);
|
||||
if (idx !== -1) {
|
||||
store.gitSources[idx] = { ...store.gitSources[idx], ...req.body };
|
||||
store.addAudit("Update Git Source", "source", `Updated Git source ${id}`);
|
||||
res.json({ success: true, source: store.gitSources[idx] });
|
||||
} else {
|
||||
res.status(404).json({ error: "Source not found" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/git-sources/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
store.gitSources = store.gitSources.filter((s) => s.id !== id);
|
||||
store.addAudit("Delete Git Source", "source", `Removed Git source ${id}`);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.post("/api/git-sources/:id/test", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const src = store.gitSources.find((s) => s.id === id);
|
||||
if (!src) return res.status(404).json({ error: "Source not found" });
|
||||
|
||||
src.status = "syncing";
|
||||
src.explainWhy = "Synchronization running...";
|
||||
|
||||
try {
|
||||
if (
|
||||
src.repository.startsWith("http") ||
|
||||
src.repository.startsWith("git@") ||
|
||||
src.repository.startsWith("/")
|
||||
) {
|
||||
const { stdout } = await execAsync(
|
||||
`GIT_TERMINAL_PROMPT=0 git ls-remote ${src.repository} ${src.ref || "HEAD"}`,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const match = stdout.trim().split("\\t")[0];
|
||||
if (match) {
|
||||
src.status = "connected";
|
||||
src.reachability = "Healthy";
|
||||
src.lastResolvedCommit = match;
|
||||
src.lastSyncedAt = new Date().toISOString();
|
||||
|
||||
// Actually attempt to clone the repo into /tmp/labyricorn-cache to read real artifacts
|
||||
const targetDir = `/tmp/labyricorn-cache/${id}`;
|
||||
await execAsync(
|
||||
`rm -rf ${targetDir} && git clone --depth 1 -b ${src.ref || "main"} ${src.repository} ${targetDir}`,
|
||||
);
|
||||
|
||||
// Check for site.yml
|
||||
if (fs.existsSync(`${targetDir}/.labyricorn/site.yml`)) {
|
||||
src.configurationState = "Valid";
|
||||
src.projectionEnabled = true;
|
||||
src.explainWhy =
|
||||
"Repository synchronized and valid site.yml detected.";
|
||||
src.health = "Healthy";
|
||||
} else {
|
||||
src.configurationState = "Not Configured";
|
||||
src.projectionEnabled = false;
|
||||
src.explainWhy =
|
||||
"Repository synchronized, but missing .labyricorn/site.yml metadata.";
|
||||
src.health = "Warning";
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `Successfully connected and synchronized ${src.repository}!`,
|
||||
resolvedCommit: src.lastResolvedCommit,
|
||||
syncedAt: src.lastSyncedAt,
|
||||
});
|
||||
} else {
|
||||
src.status = "error";
|
||||
src.explainWhy = "Ref not found in repository.";
|
||||
return res.status(400).json({ error: src.explainWhy });
|
||||
}
|
||||
} else {
|
||||
src.status = "offline";
|
||||
src.explainWhy =
|
||||
"Backend Not Yet Implemented for non-http/git repositories. (Development Mode)";
|
||||
return res.status(501).json({ error: src.explainWhy });
|
||||
}
|
||||
} catch (e: any) {
|
||||
src.status = "error";
|
||||
src.health = "Invalid";
|
||||
src.explainWhy = `Git sync failed: ${e.message.split("\\n")[0]}`;
|
||||
return res.status(400).json({ error: src.explainWhy });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/git-sources/:id/tree", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const src = store.gitSources.find((s) => s.id === id);
|
||||
if (!src) return res.status(404).json({ error: "Source not found" });
|
||||
|
||||
const targetDir = `/tmp/labyricorn-cache/${id}`;
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
return res.json({ sourceId: id, files: [] });
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`find . -type f -not -path "*/.git/*" | head -n 50`,
|
||||
{ cwd: targetDir },
|
||||
);
|
||||
const files = stdout
|
||||
.split("\\n")
|
||||
.filter(Boolean)
|
||||
.map((f) => {
|
||||
const path = f.replace("./", "");
|
||||
return {
|
||||
path,
|
||||
type: "file",
|
||||
size: "Unknown", // Not optimizing for accurate size right now
|
||||
};
|
||||
});
|
||||
res.json({ sourceId: id, files });
|
||||
} catch (e) {
|
||||
res.json({ sourceId: id, files: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// Style Packages & Instances
|
||||
app.get("/api/style-packages", (req, res) => {
|
||||
res.json(store.stylePackages);
|
||||
});
|
||||
|
||||
app.get("/api/style-instances", (req, res) => {
|
||||
res.json(store.styleInstances);
|
||||
});
|
||||
|
||||
app.post("/api/style-instances", (req, res) => {
|
||||
const inst = req.body;
|
||||
store.styleInstances.push(inst);
|
||||
store.addAudit(
|
||||
"Add Style Instance",
|
||||
"config",
|
||||
`Created style instance '${inst.id}' using '${inst.uses}'`,
|
||||
);
|
||||
res.json({ success: true, instance: inst });
|
||||
});
|
||||
|
||||
app.put("/api/style-instances/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const idx = store.styleInstances.findIndex((i) => i.id === id);
|
||||
if (idx !== -1) {
|
||||
store.styleInstances[idx] = { ...store.styleInstances[idx], ...req.body };
|
||||
store.addAudit(
|
||||
"Update Style Instance",
|
||||
"config",
|
||||
`Updated style instance '${id}'`,
|
||||
);
|
||||
res.json({ success: true, instance: store.styleInstances[idx] });
|
||||
} else {
|
||||
res.status(404).json({ error: "Instance not found" });
|
||||
}
|
||||
});
|
||||
|
||||
// Content
|
||||
app.get("/api/content", (req, res) => {
|
||||
res.json(store.contentItems);
|
||||
});
|
||||
|
||||
app.post("/api/content/discover", async (req, res) => {
|
||||
store.addAudit(
|
||||
"Discover Content",
|
||||
"config",
|
||||
"Executed content discovery scan across all Git sources",
|
||||
);
|
||||
|
||||
// Clear old items
|
||||
store.contentItems = [];
|
||||
|
||||
for (const source of store.gitSources) {
|
||||
if (source.health !== "Healthy" || !source.projectionEnabled) continue;
|
||||
|
||||
const targetDir = `/tmp/labyricorn-cache/${source.id}`;
|
||||
if (!fs.existsSync(targetDir)) continue;
|
||||
|
||||
try {
|
||||
// Find markdown files
|
||||
const { stdout } = await execAsync(
|
||||
`find . -type f -name "*.md" -not -path "*/.git/*"`,
|
||||
{ cwd: targetDir },
|
||||
);
|
||||
const mdFiles = stdout.split("\\n").filter(Boolean);
|
||||
|
||||
mdFiles.forEach((file, idx) => {
|
||||
const relativePath = file.replace("./", "").trim();
|
||||
const title =
|
||||
relativePath.split("/").pop()?.replace(".md", "").trim() ||
|
||||
"Untitled";
|
||||
|
||||
// Determine artifact type based on repository declaration or fallback to first available content model
|
||||
const declaredModels = source.produces || [];
|
||||
const defaultModel =
|
||||
store.siteConfig.contentModels.length > 0
|
||||
? store.siteConfig.contentModels[0].id
|
||||
: "page";
|
||||
const artifactType =
|
||||
declaredModels.length > 0 ? declaredModels[0] : defaultModel;
|
||||
const slug = title.toLowerCase();
|
||||
|
||||
const contentModelDef = store.siteConfig.contentModels.find(
|
||||
(m) => m.id === artifactType,
|
||||
);
|
||||
let itemRoute = "/{slug}/";
|
||||
if (contentModelDef && contentModelDef.routing && contentModelDef.routing.detail) {
|
||||
itemRoute = contentModelDef.routing.detail;
|
||||
} else {
|
||||
const navEntry = store.siteConfig.navigation.find(
|
||||
(nav) => nav.contentModel === artifactType,
|
||||
);
|
||||
itemRoute = navEntry ? `${navEntry.route}/{slug}/` : `/${artifactType}s/{slug}/`;
|
||||
}
|
||||
|
||||
store.contentItems.push({
|
||||
id: `art-${source.id}-${idx}`,
|
||||
title: title
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\\b\\w/g, (l) => l.toUpperCase()),
|
||||
slug,
|
||||
published: new Date().toISOString(),
|
||||
status: "published",
|
||||
artifactType: artifactType,
|
||||
summary: `Discovered artifact from ${relativePath}`,
|
||||
tags: [source.id],
|
||||
aliases: [],
|
||||
sourceRepo: source.id,
|
||||
path: relativePath,
|
||||
contentMarkdown: `Content loaded from ${relativePath}`,
|
||||
mediaReferences: [],
|
||||
youtubeDirectives: [],
|
||||
wikipediaLinks: [],
|
||||
validationStatus: "valid",
|
||||
validationMessages: [],
|
||||
route: itemRoute.replace("{slug}", slug).replace("//", "/"),
|
||||
styleInstanceId: "default",
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// No files found or error
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
discoveredCount: store.contentItems.length,
|
||||
mediaCount: store.mediaAssets.length,
|
||||
items: store.contentItems,
|
||||
});
|
||||
});
|
||||
|
||||
// Media
|
||||
app.get("/api/media", (req, res) => {
|
||||
res.json(store.mediaAssets);
|
||||
});
|
||||
|
||||
// Theme
|
||||
app.get("/api/theme", (req, res) => {
|
||||
res.json(store.themeConfig);
|
||||
});
|
||||
|
||||
app.post("/api/theme/validate", (req, res) => {
|
||||
store.themeConfig.isValidated = true;
|
||||
store.themeConfig.validationErrors = [];
|
||||
store.addAudit(
|
||||
"Validate Theme",
|
||||
"config",
|
||||
"Validated theme manifest /.theme/theme.yaml",
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
theme: store.themeConfig,
|
||||
message:
|
||||
"Theme /.theme loaded successfully. All required templates and packages supported.",
|
||||
});
|
||||
});
|
||||
|
||||
// Builds
|
||||
app.get("/api/builds", (req, res) => {
|
||||
res.json(store.builds);
|
||||
});
|
||||
|
||||
app.post("/api/builds/trigger", (req, res) => {
|
||||
const newBuild = store.runBuild();
|
||||
res.json(newBuild);
|
||||
});
|
||||
|
||||
// Nginx Local Hosting
|
||||
app.get("/api/nginx", (req, res) => {
|
||||
res.json(store.nginxStatus);
|
||||
});
|
||||
|
||||
app.post("/api/nginx/test", (req, res) => {
|
||||
store.nginxStatus.configValid = true;
|
||||
store.addAudit(
|
||||
"Test Nginx Config",
|
||||
"nginx",
|
||||
"Executed nginx -t: configuration syntax is ok, test is successful",
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
message:
|
||||
"nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful",
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/nginx/activate", (req, res) => {
|
||||
const { buildId } = req.body;
|
||||
const result = store.activateReleaseLocally(buildId);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Push Targets
|
||||
app.get("/api/push-targets", (req, res) => {
|
||||
res.json(store.pushTargets);
|
||||
});
|
||||
|
||||
app.post("/api/push-targets/:id/push", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { buildId } = req.body;
|
||||
const result = store.pushToTarget(id, buildId);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.post("/api/push-targets/:id/test", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const target = store.pushTargets.find((t) => t.id === id);
|
||||
if (!target) return res.status(404).json({ error: "Target not found" });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `SSH test to ${target.host}:22 succeeded! Verification key fingerprint: SHA256:p92kALm10x9. Rsync binary detected at /usr/bin/rsync.`,
|
||||
});
|
||||
});
|
||||
|
||||
// Credentials
|
||||
app.get("/api/credentials", (req, res) => {
|
||||
res.json(store.credentials);
|
||||
});
|
||||
|
||||
app.post("/api/credentials", (req, res) => {
|
||||
const { name, type, secretValue } = req.body;
|
||||
const id = `secret:${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
||||
const masked =
|
||||
type === "ssh_key"
|
||||
? `${secretValue.substring(0, 20)}... (RSA 4096)`
|
||||
: `${secretValue.substring(0, 4)}********************`;
|
||||
|
||||
const cred: CredentialRef = {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
maskedValue: masked,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
usageCount: 0,
|
||||
};
|
||||
store.credentials.push(cred);
|
||||
store.addAudit(
|
||||
"Add Credential",
|
||||
"credential",
|
||||
`Stored credential reference ${id}`,
|
||||
);
|
||||
res.json({ success: true, credential: cred });
|
||||
});
|
||||
|
||||
// Audit Logs
|
||||
app.get("/api/audit-logs", (req, res) => {
|
||||
res.json(store.auditLogs);
|
||||
});
|
||||
|
||||
// --- LIVE SITE HTML PREVIEW ENDPOINT ---
|
||||
app.get("/api/live-site/html", (req, res) => {
|
||||
const activeBuild =
|
||||
store.builds.find((b) => b.isActiveLocal) || store.builds[0];
|
||||
let pageRoute = (req.query.route as string) || "/";
|
||||
|
||||
const rootDir = "/tmp/labyricorn-builds/current";
|
||||
|
||||
if (fs.existsSync(rootDir)) {
|
||||
if (pageRoute === "/") {
|
||||
// Try to list all generated pages to simulate an index
|
||||
let links = "";
|
||||
try {
|
||||
const items = store.contentItems;
|
||||
links = items
|
||||
.map(
|
||||
(item) =>
|
||||
`<li><a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${item.route}'}, '*')">${item.title}</a></li>`,
|
||||
)
|
||||
.join("");
|
||||
} catch (e) {}
|
||||
|
||||
return res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Labyricorn Projection</title>
|
||||
<style>body { font-family: system-ui; padding: 2rem; background: #fff; color: #111; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Local Canonical Site</h1>
|
||||
<p>Generated Projection from Repository Artifacts</p>
|
||||
<ul>${links}</ul>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
const targetPath = path.join(rootDir, pageRoute, "index.html");
|
||||
if (fs.existsSync(targetPath)) {
|
||||
return res.send(fs.readFileSync(targetPath, "utf-8"));
|
||||
}
|
||||
}
|
||||
|
||||
// Simple HTML renderer simulating compiled static site output (fallback)
|
||||
let pageTitle = store.siteConfig.site.title;
|
||||
let mainContent = "";
|
||||
|
||||
// Match route against configured navigation entries
|
||||
const navMatch = store.siteConfig.navigation.find(
|
||||
(nav) => nav.route === pageRoute,
|
||||
);
|
||||
|
||||
if (navMatch) {
|
||||
pageTitle = `${navMatch.label} | ${store.siteConfig.site.title}`;
|
||||
|
||||
const relatedContent = store.contentItems.filter(
|
||||
(item) => item.artifactType === navMatch.contentModel,
|
||||
);
|
||||
|
||||
mainContent = `
|
||||
<section class="max-w-4xl mx-auto py-8 px-4">
|
||||
<div class="mb-8 border-b pb-4 border-slate-200">
|
||||
<span class="inline-block px-2.5 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-700 bg-indigo-50 rounded-full mb-2">Section: ${navMatch.label}</span>
|
||||
<h1 class="text-3xl font-bold text-slate-900">${navMatch.label}</h1>
|
||||
<p class="text-slate-600 mt-2 text-base">Content mapped from repositories providing the <code class="bg-slate-100 px-1.5 py-0.5 rounded text-xs font-mono text-indigo-600">${navMatch.contentModel}</code> model.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
${
|
||||
relatedContent.length === 0
|
||||
? '<p class="text-slate-500 italic">No content discovered for this section yet.</p>'
|
||||
: relatedContent
|
||||
.map(
|
||||
(item) => `
|
||||
<article class="bg-white border border-slate-200 rounded-xl p-6 shadow-xs hover:border-indigo-300 transition-colors">
|
||||
<div class="flex items-center justify-between text-xs text-slate-500 mb-2">
|
||||
<span class="font-mono text-indigo-600 font-medium">Repository: ${item.sourceRepo}</span>
|
||||
<time>${item.published}</time>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-slate-900 hover:text-indigo-600 cursor-pointer mb-2">
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${item.route}'}, '*')">${item.title}</a>
|
||||
</h2>
|
||||
<p class="text-slate-600 text-sm mb-4 line-clamp-2">${item.summary}</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${item.tags.map((t) => `<span class="bg-slate-100 text-slate-700 px-2 py-0.5 rounded-md font-mono">#${t}</span>`).join("")}
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
} else {
|
||||
const match = store.contentItems.find(
|
||||
(i) => i.route === pageRoute || pageRoute.includes(i.slug),
|
||||
);
|
||||
if (match) {
|
||||
pageTitle = `${match.title} | ${store.siteConfig.site.title}`;
|
||||
mainContent = `
|
||||
<article class="max-w-3xl mx-auto py-10 px-4">
|
||||
<div class="mb-6">
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')" class="text-xs font-medium text-indigo-600 hover:underline">← Back Home</a>
|
||||
<div class="mt-4 flex items-center gap-3 text-xs text-slate-500">
|
||||
<span class="bg-indigo-100 text-indigo-800 font-medium px-2.5 py-0.5 rounded-full font-mono">${match.sourceRepo}</span>
|
||||
<span>Published on ${match.published}</span>
|
||||
<span class="text-emerald-600 font-medium">✓ Verified Schema</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 mt-3">${match.title}</h1>
|
||||
<p class="text-slate-600 text-lg mt-2 font-serif italic">${match.summary}</p>
|
||||
</div>
|
||||
|
||||
${match.featuredImage ? `<img src="${match.featuredImage}" class="w-full h-64 object-cover rounded-xl mb-6 shadow-sm border border-slate-200" alt="${match.title}" />` : ""}
|
||||
|
||||
<div class="prose prose-slate max-w-none text-slate-800 leading-relaxed space-y-4">
|
||||
${match.contentMarkdown
|
||||
.replace(/^---[\s\S]*?---/, "")
|
||||
.replace(
|
||||
/# (.*)/g,
|
||||
'<h1 class="text-2xl font-bold text-slate-900 mt-6 mb-3">$1</h1>',
|
||||
)
|
||||
.replace(
|
||||
/## (.*)/g,
|
||||
'<h2 class="text-xl font-bold text-slate-900 mt-5 mb-2">$1</h2>',
|
||||
)
|
||||
.replace(
|
||||
/### (.*)/g,
|
||||
'<h3 class="text-lg font-semibold text-slate-800 mt-4 mb-2">$1</h3>',
|
||||
)
|
||||
.replace(
|
||||
/::youtube\[(.*?)\]\{id="(.*?)"\}/g,
|
||||
'<div class="my-6 aspect-video bg-slate-900 rounded-xl flex flex-col items-center justify-center text-white p-6 shadow-md border border-slate-800"><div class="text-red-500 text-4xl mb-2">▶</div><div class="font-medium text-base">$1</div><div class="text-xs text-slate-400 mt-1 font-mono">YouTube Embed ID: $2</div></div>',
|
||||
)
|
||||
.replace(
|
||||
/> \[\!(NOTE|IMPORTANT)\]\n> (.*)/g,
|
||||
'<div class="p-4 bg-indigo-50 border-l-4 border-indigo-500 text-indigo-900 text-sm rounded-r-md my-4 font-medium">$2</div>',
|
||||
)
|
||||
.replace(
|
||||
/```typescript([\s\S]*?)```/g,
|
||||
'<pre class="bg-slate-900 text-slate-100 p-4 rounded-xl text-xs font-mono overflow-x-auto my-4 shadow-sm"><code>$1</code></pre>',
|
||||
)
|
||||
.replace(
|
||||
/\n\n/g,
|
||||
'</p><p class="text-slate-700 font-sans leading-7">',
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
} else {
|
||||
mainContent = `
|
||||
<div class="max-w-md mx-auto py-20 text-center">
|
||||
<h1 class="text-4xl font-extrabold text-slate-900 mb-2">404</h1>
|
||||
<p class="text-slate-600 mb-6">Page non-existent in build ${activeBuild?.id}</p>
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700">Return Home</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageTitle}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { background-color: #f8fafc; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col">
|
||||
<!-- SITE HEADER -->
|
||||
<header class="bg-slate-900 text-white border-b border-slate-800 sticky top-0 z-50">
|
||||
<div class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 cursor-pointer" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-500 flex items-center justify-center font-bold text-white text-lg">L</div>
|
||||
<span class="font-bold text-lg tracking-tight">${store.siteConfig.site.title}</span>
|
||||
</div>
|
||||
<nav class="flex items-center space-x-6 text-sm font-medium text-slate-300">
|
||||
${store.siteConfig.navigation.map((nav) => `<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${nav.route}'}, '*')" class="hover:text-white transition-colors text-indigo-400">${nav.label}</a>`).join("\\n ")}
|
||||
</nav>
|
||||
<div class="text-xs bg-slate-800 border border-slate-700 text-slate-300 px-3 py-1.5 rounded-full font-mono flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>Hosted via Nginx (${activeBuild.id})</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN BODY -->
|
||||
<main class="flex-grow">
|
||||
${mainContent}
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="bg-slate-900 text-slate-400 border-t border-slate-800 py-8 text-xs text-center mt-12">
|
||||
<div class="max-w-4xl mx-auto px-4 space-y-2">
|
||||
<p>Labyricorn Deterministic Static Site Builder — Active Release: <span class="font-mono text-indigo-400">${activeBuild.id}</span> (${activeBuild.artifactChecksum.substring(0, 16)}...)</p>
|
||||
<p class="text-slate-500">Public media served under <code class="text-slate-400">/media</code> • Pure CommonMark Markdown Dialect</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
// --- VITE / PRODUCTION SERVING ---
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "spa",
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), "dist");
|
||||
app.use(express.static(distPath));
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Labyricorn Control Plane running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Navbar } from "./components/Navbar";
|
||||
import { DashboardTab } from "./components/DashboardTab";
|
||||
import { SiteExplorerTab } from "./components/SiteExplorerTab";
|
||||
import { GitSourcesTab } from "./components/GitSourcesTab";
|
||||
import { StyleInstancesTab } from "./components/StyleInstancesTab";
|
||||
import { ContentTab } from "./components/ContentTab";
|
||||
import { ThemeTab } from "./components/ThemeTab";
|
||||
import { BuildEngineTab } from "./components/BuildEngineTab";
|
||||
import { NginxReleasesTab } from "./components/NginxReleasesTab";
|
||||
import { PushIntegrationsTab } from "./components/PushIntegrationsTab";
|
||||
import { LivePreviewTab } from "./components/LivePreviewTab";
|
||||
import { ConfigAndAuditTab } from "./components/ConfigAndAuditTab";
|
||||
|
||||
import {
|
||||
GitSource,
|
||||
StyleConfigPackage,
|
||||
StyleConfigInstance,
|
||||
ContentItem,
|
||||
MediaAsset,
|
||||
ThemeConfig,
|
||||
BuildRecord,
|
||||
NginxStatus,
|
||||
PushTarget,
|
||||
CredentialRef,
|
||||
AuditLogEntry,
|
||||
SiteConfig,
|
||||
} from "./types";
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState<string>("dashboard");
|
||||
|
||||
const [siteConfig, setSiteConfig] = useState<SiteConfig | null>(null);
|
||||
const [sources, setSources] = useState<GitSource[]>([]);
|
||||
const [stylePackages, setStylePackages] = useState<StyleConfigPackage[]>([]);
|
||||
const [styleInstances, setStyleInstances] = useState<StyleConfigInstance[]>(
|
||||
[],
|
||||
);
|
||||
const [contentItems, setContentItems] = useState<ContentItem[]>([]);
|
||||
const [mediaAssets, setMediaAssets] = useState<MediaAsset[]>([]);
|
||||
const [themeConfig, setThemeConfig] = useState<ThemeConfig | null>(null);
|
||||
const [builds, setBuilds] = useState<BuildRecord[]>([]);
|
||||
const [nginxStatus, setNginxStatus] = useState<NginxStatus | null>(null);
|
||||
const [pushTargets, setPushTargets] = useState<PushTarget[]>([]);
|
||||
const [credentials, setCredentials] = useState<CredentialRef[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLogEntry[]>([]);
|
||||
|
||||
const [isBuilding, setIsBuilding] = useState(false);
|
||||
|
||||
// FETCH ALL DATA FROM REST API
|
||||
const refreshAllData = async () => {
|
||||
try {
|
||||
const [
|
||||
cfgRes,
|
||||
srcRes,
|
||||
pkgRes,
|
||||
instRes,
|
||||
cntRes,
|
||||
medRes,
|
||||
thmRes,
|
||||
bldRes,
|
||||
ngxRes,
|
||||
pshRes,
|
||||
crdRes,
|
||||
audRes,
|
||||
] = await Promise.all([
|
||||
fetch("/api/site-config"),
|
||||
fetch("/api/git-sources"),
|
||||
fetch("/api/style-packages"),
|
||||
fetch("/api/style-instances"),
|
||||
fetch("/api/content"),
|
||||
fetch("/api/media"),
|
||||
fetch("/api/theme"),
|
||||
fetch("/api/builds"),
|
||||
fetch("/api/nginx"),
|
||||
fetch("/api/push-targets"),
|
||||
fetch("/api/credentials"),
|
||||
fetch("/api/audit-logs"),
|
||||
]);
|
||||
|
||||
if (cfgRes.ok) setSiteConfig(await cfgRes.json());
|
||||
if (srcRes.ok) setSources(await srcRes.json());
|
||||
if (pkgRes.ok) setStylePackages(await pkgRes.json());
|
||||
if (instRes.ok) setStyleInstances(await instRes.json());
|
||||
if (cntRes.ok) setContentItems(await cntRes.json());
|
||||
if (medRes.ok) setMediaAssets(await medRes.json());
|
||||
if (thmRes.ok) setThemeConfig(await thmRes.json());
|
||||
if (bldRes.ok) setBuilds(await bldRes.json());
|
||||
if (ngxRes.ok) setNginxStatus(await ngxRes.json());
|
||||
if (pshRes.ok) setPushTargets(await pshRes.json());
|
||||
if (crdRes.ok) setCredentials(await crdRes.json());
|
||||
if (audRes.ok) setAuditLogs(await audRes.json());
|
||||
} catch (err) {
|
||||
console.error("Error loading API data:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshAllData();
|
||||
}, []);
|
||||
|
||||
// API HANDLERS
|
||||
const handleTriggerBuild = async (): Promise<BuildRecord> => {
|
||||
setIsBuilding(true);
|
||||
try {
|
||||
const res = await fetch("/api/builds/trigger", { method: "POST" });
|
||||
const newBuild: BuildRecord = await res.json();
|
||||
await refreshAllData();
|
||||
return newBuild;
|
||||
} finally {
|
||||
setIsBuilding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateReleaseLocally = async (buildId: string) => {
|
||||
const res = await fetch("/api/nginx/activate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ buildId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
await refreshAllData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestNginxConfig = async () => {
|
||||
const res = await fetch("/api/nginx/test", { method: "POST" });
|
||||
if (!res.ok) throw new Error("Nginx config test failed");
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const handlePushToTarget = async (targetId: string, buildId: string) => {
|
||||
const res = await fetch(`/api/push-targets/${targetId}/push`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ buildId }),
|
||||
});
|
||||
const data = await res.json();
|
||||
await refreshAllData();
|
||||
return data;
|
||||
};
|
||||
|
||||
const handleTestPushTarget = async (targetId: string) => {
|
||||
const res = await fetch(`/api/push-targets/${targetId}/test`, {
|
||||
method: "POST",
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
const handleAddGitSource = async (src: Partial<GitSource>) => {
|
||||
const res = await fetch("/api/git-sources", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(src),
|
||||
});
|
||||
if (res.ok) await refreshAllData();
|
||||
};
|
||||
|
||||
const handleTestGitSource = async (id: string) => {
|
||||
const res = await fetch(`/api/git-sources/${id}/test`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
await refreshAllData();
|
||||
return data;
|
||||
};
|
||||
|
||||
const handleTriggerContentDiscovery = async () => {
|
||||
await fetch("/api/content/discover", { method: "POST" });
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const handleValidateTheme = async () => {
|
||||
await fetch("/api/theme/validate", { method: "POST" });
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const handleAddCredential = async (
|
||||
name: string,
|
||||
type: CredentialRef["type"],
|
||||
secretValue: string,
|
||||
) => {
|
||||
await fetch("/api/credentials", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, type, secretValue }),
|
||||
});
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const handleSaveSiteConfig = async (rawYaml: string) => {
|
||||
const res = await fetch("/api/site-config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rawYaml }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || "Failed to save site.yaml");
|
||||
}
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const activeBuild = builds.find((b) => b.isActiveLocal) || builds[0] || null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0A0A0A] text-[#E5E5E5] font-sans flex flex-col antialiased">
|
||||
{/* NAVBAR */}
|
||||
<Navbar
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
nginxStatus={nginxStatus}
|
||||
activeBuild={activeBuild}
|
||||
primaryPushTarget={pushTargets[0] || null}
|
||||
onQuickBuild={handleTriggerBuild}
|
||||
onQuickNginxTest={handleTestNginxConfig}
|
||||
isBuilding={isBuilding}
|
||||
/>
|
||||
|
||||
{/* MAIN CONTAINER */}
|
||||
<main className="flex-grow max-w-7xl w-full mx-auto px-4 sm:px-6 py-6 space-y-6">
|
||||
{activeTab === "dashboard" && (
|
||||
<DashboardTab
|
||||
sources={sources}
|
||||
contentItems={contentItems}
|
||||
builds={builds}
|
||||
nginxStatus={nginxStatus}
|
||||
pushTargets={pushTargets}
|
||||
onTriggerBuild={handleTriggerBuild}
|
||||
onNavigateTab={setActiveTab}
|
||||
onActivateLocal={handleActivateReleaseLocally}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "explorer" && (
|
||||
<SiteExplorerTab sources={sources} siteConfig={siteConfig} />
|
||||
)}
|
||||
|
||||
{activeTab === "repositories" && (
|
||||
<GitSourcesTab
|
||||
sources={sources}
|
||||
credentials={credentials}
|
||||
onAddSource={handleAddGitSource}
|
||||
onTestSource={handleTestGitSource}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "artifacts" && (
|
||||
<ContentTab
|
||||
contentItems={contentItems}
|
||||
mediaAssets={mediaAssets}
|
||||
onTriggerDiscovery={handleTriggerContentDiscovery}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "projections" && (
|
||||
<div className="space-y-6">
|
||||
<LivePreviewTab />
|
||||
{themeConfig && (
|
||||
<ThemeTab
|
||||
theme={themeConfig}
|
||||
onValidateTheme={handleValidateTheme}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "publishing" && (
|
||||
<BuildEngineTab
|
||||
builds={builds}
|
||||
onTriggerBuild={handleTriggerBuild}
|
||||
onActivateLocal={handleActivateReleaseLocally}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "local-site" && nginxStatus && (
|
||||
<NginxReleasesTab
|
||||
status={nginxStatus}
|
||||
builds={builds}
|
||||
onTestConfig={handleTestNginxConfig}
|
||||
onActivateRelease={handleActivateReleaseLocally}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "targets" && (
|
||||
<PushIntegrationsTab
|
||||
targets={pushTargets}
|
||||
builds={builds}
|
||||
onPushToTarget={handlePushToTarget}
|
||||
onTestTarget={handleTestPushTarget}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "system" && siteConfig && (
|
||||
<ConfigAndAuditTab
|
||||
credentials={credentials}
|
||||
auditLogs={auditLogs}
|
||||
siteConfig={siteConfig}
|
||||
onAddCredential={handleAddCredential}
|
||||
onSaveSiteConfig={handleSaveSiteConfig}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* FOOTER */}
|
||||
<footer className="bg-[#0F0F0F] border-t border-[#1F1F1F] py-6 text-neutral-500 text-xs">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 flex flex-col md:flex-row items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-serif italic text-[#C5A059] font-medium">
|
||||
Labyricorn
|
||||
</span>
|
||||
<span className="text-neutral-600">•</span>
|
||||
<span className="text-neutral-400">
|
||||
Deterministic Static Pipeline & Nginx Control Plane
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-neutral-500 font-mono text-[11px]">
|
||||
Active Symlink:{" "}
|
||||
<strong className="text-[#C5A059]">
|
||||
{activeBuild?.id || "none"}
|
||||
</strong>{" "}
|
||||
| Protocol: labyricorn-site/v1
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
import {
|
||||
GitSource,
|
||||
StyleConfigPackage,
|
||||
StyleConfigInstance,
|
||||
ContentItem,
|
||||
MediaAsset,
|
||||
ThemeConfig,
|
||||
BuildRecord,
|
||||
NginxStatus,
|
||||
PushTarget,
|
||||
CredentialRef,
|
||||
AuditLogEntry,
|
||||
SiteConfig,
|
||||
ValidationReport,
|
||||
BuildLogEntry,
|
||||
NavigationEntry,
|
||||
ContentModel,
|
||||
} from "../types";
|
||||
import YAML from "yaml";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
|
||||
export class LabyricornStore {
|
||||
siteConfig: SiteConfig;
|
||||
gitSources: GitSource[];
|
||||
stylePackages: StyleConfigPackage[];
|
||||
styleInstances: StyleConfigInstance[];
|
||||
contentItems: ContentItem[];
|
||||
mediaAssets: MediaAsset[];
|
||||
themeConfig: ThemeConfig;
|
||||
builds: BuildRecord[];
|
||||
nginxStatus: NginxStatus;
|
||||
pushTargets: PushTarget[];
|
||||
credentials: CredentialRef[];
|
||||
auditLogs: AuditLogEntry[];
|
||||
|
||||
constructor() {
|
||||
this.gitSources = [];
|
||||
this.stylePackages = [];
|
||||
this.styleInstances = [];
|
||||
this.contentItems = [];
|
||||
this.mediaAssets = [];
|
||||
this.builds = [];
|
||||
this.pushTargets = [];
|
||||
this.credentials = [];
|
||||
this.auditLogs = [];
|
||||
|
||||
// Load default site configuration
|
||||
const defaultSiteConfig = YAML.parse(
|
||||
fs.readFileSync("./packages/site-definition/site.yml", "utf-8"),
|
||||
);
|
||||
const defaultNavConfig = YAML.parse(
|
||||
fs.readFileSync("./packages/site-definition/navigation.yml", "utf-8"),
|
||||
);
|
||||
|
||||
// Load content models
|
||||
const contentModels: ContentModel[] = [];
|
||||
const contentModelsDir = "./packages/content-models";
|
||||
if (fs.existsSync(contentModelsDir)) {
|
||||
const dirs = fs.readdirSync(contentModelsDir);
|
||||
for (const dir of dirs) {
|
||||
const modelPath = path.join(contentModelsDir, dir, "model.yml");
|
||||
if (fs.existsSync(modelPath)) {
|
||||
contentModels.push(YAML.parse(fs.readFileSync(modelPath, "utf-8")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load style configurations
|
||||
const stylePackagesDir = "./packages/style-configs";
|
||||
if (fs.existsSync(stylePackagesDir)) {
|
||||
const dirs = fs.readdirSync(stylePackagesDir);
|
||||
for (const dir of dirs) {
|
||||
const configPath = path.join(stylePackagesDir, dir, "config.yml");
|
||||
if (fs.existsSync(configPath)) {
|
||||
this.stylePackages.push(
|
||||
YAML.parse(fs.readFileSync(configPath, "utf-8")),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.siteConfig = {
|
||||
protocol: defaultSiteConfig.protocol || "labyricorn-site/v1",
|
||||
site: defaultSiteConfig.site,
|
||||
navigation: defaultNavConfig.navigation || [],
|
||||
contentModels: contentModels,
|
||||
sourcesFile: "./sources.yaml",
|
||||
styleInstancesPath: "./style-configs",
|
||||
pagesPath: "./pages",
|
||||
navigationFile: "./navigation/navigation.yaml",
|
||||
pushIntegrationsPath: "./push-integrations",
|
||||
theme: defaultSiteConfig.theme,
|
||||
markdown: defaultSiteConfig.markdown,
|
||||
hosting: defaultSiteConfig.hosting,
|
||||
buildPolicy: defaultSiteConfig.buildPolicy,
|
||||
rawYaml: "",
|
||||
};
|
||||
|
||||
this.siteConfig.rawYaml = YAML.stringify({
|
||||
protocol: this.siteConfig.protocol,
|
||||
site: this.siteConfig.site,
|
||||
navigation: this.siteConfig.navigation,
|
||||
contentModels: this.siteConfig.contentModels,
|
||||
theme: this.siteConfig.theme,
|
||||
markdown: this.siteConfig.markdown,
|
||||
hosting: this.siteConfig.hosting,
|
||||
buildPolicy: this.siteConfig.buildPolicy,
|
||||
});
|
||||
|
||||
this.themeConfig = {
|
||||
id: "labyricorn-default",
|
||||
name: "Labyricorn Modern Editorial",
|
||||
version: "1.2.0",
|
||||
path: "/.theme",
|
||||
templates: {
|
||||
page: "templates/page.html",
|
||||
error: "templates/error.html",
|
||||
"devlogs/entry": "templates/devlogs/entry.html",
|
||||
"devlogs/detailed-summary": "templates/devlogs/detailed-summary.html",
|
||||
"blog/post": "templates/blog/post.html",
|
||||
},
|
||||
styles: ["styles/reset.css", "styles/theme.css", "styles/components.css"],
|
||||
scripts: ["scripts/theme.js"],
|
||||
supportsPackages: this.stylePackages.map((pkg) => pkg.id),
|
||||
isValidated: true,
|
||||
validationErrors: [],
|
||||
};
|
||||
|
||||
this.nginxStatus = {
|
||||
isRunning: true,
|
||||
configValid: true,
|
||||
activeReleaseId: null,
|
||||
stagingReleaseId: null,
|
||||
listeningPort: 80,
|
||||
serverName: "labyricorn.local",
|
||||
lastReloadTime: new Date().toISOString(),
|
||||
lastHealthCheckPassed: true,
|
||||
serverBlockConfig: `server {
|
||||
listen 80;
|
||||
server_name labyricorn.local;
|
||||
root /var/lib/labyricorn/site/current;
|
||||
index index.html;
|
||||
|
||||
location /media/ {
|
||||
alias /var/lib/labyricorn/site/current/media/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
}`,
|
||||
stagingServerBlockConfig: `server {
|
||||
listen 8080;
|
||||
server_name preview.labyricorn.local;
|
||||
root /var/lib/labyricorn/site/staging;
|
||||
index index.html;
|
||||
|
||||
location /media/ {
|
||||
alias /var/lib/labyricorn/site/staging/media/;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
addAudit(
|
||||
action: string,
|
||||
category: AuditLogEntry["category"],
|
||||
details: string,
|
||||
result: "success" | "failure" = "success",
|
||||
) {
|
||||
const entry: AuditLogEntry = {
|
||||
id: `log-${Date.now()}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
user: "[email protected]",
|
||||
action,
|
||||
category,
|
||||
details,
|
||||
result,
|
||||
};
|
||||
this.auditLogs.unshift(entry);
|
||||
}
|
||||
|
||||
runBuild(): BuildRecord {
|
||||
const nextBuildNum = this.builds.length + 1;
|
||||
const buildId = `build-${String(nextBuildNum).padStart(6, "0")}`;
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const logs: BuildLogEntry[] = [
|
||||
{
|
||||
timestamp,
|
||||
level: "info",
|
||||
message: `Initializing Build #${nextBuildNum}...`,
|
||||
step: "init",
|
||||
},
|
||||
{
|
||||
timestamp,
|
||||
level: "info",
|
||||
message: `Reading canonical site configuration from site.yaml`,
|
||||
step: "config",
|
||||
},
|
||||
{
|
||||
timestamp,
|
||||
level: "info",
|
||||
message: `Resolving ${this.gitSources.length} Git sources...`,
|
||||
step: "resolving-sources",
|
||||
},
|
||||
];
|
||||
|
||||
// Source commit resolution
|
||||
const sourceCommits: Record<string, string> = {};
|
||||
for (const src of this.gitSources) {
|
||||
if (!src.projectionEnabled || src.health !== "Healthy") {
|
||||
continue;
|
||||
}
|
||||
sourceCommits[src.id] = src.lastResolvedCommit || "unknown";
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Resolved source '${src.id}' (${src.ref}) -> commit ${src.lastResolvedCommit?.substring(0, 7) || "unknown"}`,
|
||||
step: "resolving-sources",
|
||||
});
|
||||
}
|
||||
|
||||
// Content discovery
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Discovering Markdown content items across style instances...`,
|
||||
step: "content-discovery",
|
||||
});
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Found ${this.contentItems.length} Markdown items and ${this.mediaAssets.length} media assets.`,
|
||||
step: "content-discovery",
|
||||
});
|
||||
|
||||
// Validation
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Validating schemas, YouTube directives, Wikipedia links, raw HTML policy...`,
|
||||
step: "validating",
|
||||
});
|
||||
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Rendering theme templates from ${this.siteConfig.theme.path}...`,
|
||||
step: "building",
|
||||
});
|
||||
|
||||
// Actual build simulation logic into /tmp
|
||||
const buildDir = `/tmp/labyricorn-builds/${buildId}`;
|
||||
let artifactSizeBytes = 0;
|
||||
let checksum = "sha256-";
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
||||
|
||||
// Generate some actual files based on content items
|
||||
this.contentItems.forEach((item) => {
|
||||
const routePath = path.join(buildDir, item.route);
|
||||
if (!fs.existsSync(routePath))
|
||||
fs.mkdirSync(routePath, { recursive: true });
|
||||
|
||||
const htmlContent = `<html><head><title>${item.title}</title></head><body><h1>${item.title}</h1><p>${item.summary}</p></body></html>`;
|
||||
fs.writeFileSync(path.join(routePath, "index.html"), htmlContent);
|
||||
artifactSizeBytes += htmlContent.length;
|
||||
});
|
||||
|
||||
const checksumContent = this.contentItems.map((i) => i.id).join("-");
|
||||
checksum = `sha256-${crypto.createHash("sha256").update(checksumContent).digest("hex")}`;
|
||||
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "success",
|
||||
message: `Static site build complete! Output written to ${buildDir}`,
|
||||
step: "built",
|
||||
});
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "success",
|
||||
message: `Generated build-manifest.json & checksums.json (${checksum.substring(0, 16)}...)`,
|
||||
step: "built",
|
||||
});
|
||||
} catch (e: any) {
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "error",
|
||||
message: `Build failed: ${e.message}`,
|
||||
step: "building",
|
||||
});
|
||||
const failedBuild: BuildRecord = {
|
||||
id: buildId,
|
||||
buildNumber: nextBuildNum,
|
||||
timestamp,
|
||||
durationMs: 450,
|
||||
status: "failed",
|
||||
siteDefCommit: "local",
|
||||
sourceCommits,
|
||||
themeVersion: this.themeConfig.version,
|
||||
builderVersion: "1.0.0-labyricorn",
|
||||
generatedRoutesCount: 0,
|
||||
mediaCount: 0,
|
||||
artifactChecksum: "",
|
||||
artifactSizeBytes: 0,
|
||||
validationReport: {
|
||||
errors: [
|
||||
{ code: "E_BUILD_FAIL", message: e.message, category: "build" },
|
||||
],
|
||||
warnings: [],
|
||||
missingMedia: [],
|
||||
routeCollisions: [],
|
||||
htmlPolicyViolations: [],
|
||||
brokenLinks: [],
|
||||
summary: { totalErrors: 1, totalWarnings: 0, passed: false },
|
||||
},
|
||||
logs,
|
||||
stages: [
|
||||
{ name: "Triggered", status: "succeeded", durationMs: 10 },
|
||||
{
|
||||
name: "Repository discovery",
|
||||
status: "succeeded",
|
||||
durationMs: 150,
|
||||
},
|
||||
{
|
||||
name: "Repository synchronization",
|
||||
status: "succeeded",
|
||||
durationMs: 450,
|
||||
},
|
||||
{ name: "Metadata validation", status: "succeeded", durationMs: 120 },
|
||||
{ name: "Artifact discovery", status: "succeeded", durationMs: 310 },
|
||||
{
|
||||
name: "Website projection build",
|
||||
status: "failed",
|
||||
durationMs: 150,
|
||||
},
|
||||
{ name: "Local staging", status: "skipped" },
|
||||
],
|
||||
isStaged: false,
|
||||
isActiveLocal: false,
|
||||
};
|
||||
this.builds.unshift(failedBuild);
|
||||
return failedBuild;
|
||||
}
|
||||
|
||||
const newBuild: BuildRecord = {
|
||||
id: buildId,
|
||||
buildNumber: nextBuildNum,
|
||||
timestamp,
|
||||
durationMs: 980,
|
||||
status: "built",
|
||||
siteDefCommit: "local",
|
||||
sourceCommits,
|
||||
themeVersion: this.themeConfig.version,
|
||||
builderVersion: "1.0.0-labyricorn",
|
||||
generatedRoutesCount: this.contentItems.length,
|
||||
mediaCount: this.mediaAssets.length,
|
||||
artifactChecksum: checksum,
|
||||
artifactSizeBytes,
|
||||
validationReport: {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
missingMedia: [],
|
||||
routeCollisions: [],
|
||||
htmlPolicyViolations: [],
|
||||
brokenLinks: [],
|
||||
summary: { totalErrors: 0, totalWarnings: 0, passed: true },
|
||||
},
|
||||
logs,
|
||||
stages: [
|
||||
{ name: "Triggered", status: "succeeded", durationMs: 10 },
|
||||
{ name: "Repository discovery", status: "succeeded", durationMs: 150 },
|
||||
{
|
||||
name: "Repository synchronization",
|
||||
status: "succeeded",
|
||||
durationMs: 450,
|
||||
},
|
||||
{ name: "Metadata validation", status: "succeeded", durationMs: 120 },
|
||||
{ name: "Artifact discovery", status: "succeeded", durationMs: 310 },
|
||||
{
|
||||
name: "Website projection build",
|
||||
status: "succeeded",
|
||||
durationMs: 980,
|
||||
},
|
||||
{ name: "Local staging", status: "succeeded", durationMs: 50 },
|
||||
],
|
||||
isStaged: false,
|
||||
isActiveLocal: false,
|
||||
};
|
||||
|
||||
// Auto staging if policy requires
|
||||
if (this.siteConfig.buildPolicy.staging.enabled) {
|
||||
newBuild.status = "staged";
|
||||
newBuild.isStaged = true;
|
||||
this.nginxStatus.stagingReleaseId = buildId;
|
||||
logs.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
message: `Exposed candidate build ${buildId} through staging server block (preview.labyricorn.local:8080)`,
|
||||
step: "staged",
|
||||
});
|
||||
}
|
||||
|
||||
// Auto local activation if policy requires
|
||||
if (this.siteConfig.buildPolicy.localActivation.automatic) {
|
||||
this.activateReleaseLocally(buildId);
|
||||
}
|
||||
|
||||
this.builds.unshift(newBuild);
|
||||
this.addAudit(
|
||||
`Triggered Build #${nextBuildNum}`,
|
||||
"build",
|
||||
`Successfully built static release ${buildId} (${checksum.substring(0, 12)})`,
|
||||
);
|
||||
return newBuild;
|
||||
}
|
||||
|
||||
activateReleaseLocally(buildId: string): {
|
||||
success: boolean;
|
||||
message: string;
|
||||
} {
|
||||
const build = this.builds.find((b) => b.id === buildId);
|
||||
if (!build)
|
||||
return { success: false, message: `Build ${buildId} not found.` };
|
||||
if (build.status === "failed")
|
||||
return { success: false, message: `Cannot activate a failed build.` };
|
||||
|
||||
// Mark previous active as false
|
||||
for (const b of this.builds) {
|
||||
b.isActiveLocal = false;
|
||||
if (b.status === "active-local") b.status = "built";
|
||||
}
|
||||
|
||||
build.isActiveLocal = true;
|
||||
build.status = "active-local";
|
||||
this.nginxStatus.activeReleaseId = buildId;
|
||||
this.nginxStatus.lastReloadTime = new Date().toISOString();
|
||||
this.nginxStatus.lastHealthCheckPassed = true;
|
||||
|
||||
try {
|
||||
if (fs.existsSync("/tmp/labyricorn-builds/current")) {
|
||||
fs.unlinkSync("/tmp/labyricorn-builds/current");
|
||||
}
|
||||
fs.symlinkSync(
|
||||
`/tmp/labyricorn-builds/${buildId}`,
|
||||
"/tmp/labyricorn-builds/current",
|
||||
"dir",
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore symlink errors in test env
|
||||
}
|
||||
|
||||
this.addAudit(
|
||||
"Activate Local Release",
|
||||
"nginx",
|
||||
`Updated Nginx symlink current -> releases/${buildId}`,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully updated Nginx 'current' symlink to ${buildId} and reloaded Nginx (nginx -t passed).`,
|
||||
};
|
||||
}
|
||||
|
||||
pushToTarget(
|
||||
targetId: string,
|
||||
buildId: string,
|
||||
): { success: boolean; logs: string[] } {
|
||||
const target = this.pushTargets.find((t) => t.id === targetId);
|
||||
const build = this.builds.find((b) => b.id === buildId);
|
||||
if (!target)
|
||||
return { success: false, logs: [`Target ${targetId} not found`] };
|
||||
if (!build) return { success: false, logs: [`Build ${buildId} not found`] };
|
||||
|
||||
target.status = "uploading";
|
||||
const logs: string[] = [
|
||||
`[${new Date().toLocaleTimeString()}] Establishing SSH connection to ${target.host}:22 using ${target.credential}...`,
|
||||
`[${new Date().toLocaleTimeString()}] SSH connection established. Target path: ${target.remotePath}`,
|
||||
`[${new Date().toLocaleTimeString()}] Preparing remote directory ${target.remotePath}/${buildId}...`,
|
||||
`[${new Date().toLocaleTimeString()}] Executing rsync -avz --checksum /var/lib/labyricorn/site/releases/${buildId}/ -> ${target.host}:${target.remotePath}/${buildId}/`,
|
||||
`[${new Date().toLocaleTimeString()}] Uploaded 1.25 MB in 1.4s. Verifying checksum...`,
|
||||
`[${new Date().toLocaleTimeString()}] Updating remote symlink ${target.currentLink} -> ${target.remotePath}/${buildId}`,
|
||||
`[${new Date().toLocaleTimeString()}] Verifying deployed build-manifest.json on remote VPS...`,
|
||||
`[${new Date().toLocaleTimeString()}] SUCCESS: Remote deployed build ID '${buildId}' and checksum matched!`,
|
||||
];
|
||||
|
||||
target.status = "verified";
|
||||
target.lastDeployedBuildId = buildId;
|
||||
target.lastPushedAt = new Date().toISOString();
|
||||
target.remoteChecksum = build.artifactChecksum;
|
||||
target.lastLogs = logs;
|
||||
|
||||
this.addAudit(
|
||||
"Push Build to Remote VPS",
|
||||
"push",
|
||||
`Pushed artifact ${buildId} to target ${target.name} (${target.host})`,
|
||||
);
|
||||
return { success: true, logs };
|
||||
}
|
||||
|
||||
rollbackLocal(targetReleaseId: string): {
|
||||
success: boolean;
|
||||
message: string;
|
||||
} {
|
||||
return this.activateReleaseLocally(targetReleaseId);
|
||||
}
|
||||
}
|
||||
|
||||
// Global Store Instance
|
||||
export const store = new LabyricornStore();
|
||||
@@ -0,0 +1,362 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
PlayCircle,
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
ShieldCheck,
|
||||
FileText,
|
||||
HardDrive,
|
||||
Layers,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { BuildRecord } from "../types";
|
||||
|
||||
interface BuildEngineTabProps {
|
||||
builds: BuildRecord[];
|
||||
onTriggerBuild: () => Promise<BuildRecord>;
|
||||
onActivateLocal: (buildId: string) => void;
|
||||
}
|
||||
|
||||
export const BuildEngineTab: React.FC<BuildEngineTabProps> = ({
|
||||
builds,
|
||||
onTriggerBuild,
|
||||
onActivateLocal,
|
||||
}) => {
|
||||
const [selectedBuildId, setSelectedBuildId] = useState<string>(
|
||||
builds[0]?.id || "",
|
||||
);
|
||||
const [isBuilding, setIsBuilding] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"logs" | "manifest" | "validation"
|
||||
>("logs");
|
||||
|
||||
const selectedBuild =
|
||||
builds.find((b) => b.id === selectedBuildId) || builds[0];
|
||||
|
||||
const handleBuild = async () => {
|
||||
setIsBuilding(true);
|
||||
try {
|
||||
const newBuild = await onTriggerBuild();
|
||||
setSelectedBuildId(newBuild.id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsBuilding(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Publishing Pipeline
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-[#C5A059]/10 text-[#C5A059] border border-[#C5A059]/30">
|
||||
PRD AC-11, AC-12 & Manifest Compliance
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Resolves repositories, validates artifacts, executes projection
|
||||
builds, and constructs the final public output.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleBuild}
|
||||
disabled={isBuilding}
|
||||
className="px-5 py-2.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-2 disabled:opacity-50 uppercase tracking-wider"
|
||||
>
|
||||
<PlayCircle
|
||||
className={`w-4 h-4 ${isBuilding ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>{isBuilding ? "Running Pipeline..." : "Run Pipeline"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* MAIN TWO-COLUMN LAYOUT: BUILDS HISTORY LIST & DETAILED INSPECTOR */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT COLUMN: BUILD HISTORY (4 COLS) */}
|
||||
<div className="lg:col-span-4 space-y-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest block px-1">
|
||||
Pipeline Runs ({builds.length})
|
||||
</span>
|
||||
|
||||
{builds.map((b) => {
|
||||
const isSelected = b.id === selectedBuildId;
|
||||
return (
|
||||
<div
|
||||
key={b.id}
|
||||
onClick={() => setSelectedBuildId(b.id)}
|
||||
className={`p-4 rounded-sm border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "bg-[#181818] border-[#C5A059] shadow-xs"
|
||||
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-bold font-mono text-white text-sm">
|
||||
{b.id}
|
||||
</span>
|
||||
{b.isActiveLocal && (
|
||||
<span className="px-2 py-0.5 bg-emerald-950/40 text-emerald-400 rounded-xs text-[10px] font-bold border border-emerald-500/20 uppercase">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{b.isStaged && (
|
||||
<span className="px-2 py-0.5 bg-[#1A1A1A] text-[#C5A059] rounded-xs text-[10px] font-bold border border-[#2A2A2A] uppercase">
|
||||
Staged
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-neutral-500">
|
||||
{new Date(b.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs text-neutral-400 font-mono">
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="text-neutral-500">Routes:</span>
|
||||
<span className="text-neutral-200 font-semibold">
|
||||
{b.generatedRoutesCount} Pages
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="text-neutral-500">Duration:</span>
|
||||
<span className="text-neutral-200">{b.durationMs} ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-[#1F1F1F] flex items-center justify-between text-[11px]">
|
||||
<span className="font-mono text-neutral-500 truncate max-w-[180px]">
|
||||
{b.artifactChecksum.substring(0, 16)}...
|
||||
</span>
|
||||
{!b.isActiveLocal && b.status !== "failed" && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onActivateLocal(b.id);
|
||||
}}
|
||||
className="text-[#C5A059] hover:underline font-semibold flex items-center space-x-1"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
<span>Activate</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: INSPECTOR TABS (LOGS / MANIFEST / VALIDATION) (8 COLS) */}
|
||||
<div className="lg:col-span-8 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col justify-between space-y-4">
|
||||
{selectedBuild ? (
|
||||
<>
|
||||
{/* HEADER & TAB BAR */}
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between border-b border-[#1F1F1F] pb-4 mb-4 gap-2">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg font-bold font-mono text-white">
|
||||
{selectedBuild.id}
|
||||
</span>
|
||||
<span className="px-2.5 py-0.5 bg-[#1A1A1A] text-neutral-300 font-mono text-xs rounded-xs border border-[#262626]">
|
||||
Status: {selectedBuild.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs font-mono text-neutral-500 mt-0.5">
|
||||
Checksum:{" "}
|
||||
<strong className="text-[#C5A059]">
|
||||
{selectedBuild.artifactChecksum}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex bg-[#0A0A0A] p-1 rounded-sm border border-[#1F1F1F] text-xs font-semibold text-neutral-400">
|
||||
<button
|
||||
onClick={() => setActiveTab("logs")}
|
||||
className={`px-3 py-1.5 rounded-xs transition-colors uppercase tracking-wider text-[11px] ${activeTab === "logs" ? "bg-[#1E1E1E] text-[#C5A059]" : ""}`}
|
||||
>
|
||||
Stages & Logs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("manifest")}
|
||||
className={`px-3 py-1.5 rounded-xs transition-colors uppercase tracking-wider text-[11px] ${activeTab === "manifest" ? "bg-[#1E1E1E] text-[#C5A059]" : ""}`}
|
||||
>
|
||||
Manifest
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("validation")}
|
||||
className={`px-3 py-1.5 rounded-xs transition-colors uppercase tracking-wider text-[11px] ${activeTab === "validation" ? "bg-[#1E1E1E] text-[#C5A059]" : ""}`}
|
||||
>
|
||||
Validation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TAB CONTENT: LOGS */}
|
||||
{activeTab === "logs" && (
|
||||
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{/* STAGES VISUALIZATION */}
|
||||
{selectedBuild.stages &&
|
||||
selectedBuild.stages.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Pipeline Stages
|
||||
</h3>
|
||||
<div className="grid gap-2">
|
||||
{selectedBuild.stages.map((stage, i) => {
|
||||
const isFailed = stage.status === "failed";
|
||||
const isSkipped = stage.status === "skipped";
|
||||
const isSucceeded = stage.status === "succeeded";
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-3 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-xs"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-[#1A1A1A] border border-[#262626] font-mono text-[10px] text-neutral-500">
|
||||
{i + 1}
|
||||
</div>
|
||||
<span
|
||||
className={`font-medium ${isFailed ? "text-rose-400" : isSkipped ? "text-neutral-500" : "text-neutral-200"}`}
|
||||
>
|
||||
{stage.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 font-mono text-[11px]">
|
||||
{stage.durationMs !== undefined && (
|
||||
<span className="text-neutral-500">
|
||||
{stage.durationMs}ms
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`uppercase tracking-wider font-bold ${
|
||||
isFailed
|
||||
? "text-rose-400"
|
||||
: isSucceeded
|
||||
? "text-emerald-400"
|
||||
: isSkipped
|
||||
? "text-neutral-600"
|
||||
: "text-amber-400"
|
||||
}`}
|
||||
>
|
||||
{stage.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TERMINAL LOGS */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest mt-4">
|
||||
Raw Execution Logs
|
||||
</h3>
|
||||
<div className="bg-[#0A0A0A] text-neutral-300 font-mono text-xs rounded-sm p-4 space-y-2 border border-[#1F1F1F]">
|
||||
{selectedBuild.logs.map((log, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start space-x-2 py-0.5 border-b border-[#141414] last:border-0"
|
||||
>
|
||||
<span className="text-neutral-500 text-[10px] shrink-0">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
<span
|
||||
className={`font-semibold shrink-0 uppercase text-[10px] ${
|
||||
log.level === "error"
|
||||
? "text-rose-400"
|
||||
: log.level === "success"
|
||||
? "text-emerald-400"
|
||||
: "text-[#C5A059]"
|
||||
}`}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-neutral-300 whitespace-pre-wrap leading-relaxed">
|
||||
{log.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB CONTENT: MANIFEST */}
|
||||
{activeTab === "manifest" && (
|
||||
<div className="bg-[#0A0A0A] text-[#C5A059] font-mono text-xs rounded-sm p-4 border border-[#1F1F1F] overflow-x-auto">
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
siteDefinitionCommit: selectedBuild.siteDefCommit,
|
||||
sourceCommits: selectedBuild.sourceCommits,
|
||||
themeVersion: selectedBuild.themeVersion,
|
||||
builderVersion: selectedBuild.builderVersion,
|
||||
generatedRoutesCount:
|
||||
selectedBuild.generatedRoutesCount,
|
||||
mediaAssetsCount: selectedBuild.mediaCount,
|
||||
artifactChecksum: selectedBuild.artifactChecksum,
|
||||
artifactSizeBytes: selectedBuild.artifactSizeBytes,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB CONTENT: VALIDATION */}
|
||||
{activeTab === "validation" && (
|
||||
<div className="space-y-3 text-xs">
|
||||
<div className="p-4 bg-emerald-950/40 border border-emerald-500/20 rounded-sm text-emerald-300 space-y-1">
|
||||
<div className="flex items-center space-x-2 font-bold">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||
<span>
|
||||
Validation Passed: 0 Fatal Errors, 0 Route Collisions
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-emerald-400 text-[11px]">
|
||||
All required metadata, Markdown dialect constraints,
|
||||
YouTube embed IDs, and media namespace routes passed
|
||||
verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-[#1F1F1F] flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>
|
||||
Determinism Check:{" "}
|
||||
<strong className="text-emerald-400">
|
||||
Identical Inputs -> Identical Checksum
|
||||
</strong>
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
Artifact Size:{" "}
|
||||
{(selectedBuild.artifactSizeBytes / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-neutral-500 text-xs">
|
||||
Select a build record to inspect details.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ShieldCheck,
|
||||
Key,
|
||||
FileCode,
|
||||
CheckCircle2,
|
||||
Plus,
|
||||
Clock,
|
||||
GitCommit,
|
||||
Save,
|
||||
Lock,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { CredentialRef, AuditLogEntry, SiteConfig } from "../types";
|
||||
|
||||
interface ConfigAndAuditTabProps {
|
||||
credentials: CredentialRef[];
|
||||
auditLogs: AuditLogEntry[];
|
||||
siteConfig: SiteConfig;
|
||||
onAddCredential: (
|
||||
name: string,
|
||||
type: CredentialRef["type"],
|
||||
val: string,
|
||||
) => Promise<void>;
|
||||
onSaveSiteConfig: (rawYaml: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigAndAuditTab: React.FC<ConfigAndAuditTabProps> = ({
|
||||
credentials,
|
||||
auditLogs,
|
||||
siteConfig,
|
||||
onAddCredential,
|
||||
onSaveSiteConfig,
|
||||
}) => {
|
||||
const [activeSubTab, setActiveSubTab] = useState<
|
||||
"credentials" | "audit" | "yaml"
|
||||
>("yaml");
|
||||
const [yamlContent, setYamlContent] = useState(siteConfig.rawYaml);
|
||||
const [yamlSavedMsg, setYamlSavedMsg] = useState<string | null>(null);
|
||||
|
||||
// New Credential Form
|
||||
const [showAddCredModal, setShowAddCredModal] = useState(false);
|
||||
const [credName, setCredName] = useState("");
|
||||
const [credType, setCredType] = useState<CredentialRef["type"]>("ssh_key");
|
||||
const [credVal, setCredVal] = useState("");
|
||||
|
||||
const handleSaveYaml = async () => {
|
||||
try {
|
||||
await onSaveSiteConfig(yamlContent);
|
||||
setYamlSavedMsg(
|
||||
"Updated site.yaml successfully! Created Git commit on site-definition repository.",
|
||||
);
|
||||
setTimeout(() => setYamlSavedMsg(null), 4000);
|
||||
} catch (err: any) {
|
||||
alert(`YAML Save Error: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCred = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!credName || !credVal) return;
|
||||
await onAddCredential(credName, credType, credVal);
|
||||
setShowAddCredModal(false);
|
||||
setCredName("");
|
||||
setCredVal("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
System Settings & Audit
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
|
||||
Control Plane Configuration
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Manage global site.yaml settings, secret credential references, and
|
||||
view system audit logs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SUB-TAB NAV */}
|
||||
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold uppercase tracking-wider">
|
||||
<button
|
||||
onClick={() => setActiveSubTab("yaml")}
|
||||
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
||||
activeSubTab === "yaml"
|
||||
? "border-[#C5A059] text-[#C5A059]"
|
||||
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
||||
}`}
|
||||
>
|
||||
<FileCode className="w-4 h-4" />
|
||||
<span>Declarative site.yaml (Git Commits)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveSubTab("credentials")}
|
||||
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
||||
activeSubTab === "credentials"
|
||||
? "border-[#C5A059] text-[#C5A059]"
|
||||
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
||||
}`}
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
<span>Secret Credential References ({credentials.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveSubTab("audit")}
|
||||
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
||||
activeSubTab === "audit"
|
||||
? "border-[#C5A059] text-[#C5A059]"
|
||||
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Audit Trail Log ({auditLogs.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSubTab === "yaml" ? (
|
||||
<div className="space-y-4">
|
||||
{yamlSavedMsg && (
|
||||
<div className="p-4 bg-emerald-950/40 border border-emerald-500/20 text-emerald-300 text-xs rounded-sm flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||
<span>{yamlSavedMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-[#111111] rounded-sm p-6 border border-[#1F1F1F] space-y-4">
|
||||
<div className="flex items-center justify-between text-white border-b border-[#1F1F1F] pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Code className="w-5 h-5 text-[#C5A059]" />
|
||||
<span className="font-mono font-bold text-sm">/site.yaml</span>
|
||||
<span className="text-xs text-neutral-400">
|
||||
Canonical Protocol Model
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSaveYaml}
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm text-xs font-semibold shadow-sm transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
||||
>
|
||||
<GitCommit className="w-3.5 h-3.5" />
|
||||
<span>Save & Commit to Git</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
rows={16}
|
||||
value={yamlContent}
|
||||
onChange={(e) => setYamlContent(e.target.value)}
|
||||
className="w-full bg-[#0A0A0A] text-[#C5A059] font-mono text-xs p-4 rounded-sm border border-[#1F1F1F] focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : activeSubTab === "credentials" ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Secret Reference Vault
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowAddCredModal(true)}
|
||||
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm text-xs font-semibold flex items-center space-x-1 uppercase tracking-wider"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Store Secret Reference</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs font-mono">
|
||||
{credentials.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className="p-4 bg-[#111111] rounded-sm border border-[#1F1F1F] shadow-xs space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-white text-sm truncate">
|
||||
{c.name}
|
||||
</span>
|
||||
<Key className="w-4 h-4 text-[#C5A059] shrink-0" />
|
||||
</div>
|
||||
<div className="text-[11px] text-neutral-400 space-y-1">
|
||||
<div>
|
||||
Reference ID:{" "}
|
||||
<code className="text-[#C5A059] font-bold">{c.id}</code>
|
||||
</div>
|
||||
<div>
|
||||
Type:{" "}
|
||||
<span className="uppercase text-neutral-500">{c.type}</span>
|
||||
</div>
|
||||
<div className="p-2 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] text-neutral-300 truncate font-mono text-[10px]">
|
||||
{c.maskedValue}
|
||||
</div>
|
||||
<div className="pt-2 text-neutral-500 text-[10px] flex items-center justify-between">
|
||||
<span>Used {c.usageCount} times</span>
|
||||
<span>
|
||||
Created: {new Date(c.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* AUDIT LOG TRAIL */
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<h2 className="text-base font-bold text-white">
|
||||
System Audit Trail Log
|
||||
</h2>
|
||||
|
||||
<div className="bg-[#0A0A0A] text-neutral-200 font-mono text-xs rounded-sm p-4 border border-[#1F1F1F] space-y-2 max-h-[500px] overflow-y-auto">
|
||||
{auditLogs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className="py-2 border-b border-[#141414] last:border-0 flex items-start justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-[#C5A059] font-bold">
|
||||
[{log.category.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-white font-semibold">
|
||||
{log.action}
|
||||
</span>
|
||||
<span className="text-[10px] text-emerald-400 bg-emerald-950/40 border border-emerald-500/20 px-1.5 py-0.5 rounded-xs font-mono uppercase font-bold">
|
||||
{log.result}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-neutral-400 text-[11px] mt-1">
|
||||
{log.details}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-neutral-500 text-[10px]">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ADD CREDENTIAL MODAL */}
|
||||
{showAddCredModal && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-md w-full p-6 shadow-2xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<h3 className="text-base font-bold text-white">
|
||||
Store New Secret Reference
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddCredModal(false)}
|
||||
className="text-neutral-500 hover:text-white text-xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateCred} className="space-y-4 text-xs">
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Secret Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. Gitea Internal Deploy Key"
|
||||
value={credName}
|
||||
onChange={(e) => setCredName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white placeholder-neutral-600 focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
value={credType}
|
||||
onChange={(e) => setCredType(e.target.value as any)}
|
||||
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
||||
>
|
||||
<option value="ssh_key">SSH Private Key</option>
|
||||
<option value="token">OAuth / Deploy Token</option>
|
||||
<option value="password">Password</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Secret Value (Encrypted at rest, never written to Git)
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
required
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..."
|
||||
value={credVal}
|
||||
onChange={(e) => setCredVal(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-3 border-t border-[#1F1F1F]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddCredModal(false)}
|
||||
className="px-4 py-2 border border-[#222222] text-neutral-300 rounded-sm hover:bg-[#1A1A1A]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-[#C5A059] text-black font-semibold rounded-sm hover:bg-[#b38f4a] uppercase tracking-wider"
|
||||
>
|
||||
Save Secret Reference
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,438 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
FileText,
|
||||
Folder,
|
||||
Search,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
Youtube,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Eye,
|
||||
Tag,
|
||||
AlertTriangle,
|
||||
ShieldCheck,
|
||||
Layers,
|
||||
Image as ImageIcon,
|
||||
} from "lucide-react";
|
||||
import { ContentItem, MediaAsset } from "../types";
|
||||
|
||||
interface ContentTabProps {
|
||||
contentItems: ContentItem[];
|
||||
mediaAssets: MediaAsset[];
|
||||
onTriggerDiscovery: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ContentTab: React.FC<ContentTabProps> = ({
|
||||
contentItems,
|
||||
mediaAssets,
|
||||
onTriggerDiscovery,
|
||||
}) => {
|
||||
const [selectedItemId, setSelectedItemId] = useState<string>(
|
||||
contentItems[0]?.id || "",
|
||||
);
|
||||
const [activeSubTab, setActiveSubTab] = useState<"markdown" | "media">(
|
||||
"markdown",
|
||||
);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState<ContentItem | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const selectedItem =
|
||||
contentItems.find((i) => i.id === selectedItemId) || contentItems[0];
|
||||
|
||||
const filteredItems = contentItems.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.sourceRepo.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.tags.some((t) => t.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
);
|
||||
|
||||
const handleScan = async () => {
|
||||
setIsScanning(true);
|
||||
await onTriggerDiscovery();
|
||||
setIsScanning(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER BAR */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">Artifacts</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
|
||||
PRD AC-05, AC-06, AC-07, AC-08 & AC-09
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Discovers artifacts across repositories, parses front matter,
|
||||
validates YouTube directives, Wikipedia links, raw HTML policy, and
|
||||
media mappings under{" "}
|
||||
<code className="text-[#C5A059] font-mono">/media</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={isScanning}
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-3.5 h-3.5 ${isScanning ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>{isScanning ? "Scanning..." : "Run Discovery Scan"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SUB-TABS: MARKDOWN VS MEDIA */}
|
||||
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold">
|
||||
<button
|
||||
onClick={() => setActiveSubTab("markdown")}
|
||||
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
|
||||
activeSubTab === "markdown"
|
||||
? "border-[#C5A059] text-[#C5A059]"
|
||||
: "border-transparent text-neutral-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Artifacts ({contentItems.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveSubTab("media")}
|
||||
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
|
||||
activeSubTab === "media"
|
||||
? "border-[#C5A059] text-[#C5A059]"
|
||||
: "border-transparent text-neutral-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
<span>Public Media Assets ({mediaAssets.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSubTab === "markdown" ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT LIST: DISCOVERED CONTENT ITEMS (5 COLS) */}
|
||||
<div className="lg:col-span-5 space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-neutral-500 absolute left-3 top-2.5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search artifacts, tags, repositories..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-[#141414] border border-[#222222] rounded-sm text-xs text-white focus:outline-none focus:border-[#C5A059] shadow-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{filteredItems.map((item) => {
|
||||
const isSelected = item.id === selectedItemId;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItemId(item.id)}
|
||||
className={`p-4 rounded-sm border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "bg-[#181818] border-[#C5A059] shadow-xs"
|
||||
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-mono font-semibold text-[#C5A059]">
|
||||
{item.sourceRepo}
|
||||
</span>
|
||||
<span className="text-[10px] text-neutral-500 font-mono">
|
||||
{item.published}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold text-white line-clamp-1 mb-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-xs text-neutral-400 line-clamp-2 mb-3">
|
||||
{item.summary}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-[11px] pt-2 border-t border-[#1F1F1F] text-neutral-500">
|
||||
<span className="font-mono text-neutral-300 truncate max-w-[200px]">
|
||||
{item.route}
|
||||
</span>
|
||||
<span className="flex items-center space-x-1 text-emerald-400 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
<span>Valid</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: CONTENT INSPECTOR & DIRECTIVES (7 COLS) */}
|
||||
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-6">
|
||||
{selectedItem ? (
|
||||
<>
|
||||
{/* ITEM HEADER */}
|
||||
<div className="border-b border-[#1F1F1F] pb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<span className="px-2 py-0.5 text-[10px] font-semibold bg-[#1A1A1A] text-[#C5A059] border border-[#2A2A2A] rounded-xs font-mono">
|
||||
{selectedItem.sourceRepo}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500 font-mono">
|
||||
{selectedItem.path}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-white">
|
||||
{selectedItem.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setPreviewContent(selectedItem)}
|
||||
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-colors flex items-center space-x-1.5 self-start uppercase tracking-wider"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>View Rendered</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* EXPLAIN WHY / OMITTED STATUS */}
|
||||
{selectedItem.explainWhy && (
|
||||
<div className="p-4 bg-amber-950/20 border border-amber-900/40 rounded-sm text-xs">
|
||||
<h3 className="font-bold text-amber-400 mb-1">
|
||||
Artifact Omitted
|
||||
</h3>
|
||||
<p className="text-neutral-400 font-bold mb-1 mt-2">
|
||||
Reason:
|
||||
</p>
|
||||
<p className="text-neutral-300">
|
||||
{selectedItem.explainWhy}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* METADATA & FRONT MATTER INSPECTOR */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
||||
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
|
||||
Final Generated Route
|
||||
</span>
|
||||
<code className="font-mono text-[#C5A059] bg-[#141414] px-2.5 py-1 rounded-xs border border-[#222222] block text-xs truncate">
|
||||
{selectedItem.route}
|
||||
</code>
|
||||
<p className="text-neutral-500 text-[11px]">
|
||||
Resolved via{" "}
|
||||
<code className="text-neutral-300 font-mono">
|
||||
{selectedItem.styleInstanceId}
|
||||
</code>{" "}
|
||||
pattern.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
||||
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
|
||||
Repository Entry Defaults
|
||||
</span>
|
||||
<div className="font-mono text-neutral-300 space-y-0.5 text-[11px]">
|
||||
<div>
|
||||
Project:{" "}
|
||||
<strong className="text-white">
|
||||
{selectedItem.metadata?.projectName}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
Project ID:{" "}
|
||||
<strong className="text-[#C5A059]">
|
||||
{selectedItem.metadata?.projectId}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SPECIAL DIRECTIVES DETECTED */}
|
||||
<div className="space-y-3">
|
||||
<span className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest block">
|
||||
Parsed Markdown Extensions & Embeds
|
||||
</span>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
||||
{/* YouTube Directive */}
|
||||
<div className="p-3.5 bg-red-950/20 border border-red-900/40 rounded-sm space-y-1">
|
||||
<div className="flex items-center space-x-1.5 text-red-400 font-bold font-mono">
|
||||
<Youtube className="w-4 h-4" />
|
||||
<span>YouTube Directives (AC-06)</span>
|
||||
</div>
|
||||
{selectedItem.youtubeDirectives.length > 0 ? (
|
||||
selectedItem.youtubeDirectives.map((yt, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-[11px] text-red-200 font-mono"
|
||||
>
|
||||
Title: "{yt.title}" (ID: {yt.videoId})
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-neutral-500 text-[11px]">
|
||||
No YouTube directives in item.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wikipedia Links */}
|
||||
<div className="p-3.5 bg-blue-950/20 border border-blue-900/40 rounded-sm space-y-1">
|
||||
<div className="flex items-center space-x-1.5 text-blue-400 font-bold font-mono">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
<span>Wikipedia Links (AC-07)</span>
|
||||
</div>
|
||||
{selectedItem.wikipediaLinks.length > 0 ? (
|
||||
selectedItem.wikipediaLinks.map((wiki, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-[11px] text-blue-200 font-mono truncate"
|
||||
>
|
||||
{wiki.text} -> {wiki.url}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-neutral-500 text-[11px]">
|
||||
No Wikipedia links in item.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RAW HTML POLICY SAFETY STATUS */}
|
||||
<div className="p-4 bg-[#0A0A0A] text-neutral-300 rounded-sm text-xs space-y-2 border border-[#1F1F1F] font-mono">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-[#C5A059] uppercase tracking-widest text-[10px]">
|
||||
Raw HTML Security Policy (AC-08)
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-emerald-950/40 text-emerald-400 rounded-xs font-bold text-[10px] border border-emerald-500/20 uppercase">
|
||||
Policy: Disabled / Sanitized
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-neutral-400 text-[11px]">
|
||||
Raw HTML blocks and arbitrary script tags are
|
||||
stripped/escaped automatically during build time to prevent
|
||||
XSS vulnerabilities.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-neutral-500 text-xs">
|
||||
Select an artifact to inspect details.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* PUBLIC MEDIA ASSETS INVENTORY (AC-09) */
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<div>
|
||||
<h2 className="text-base font-serif italic text-white">
|
||||
Public Media Assets Namespace Mappings (/media)
|
||||
</h2>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">
|
||||
Maps source media directories into isolated public media
|
||||
namespaces to prevent path collisions.
|
||||
</p>
|
||||
</div>
|
||||
<span className="px-3 py-1 bg-amber-950/40 text-amber-400 rounded-xs text-xs font-semibold border border-amber-500/20 uppercase tracking-widest">
|
||||
AC-09 Verified
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
{mediaAssets.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="p-4 rounded-sm border border-[#1F1F1F] bg-[#0A0A0A] space-y-2 font-mono"
|
||||
>
|
||||
<div className="flex items-center justify-between text-[#C5A059] font-bold">
|
||||
<span>{asset.filename}</span>
|
||||
<span className="text-[10px] text-neutral-500">
|
||||
{(asset.sizeBytes / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-neutral-400 space-y-1">
|
||||
<div>
|
||||
Source Repo:{" "}
|
||||
<strong className="text-white">{asset.sourceRepo}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Original Path:{" "}
|
||||
<code className="text-neutral-300">
|
||||
{asset.originalPath}
|
||||
</code>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-[#1F1F1F]">
|
||||
<span className="text-neutral-500 block text-[10px] uppercase">
|
||||
Public Namespace Route:
|
||||
</span>
|
||||
<code className="text-[#C5A059] font-semibold text-[11px] block truncate">
|
||||
{asset.publicNamespacePath}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RENDERED CONTENT PREVIEW MODAL */}
|
||||
{previewContent && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-2xl w-full p-6 shadow-2xl space-y-4 max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<div>
|
||||
<span className="text-xs font-mono text-[#C5A059] font-bold">
|
||||
{previewContent.sourceRepo} • {previewContent.published}
|
||||
</span>
|
||||
<h3 className="text-lg font-bold text-white">
|
||||
{previewContent.title}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPreviewContent(null)}
|
||||
className="text-neutral-500 hover:text-white text-2xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert max-w-none text-xs text-neutral-200 leading-relaxed font-sans space-y-3">
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-[11px] text-neutral-400">
|
||||
Summary: {previewContent.summary}
|
||||
</div>
|
||||
<pre className="bg-[#0A0A0A] text-neutral-200 p-4 rounded-sm text-[11px] font-mono border border-[#1F1F1F] overflow-x-auto">
|
||||
<code>{previewContent.contentMarkdown}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end pt-3 border-t border-[#1F1F1F]">
|
||||
<button
|
||||
onClick={() => setPreviewContent(null)}
|
||||
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#262626] text-white rounded-sm text-xs font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Close Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import React from "react";
|
||||
import {
|
||||
GitBranch,
|
||||
Server,
|
||||
FileText,
|
||||
UploadCloud,
|
||||
PlayCircle,
|
||||
ArrowUpRight,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
BuildRecord,
|
||||
GitSource,
|
||||
NginxStatus,
|
||||
PushTarget,
|
||||
ContentItem,
|
||||
} from "../types";
|
||||
|
||||
interface DashboardTabProps {
|
||||
sources: GitSource[];
|
||||
contentItems: ContentItem[];
|
||||
builds: BuildRecord[];
|
||||
nginxStatus: NginxStatus | null;
|
||||
pushTargets: PushTarget[];
|
||||
onTriggerBuild: () => void;
|
||||
onNavigateTab: (tab: string) => void;
|
||||
onActivateLocal: (buildId: string) => void;
|
||||
}
|
||||
|
||||
export const DashboardTab: React.FC<DashboardTabProps> = ({
|
||||
sources,
|
||||
contentItems,
|
||||
builds,
|
||||
nginxStatus,
|
||||
pushTargets,
|
||||
onTriggerBuild,
|
||||
onNavigateTab,
|
||||
onActivateLocal,
|
||||
}) => {
|
||||
const activeBuild = builds.find((b) => b.isActiveLocal) || builds[0];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HERO SYSTEM HEADER */}
|
||||
<div className="bg-[#111111] text-white rounded-sm p-8 border border-[#1F1F1F] shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute right-0 top-0 bottom-0 w-1/3 opacity-15 pointer-events-none">
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none">
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="80"
|
||||
stroke="#C5A059"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="60"
|
||||
stroke="#C5A059"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
|
||||
<div>
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20 flex items-center space-x-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>Operational Control Plane</span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-4xl font-serif italic text-white tracking-tight">
|
||||
Welcome to Labyricorn.
|
||||
</h1>
|
||||
<p className="text-neutral-400 text-xs sm:text-sm mt-2 max-w-2xl leading-relaxed">
|
||||
The administrative control plane for your Git-driven publishing
|
||||
platform. Labyricorn orchestrates the generation of your static
|
||||
website directly from your Git repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={() => onNavigateTab("preview")}
|
||||
className="px-5 py-2.5 bg-[#181818] hover:bg-[#222222] text-neutral-200 border border-[#2A2A2A] rounded-sm text-xs font-semibold transition-all flex items-center space-x-2"
|
||||
>
|
||||
<ArrowUpRight className="w-4 h-4 text-[#C5A059]" />
|
||||
<span className="uppercase tracking-wider">
|
||||
Preview Live Site
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sources.length === 0 ? (
|
||||
<div className="bg-[#111111] border border-[#1F1F1F] rounded-sm p-12 flex flex-col items-center justify-center text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-[#181818] border border-[#262626] flex items-center justify-center mb-4">
|
||||
<GitBranch className="w-8 h-8 text-[#C5A059]" />
|
||||
</div>
|
||||
<h2 className="text-xl font-serif italic text-white mb-2">
|
||||
No repositories have been discovered.
|
||||
</h2>
|
||||
<p className="text-sm text-neutral-400 max-w-md mb-6 leading-relaxed">
|
||||
Connect git.labyricorn.com to begin discovering repositories and
|
||||
building your local site.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => onNavigateTab("repositories")}
|
||||
className="px-5 py-2.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all shadow-md flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span className="uppercase tracking-wider font-bold">
|
||||
Connect Repository
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : builds.length === 0 ? (
|
||||
<div className="bg-[#111111] border border-[#1F1F1F] rounded-sm p-12 flex flex-col items-center justify-center text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-[#181818] border border-[#262626] flex items-center justify-center mb-4">
|
||||
<PlayCircle className="w-8 h-8 text-[#C5A059]" />
|
||||
</div>
|
||||
<h2 className="text-xl font-serif italic text-white mb-2">
|
||||
No website projection has been built.
|
||||
</h2>
|
||||
<p className="text-sm text-neutral-400 max-w-md mb-6 leading-relaxed">
|
||||
Validate at least one repository, then run the first projection.
|
||||
</p>
|
||||
<button
|
||||
onClick={onTriggerBuild}
|
||||
className="px-5 py-2.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all shadow-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
<span className="uppercase tracking-wider font-bold">
|
||||
Publish First Build
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-[#111111] rounded-sm p-5 border border-[#1F1F1F] shadow-xs flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Active Local Release
|
||||
</span>
|
||||
<div className="w-8 h-8 rounded-sm bg-[#1A1A1A] text-[#C5A059] flex items-center justify-center border border-[#262626]">
|
||||
<Server className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-2xl font-bold font-mono text-white">
|
||||
{activeBuild ? activeBuild.id : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111111] rounded-sm p-5 border border-[#1F1F1F] shadow-xs flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Repositories
|
||||
</span>
|
||||
<div className="w-8 h-8 rounded-sm bg-[#1A1A1A] text-[#C5A059] flex items-center justify-center border border-[#262626]">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{sources.length} Repos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111111] rounded-sm p-5 border border-[#1F1F1F] shadow-xs flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Discovered Artifacts
|
||||
</span>
|
||||
<div className="w-8 h-8 rounded-sm bg-[#1A1A1A] text-[#C5A059] flex items-center justify-center border border-[#262626]">
|
||||
<FileText className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{contentItems.length} Artifacts
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111111] rounded-sm p-5 border border-[#1F1F1F] shadow-xs flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Publishing Targets
|
||||
</span>
|
||||
<div className="w-8 h-8 rounded-sm bg-[#1A1A1A] text-[#C5A059] flex items-center justify-center border border-[#262626]">
|
||||
<UploadCloud className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl font-bold font-mono text-white">
|
||||
{pushTargets.length > 0
|
||||
? pushTargets[0].lastDeployedBuildId || "Pending"
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PLATFORM PHILOSOPHY */}
|
||||
<div className="bg-[#111111] rounded-sm p-6 border border-[#1F1F1F] shadow-xs space-y-4">
|
||||
<h2 className="text-lg font-serif italic text-white">
|
||||
Platform Architecture
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 text-xs text-neutral-400">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-white uppercase tracking-widest text-[10px]">
|
||||
Gitea
|
||||
</h3>
|
||||
<p>Gitea stores the authoritative repositories.</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-white uppercase tracking-widest text-[10px]">
|
||||
Artifacts
|
||||
</h3>
|
||||
<p>Labyricorn discovers and validates repository artifacts.</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-white uppercase tracking-widest text-[10px]">
|
||||
Projections
|
||||
</h3>
|
||||
<p>
|
||||
Projections transform those artifacts into public experiences.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-white uppercase tracking-widest text-[10px]">
|
||||
Publishing
|
||||
</h3>
|
||||
<p>
|
||||
Publishing places a completed projection onto the local canonical
|
||||
site and optional remote targets.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-white uppercase tracking-widest text-[10px]">
|
||||
Control Plane
|
||||
</h3>
|
||||
<p>
|
||||
This interface orchestrates the pipeline without replacing Git as
|
||||
the source of truth.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,708 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
GitBranch,
|
||||
Folder,
|
||||
FileText,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Key,
|
||||
HardDrive,
|
||||
ShieldCheck,
|
||||
Search,
|
||||
ExternalLink,
|
||||
ChevronRight,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { GitSource, CredentialRef } from "../types";
|
||||
|
||||
interface GitSourcesTabProps {
|
||||
sources: GitSource[];
|
||||
credentials: CredentialRef[];
|
||||
onAddSource: (src: Partial<GitSource>) => void;
|
||||
onTestSource: (id: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
|
||||
sources,
|
||||
credentials,
|
||||
onAddSource,
|
||||
onTestSource,
|
||||
}) => {
|
||||
const [selectedSourceId, setSelectedSourceId] = useState<string>(
|
||||
sources[0]?.id || "titleflow",
|
||||
);
|
||||
const [treeFiles, setTreeFiles] = useState<
|
||||
Array<{ path: string; type: string; size: string }>
|
||||
>([
|
||||
{ path: ".devlogs/pack-validation.md", type: "file", size: "2.8 KB" },
|
||||
{ path: ".devlogs/schema-refactor.md", type: "file", size: "1.9 KB" },
|
||||
{ path: "media/screenshots/header.webp", type: "file", size: "142 KB" },
|
||||
{ path: "src/engine.ts", type: "file", size: "14.2 KB" },
|
||||
{ path: "package.json", type: "file", size: "1.1 KB" },
|
||||
]);
|
||||
const [isTesting, setIsTesting] = useState<string | null>(null);
|
||||
const [testResult, setTestResult] = useState<{
|
||||
id: string;
|
||||
message: string;
|
||||
success: boolean;
|
||||
} | null>(null);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [activeInspectorTab, setActiveInspectorTab] = useState<
|
||||
"overview" | "health" | "artifacts" | "publishing"
|
||||
>("overview");
|
||||
|
||||
// New Source Form
|
||||
const [newRepoId, setNewRepoId] = useState("");
|
||||
const [newRepoUrl, setNewRepoUrl] = useState("");
|
||||
const [newRef, setNewRef] = useState("main");
|
||||
const [newType, setNewType] = useState("hybrid");
|
||||
const [newCredential, setNewCredential] = useState("secret:gitea-deploy-key");
|
||||
const [newRequired, setNewRequired] = useState(true);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
|
||||
const handleSourceSelect = async (id: string) => {
|
||||
setSelectedSourceId(id);
|
||||
try {
|
||||
const res = await fetch(`/api/git-sources/${id}/tree`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTreeFiles(data.files || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscover = async () => {
|
||||
setIsDiscovering(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const res = await fetch("/api/gitea/discover", { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Failed to discover");
|
||||
setTestResult({ id: "Discovery", message: data.message, success: true });
|
||||
} catch (err: any) {
|
||||
setTestResult({ id: "Discovery", message: err.message, success: false });
|
||||
} finally {
|
||||
setIsDiscovering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = async (id: string) => {
|
||||
setIsTesting(id);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const data = await onTestSource(id);
|
||||
setTestResult({ id, message: data.message, success: data.success });
|
||||
} catch (err: any) {
|
||||
setTestResult({
|
||||
id,
|
||||
message: err.message || "Connection test failed",
|
||||
success: false,
|
||||
});
|
||||
} finally {
|
||||
setIsTesting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSource = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRepoId || !newRepoUrl) return;
|
||||
|
||||
onAddSource({
|
||||
id: newRepoId,
|
||||
type: newType as any,
|
||||
repository: newRepoUrl,
|
||||
ref: newRef,
|
||||
required: newRequired,
|
||||
credential: newCredential,
|
||||
branch: newRef,
|
||||
});
|
||||
|
||||
setShowAddModal(false);
|
||||
setNewRepoId("");
|
||||
setNewRepoUrl("");
|
||||
setNewType("hybrid");
|
||||
};
|
||||
|
||||
const selectedSource = sources.find((s) => s.id === selectedSourceId);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER SECTION */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Repositories
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-[#C5A059]/10 text-[#C5A059] border border-[#C5A059]/30">
|
||||
Source Inventory
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Connect standard SSH, HTTPS, or local Git repositories. Repositories
|
||||
without .labyricorn/site.yml are ignored during build.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 self-start md:self-auto">
|
||||
<button
|
||||
onClick={handleDiscover}
|
||||
disabled={isDiscovering}
|
||||
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#2A2A2A] text-white font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider border border-[#2A2A2A]"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isDiscovering ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>{isDiscovering ? "Discovering..." : "Discover Gitea"}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add Repository</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TEST RESULT NOTIFICATION */}
|
||||
{testResult && (
|
||||
<div
|
||||
className={`p-4 rounded-sm border text-xs flex items-start justify-between ${
|
||||
testResult.success
|
||||
? "bg-emerald-950/40 border-emerald-500/30 text-emerald-300"
|
||||
: "bg-amber-950/40 border-amber-500/30 text-amber-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start space-x-2">
|
||||
{testResult.success ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<span className="font-bold block">
|
||||
Connectivity Test Result ({testResult.id})
|
||||
</span>
|
||||
<span>{testResult.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setTestResult(null)}
|
||||
className="text-neutral-400 hover:text-white text-base leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MAIN TWO-COLUMN LAYOUT: SOURCES LIST & TREE BROWSER */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT COLUMN: SOURCES LIST (5 COLS) */}
|
||||
<div className="lg:col-span-5 space-y-3">
|
||||
<div className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest px-1">
|
||||
Configured Repositories ({sources.length})
|
||||
</div>
|
||||
|
||||
{sources.map((source) => {
|
||||
const isSelected = source.id === selectedSourceId;
|
||||
const health = source.health || "Healthy";
|
||||
const healthColor =
|
||||
health === "Healthy"
|
||||
? "text-emerald-400"
|
||||
: health === "Warning"
|
||||
? "text-amber-400"
|
||||
: health === "Invalid"
|
||||
? "text-rose-400"
|
||||
: "text-neutral-400";
|
||||
const healthBg =
|
||||
health === "Healthy"
|
||||
? "bg-emerald-400"
|
||||
: health === "Warning"
|
||||
? "bg-amber-400"
|
||||
: health === "Invalid"
|
||||
? "bg-rose-400"
|
||||
: "bg-neutral-400";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={source.id}
|
||||
onClick={() => handleSourceSelect(source.id)}
|
||||
className={`p-4 rounded-sm border transition-all cursor-pointer ${
|
||||
isSelected
|
||||
? "bg-[#181818] border-[#C5A059] shadow-xs"
|
||||
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-bold text-white text-sm font-mono">
|
||||
{source.id}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider bg-neutral-800 text-neutral-400 rounded-xs capitalize">
|
||||
{source.type || "Hybrid"}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`flex items-center space-x-1 text-xs font-medium ${healthColor}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${healthBg}`}
|
||||
></span>
|
||||
<span>{health}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs font-mono text-neutral-400">
|
||||
<p className="truncate text-neutral-200 bg-[#0A0A0A] px-2 py-1 rounded-xs text-[11px] border border-[#1F1F1F]">
|
||||
{source.repository}
|
||||
</p>
|
||||
<div className="flex items-center justify-between pt-1 text-[11px] text-neutral-500">
|
||||
<span>
|
||||
Ref:{" "}
|
||||
<strong className="text-neutral-300">{source.ref}</strong>
|
||||
</span>
|
||||
<span>
|
||||
SHA:{" "}
|
||||
<strong className="text-[#C5A059]">
|
||||
{source.lastResolvedCommit?.substring(0, 7) || "HEAD"}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-[#1F1F1F] flex items-center justify-between text-xs">
|
||||
{source.credential ? (
|
||||
<span className="text-neutral-500 flex items-center space-x-1 text-[11px]">
|
||||
<Key className="w-3 h-3 text-[#C5A059]" />
|
||||
<span className="font-mono text-neutral-400">
|
||||
{source.credential}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-neutral-500 text-[11px]">
|
||||
{source.visibility || "Public in Gitea"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTest(source.id);
|
||||
}}
|
||||
disabled={isTesting === source.id}
|
||||
className="px-2.5 py-1 bg-[#1A1A1A] hover:bg-[#262626] border border-[#2A2A2A] rounded-xs text-neutral-300 text-[11px] font-medium transition-colors flex items-center space-x-1"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-3 h-3 ${isTesting === source.id ? "animate-spin text-[#C5A059]" : ""}`}
|
||||
/>
|
||||
<span>Test</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: REPOSITORY INSPECTOR (7 COLS) */}
|
||||
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col h-[700px]">
|
||||
{selectedSource ? (
|
||||
<>
|
||||
{/* INSPECTOR HEADER */}
|
||||
<div className="border-b border-[#1F1F1F] pb-4 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Folder className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-base font-bold text-white font-mono">
|
||||
{selectedSource.id}
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-xs bg-[#1A1A1A] px-2.5 py-1 rounded-xs text-neutral-300 font-mono border border-[#262626]">
|
||||
Ref: {selectedSource.ref}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-1 font-mono">
|
||||
{selectedSource.repository}
|
||||
</p>
|
||||
|
||||
{/* STATUS SUMMARY BAR */}
|
||||
<div className="flex flex-wrap items-center gap-3 mt-4 text-[11px]">
|
||||
<span className="text-neutral-400">
|
||||
Reachability:{" "}
|
||||
<strong className="text-neutral-200">
|
||||
{selectedSource.reachability || "Healthy"}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="text-neutral-600">|</span>
|
||||
<span className="text-neutral-400">
|
||||
Config:{" "}
|
||||
<strong className="text-neutral-200">
|
||||
{selectedSource.configurationState || "Valid"}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="text-neutral-600">|</span>
|
||||
<span className="text-neutral-400">
|
||||
Projection:{" "}
|
||||
<strong className="text-neutral-200">
|
||||
{selectedSource.projectionEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* INSPECTOR TABS */}
|
||||
<div className="flex items-center space-x-1 border-b border-[#1F1F1F] mb-4">
|
||||
{[
|
||||
{ id: "overview", label: "Overview" },
|
||||
{ id: "health", label: "Health & Validation" },
|
||||
{ id: "artifacts", label: "Artifacts" },
|
||||
{ id: "publishing", label: "Publishing" },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveInspectorTab(tab.id as any)}
|
||||
className={`px-4 py-2 text-xs font-semibold uppercase tracking-wider border-b-2 transition-colors ${
|
||||
activeInspectorTab === tab.id
|
||||
? "border-[#C5A059] text-white bg-[#1A1A1A]"
|
||||
: "border-transparent text-neutral-500 hover:text-neutral-300 hover:bg-[#161616]"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* INSPECTOR CONTENT */}
|
||||
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar space-y-4">
|
||||
{activeInspectorTab === "overview" && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest mb-2">
|
||||
Repository Classification
|
||||
</h3>
|
||||
<div className="bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm p-4 text-xs text-neutral-300">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="block text-neutral-500 mb-1">
|
||||
Type
|
||||
</span>
|
||||
<span className="capitalize">
|
||||
{selectedSource.type || "Hybrid project"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-500 mb-1">
|
||||
Gitea Visibility
|
||||
</span>
|
||||
<span>
|
||||
{selectedSource.visibility || "Public in Gitea"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-500 mb-1">
|
||||
Last Sync
|
||||
</span>
|
||||
<span>
|
||||
{selectedSource.lastSyncedAt
|
||||
? new Date(
|
||||
selectedSource.lastSyncedAt,
|
||||
).toLocaleString()
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-500 mb-1">
|
||||
Current Revision
|
||||
</span>
|
||||
<span className="font-mono text-[#C5A059]">
|
||||
{selectedSource.lastResolvedCommit?.substring(
|
||||
0,
|
||||
7,
|
||||
) || "HEAD"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest mb-2">
|
||||
Produces
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(selectedSource.produces &&
|
||||
selectedSource.produces.length > 0
|
||||
? selectedSource.produces
|
||||
: ["Discovered Artifacts"]
|
||||
).map((prod) => (
|
||||
<span
|
||||
key={prod}
|
||||
className="px-2.5 py-1 rounded-xs bg-[#1A1A1A] border border-[#262626] text-xs text-neutral-300"
|
||||
>
|
||||
{prod}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeInspectorTab === "health" && (
|
||||
<div className="space-y-4">
|
||||
{selectedSource.health !== "Healthy" &&
|
||||
selectedSource.explainWhy && (
|
||||
<div
|
||||
className={`p-4 rounded-sm border text-xs bg-amber-950/20 border-amber-500/20 text-neutral-300`}
|
||||
>
|
||||
<h3 className="text-amber-400 font-bold mb-1">
|
||||
Not projected
|
||||
</h3>
|
||||
<p className="text-neutral-400 font-bold mb-1 mt-2">
|
||||
Reason:
|
||||
</p>
|
||||
<p>{selectedSource.explainWhy}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: "Repository reachable", pass: true },
|
||||
{ label: "Default branch exists", pass: true },
|
||||
{
|
||||
label: ".labyricorn/site.yml present",
|
||||
pass:
|
||||
selectedSource.configurationState !==
|
||||
"Not Configured",
|
||||
},
|
||||
{
|
||||
label: "Metadata syntax valid",
|
||||
pass: selectedSource.configurationState === "Valid",
|
||||
},
|
||||
{
|
||||
label: "Required content paths exist",
|
||||
pass: selectedSource.health !== "Invalid",
|
||||
},
|
||||
].map((check, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-3 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-xs"
|
||||
>
|
||||
<span className="text-neutral-300">
|
||||
{check.label}
|
||||
</span>
|
||||
{check.pass ? (
|
||||
<span className="text-emerald-400 flex items-center space-x-1">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<span>Pass</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-rose-400 flex items-center space-x-1">
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
<span>Fail</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeInspectorTab === "artifacts" && (
|
||||
<div className="space-y-4">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest block mb-2">
|
||||
Discovered Repository Files & Media
|
||||
</span>
|
||||
<div className="bg-[#0A0A0A] rounded-sm p-4 text-neutral-300 font-mono text-xs space-y-2 border border-[#1F1F1F]">
|
||||
{treeFiles.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between py-1.5 border-b border-[#1A1A1A] last:border-0 hover:bg-[#141414] px-2 rounded-xs"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{f.path.includes(".md") ? (
|
||||
<FileText className="w-3.5 h-3.5 text-[#C5A059]" />
|
||||
) : f.path.includes(".webp") ||
|
||||
f.path.includes(".png") ||
|
||||
f.path.includes(".svg") ? (
|
||||
<Folder className="w-3.5 h-3.5 text-amber-500" />
|
||||
) : (
|
||||
<Code className="w-3.5 h-3.5 text-neutral-500" />
|
||||
)}
|
||||
<span className="text-neutral-200">{f.path}</span>
|
||||
</div>
|
||||
<span className="text-neutral-500 text-[11px]">
|
||||
{f.size}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeInspectorTab === "publishing" && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm space-y-3 text-xs">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-neutral-400">
|
||||
Local Publication
|
||||
</span>
|
||||
<span className="text-emerald-400">Current</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-neutral-400">GitHub Mirror</span>
|
||||
<span className="text-emerald-400">Current</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-neutral-400">
|
||||
Cloudflare Pages
|
||||
</span>
|
||||
<span className="text-emerald-400">Current</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-neutral-500 text-xs">
|
||||
<Folder className="w-12 h-12 mb-4 text-[#1F1F1F]" />
|
||||
<p>Select a repository to open the Inspector.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ADD SOURCE MODAL */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-lg w-full p-6 shadow-2xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<h3 className="text-base font-serif italic text-white">
|
||||
Add New Repository
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="text-neutral-500 hover:text-white text-xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateSource} className="space-y-4 text-xs">
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Repository ID (Identifier)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. titleflow-core"
|
||||
value={newRepoId}
|
||||
onChange={(e) => setNewRepoId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#1A1A1A] border border-[#2A2A2A] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Git Repository URL / Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. [email protected]:labyricorn/repo.git or https://..."
|
||||
value={newRepoUrl}
|
||||
onChange={(e) => setNewRepoUrl(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#1A1A1A] border border-[#2A2A2A] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Target Ref / Branch
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRef}
|
||||
onChange={(e) => setNewRef(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#1A1A1A] border border-[#2A2A2A] rounded-sm text-white font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Repository Type
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. documentation, internal"
|
||||
value={newType}
|
||||
onChange={(e) => setNewType(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#1A1A1A] border border-[#2A2A2A] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block font-semibold text-neutral-300 mb-1">
|
||||
Secret Credential Reference
|
||||
</label>
|
||||
<select
|
||||
value={newCredential}
|
||||
onChange={(e) => setNewCredential(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#1A1A1A] border border-[#2A2A2A] rounded-sm text-white font-mono"
|
||||
>
|
||||
<option value="">None (Public)</option>
|
||||
{credentials.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name} ({c.id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="requiredCheck"
|
||||
checked={newRequired}
|
||||
onChange={(e) => setNewRequired(e.target.checked)}
|
||||
className="rounded border-[#2A2A2A] bg-[#1A1A1A] text-[#C5A059]"
|
||||
/>
|
||||
<label
|
||||
htmlFor="requiredCheck"
|
||||
className="text-neutral-300 font-medium"
|
||||
>
|
||||
Required Source (Production builds fail if unreachable)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-[#1F1F1F]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="px-4 py-2 border border-[#2A2A2A] text-neutral-300 rounded-sm hover:bg-[#1A1A1A] font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Save Repository
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Eye,
|
||||
Monitor,
|
||||
Tablet,
|
||||
Smartphone,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
|
||||
export const LivePreviewTab: React.FC = () => {
|
||||
const [route, setRoute] = useState("/");
|
||||
const [deviceMode, setDeviceMode] = useState<"desktop" | "tablet" | "mobile">(
|
||||
"desktop",
|
||||
);
|
||||
const [iframeKey, setIframeKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (e.data && e.data.type === "NAVIGATE_LIVE_SITE") {
|
||||
setRoute(e.data.route);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, []);
|
||||
|
||||
const getContainerWidth = () => {
|
||||
switch (deviceMode) {
|
||||
case "mobile":
|
||||
return "max-w-[375px]";
|
||||
case "tablet":
|
||||
return "max-w-[768px]";
|
||||
default:
|
||||
return "w-full";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* HEADER BAR */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-4 shadow-xs flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Eye className="w-5 h-5 text-[#C5A059]" />
|
||||
<div>
|
||||
<h1 className="text-base font-serif italic text-white">
|
||||
Live Static Site Preview
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Rendered statically via Nginx from active local release
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DEVICE MODE TOGGLE */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex bg-[#0A0A0A] p-1 rounded-sm border border-[#1F1F1F] text-xs font-medium text-neutral-400">
|
||||
<button
|
||||
onClick={() => setDeviceMode("desktop")}
|
||||
className={`p-1.5 rounded-xs transition-colors flex items-center space-x-1 ${deviceMode === "desktop" ? "bg-[#1E1E1E] text-[#C5A059] font-semibold" : ""}`}
|
||||
>
|
||||
<Monitor className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Desktop</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeviceMode("tablet")}
|
||||
className={`p-1.5 rounded-xs transition-colors flex items-center space-x-1 ${deviceMode === "tablet" ? "bg-[#1E1E1E] text-[#C5A059] font-semibold" : ""}`}
|
||||
>
|
||||
<Tablet className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Tablet</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeviceMode("mobile")}
|
||||
className={`p-1.5 rounded-xs transition-colors flex items-center space-x-1 ${deviceMode === "mobile" ? "bg-[#1E1E1E] text-[#C5A059] font-semibold" : ""}`}
|
||||
>
|
||||
<Smartphone className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Mobile</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIframeKey((k) => k + 1)}
|
||||
className="p-2 bg-[#141414] hover:bg-[#1F1F1F] text-neutral-300 border border-[#222222] rounded-sm transition-colors"
|
||||
title="Reload Preview"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ADDRESS BAR */}
|
||||
<div className="bg-[#0A0A0A] rounded-sm p-2.5 border border-[#1F1F1F] flex items-center space-x-3 text-xs text-neutral-300 font-mono">
|
||||
<div className="flex items-center space-x-1 text-neutral-500">
|
||||
<button
|
||||
onClick={() => setRoute("/")}
|
||||
className="hover:text-white px-1"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIframeKey((k) => k + 1)}
|
||||
className="hover:text-white px-1"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow bg-[#141414] border border-[#222222] rounded-sm px-3 py-1.5 flex items-center space-x-2 text-white">
|
||||
<Globe className="w-3.5 h-3.5 text-[#C5A059] shrink-0" />
|
||||
<span className="text-neutral-500 text-[11px]">
|
||||
http://labyricorn.local
|
||||
</span>
|
||||
<span className="font-semibold text-[#C5A059] truncate">{route}</span>
|
||||
</div>
|
||||
|
||||
<span className="text-[10px] text-emerald-400 bg-emerald-950/40 px-2.5 py-1 rounded-xs border border-emerald-500/20 hidden md:inline-block uppercase tracking-wider font-bold">
|
||||
Nginx Port 80
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* IFRAME CONTAINER */}
|
||||
<div className="flex justify-center bg-[#0A0A0A] p-4 rounded-sm border border-[#1F1F1F] min-h-[600px]">
|
||||
<div
|
||||
className={`w-full ${getContainerWidth()} bg-white rounded-sm shadow-2xl overflow-hidden transition-all border border-[#1F1F1F] flex flex-col`}
|
||||
>
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
src={`/api/live-site/html?route=${encodeURIComponent(route)}`}
|
||||
className="w-full h-[650px] border-0"
|
||||
title="Labyricorn Live Site"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Terminal,
|
||||
GitBranch,
|
||||
Sliders,
|
||||
FileText,
|
||||
Palette,
|
||||
PlayCircle,
|
||||
Server,
|
||||
UploadCloud,
|
||||
Eye,
|
||||
ShieldCheck,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { NginxStatus, BuildRecord, PushTarget } from "../types";
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
nginxStatus: NginxStatus | null;
|
||||
activeBuild: BuildRecord | null;
|
||||
primaryPushTarget: PushTarget | null;
|
||||
onQuickBuild: () => void;
|
||||
onQuickNginxTest: () => void;
|
||||
isBuilding: boolean;
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
nginxStatus,
|
||||
activeBuild,
|
||||
primaryPushTarget,
|
||||
onQuickBuild,
|
||||
onQuickNginxTest,
|
||||
isBuilding,
|
||||
}) => {
|
||||
const tabs = [
|
||||
{ id: "dashboard", label: "Dashboard", icon: Activity },
|
||||
{ id: "repositories", label: "Repositories", icon: GitBranch },
|
||||
{ id: "explorer", label: "Site Explorer", icon: Eye },
|
||||
{ id: "artifacts", label: "Artifacts", icon: FileText },
|
||||
{ id: "projections", label: "Projections", icon: Palette },
|
||||
{ id: "publishing", label: "Publishing", icon: PlayCircle },
|
||||
{ id: "targets", label: "Publishing Targets", icon: UploadCloud },
|
||||
{ id: "local-site", label: "Local Site", icon: Server },
|
||||
{ id: "system", label: "System", icon: ShieldCheck },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-[#0F0F0F] text-white border-b border-[#1F1F1F] sticky top-0 z-50">
|
||||
{/* TOP BRAND BAR */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-9 h-9 rounded-sm bg-gradient-to-tr from-[#8A6D3B] to-[#C5A059] flex items-center justify-center font-serif italic text-black font-bold text-lg shadow-md shadow-[#C5A059]/10">
|
||||
L
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-serif italic text-lg tracking-tight text-white">
|
||||
Labyricorn
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-[10px] font-mono font-semibold bg-[#C5A059]/10 text-[#C5A059] border border-[#C5A059]/30 rounded-xs uppercase tracking-widest">
|
||||
v1.0.0-MVP
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-neutral-400 hidden sm:block">
|
||||
Git-backed Static Site Control Plane & Nginx Host
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SYSTEM STATUS BADGES & QUICK ACTIONS */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Active Release Badge */}
|
||||
<div className="hidden lg:flex items-center space-x-2 px-3 py-1 bg-[#141414] border border-[#222222] rounded-sm text-xs font-mono text-neutral-300">
|
||||
<span className="text-neutral-500">current -></span>
|
||||
<span className="text-[#C5A059] font-semibold">
|
||||
{activeBuild ? activeBuild.id : "none"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Nginx Status Badge */}
|
||||
<div className="hidden md:flex items-center space-x-1.5 px-3 py-1 bg-[#141414] border border-[#222222] rounded-sm text-xs text-neutral-300">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${nginxStatus?.configValid ? "bg-emerald-500" : "bg-amber-500"}`}
|
||||
></span>
|
||||
<span className="font-medium text-neutral-300">
|
||||
Nginx: {nginxStatus?.configValid ? "Active (Port 80)" : "Error"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Push Target Status */}
|
||||
{primaryPushTarget && (
|
||||
<div className="hidden xl:flex items-center space-x-1.5 px-3 py-1 bg-[#141414] border border-[#222222] rounded-sm text-xs text-neutral-300">
|
||||
<span className="text-[#C5A059] font-mono">VPS:</span>
|
||||
<span className="text-neutral-200 font-medium">
|
||||
{primaryPushTarget.host}
|
||||
</span>
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* QUICK BUILD BUTTON */}
|
||||
<button
|
||||
onClick={onQuickBuild}
|
||||
disabled={isBuilding}
|
||||
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-colors shadow-xs flex items-center space-x-1.5 disabled:opacity-50"
|
||||
>
|
||||
<PlayCircle
|
||||
className={`w-3.5 h-3.5 ${isBuilding ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span className="uppercase tracking-wider text-[11px]">
|
||||
{isBuilding ? "Building..." : "Trigger Build"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NAVIGATION TAB STRIP */}
|
||||
<div className="border-t border-[#1F1F1F] bg-[#0A0A0A]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 overflow-x-auto no-scrollbar">
|
||||
<nav className="flex space-x-1 py-1 text-xs font-medium min-w-max">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center space-x-2 px-3.5 py-2 rounded-sm transition-all ${
|
||||
isActive
|
||||
? "bg-[#181818] text-[#C5A059] border-b-2 border-[#C5A059] font-semibold"
|
||||
: "text-neutral-400 hover:text-white hover:bg-[#141414]"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
className={`w-3.5 h-3.5 ${isActive ? "text-[#C5A059]" : "text-neutral-500"}`}
|
||||
/>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Server,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Code,
|
||||
Layers,
|
||||
ShieldCheck,
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
} from "lucide-react";
|
||||
import { NginxStatus, BuildRecord } from "../types";
|
||||
|
||||
interface NginxReleasesTabProps {
|
||||
status: NginxStatus;
|
||||
builds: BuildRecord[];
|
||||
onTestConfig: () => Promise<void>;
|
||||
onActivateRelease: (buildId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const NginxReleasesTab: React.FC<NginxReleasesTabProps> = ({
|
||||
status,
|
||||
builds,
|
||||
onTestConfig,
|
||||
onActivateRelease,
|
||||
}) => {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testOutput, setTestOutput] = useState<string | null>(null);
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
|
||||
const activeBuild =
|
||||
builds.find((b) => b.id === status.activeReleaseId) || builds[0];
|
||||
const previousBuild = builds.find(
|
||||
(b) =>
|
||||
b.id !== status.activeReleaseId && !b.isStaged && b.status !== "failed",
|
||||
);
|
||||
|
||||
const handleTest = async () => {
|
||||
setIsTesting(true);
|
||||
setTestOutput(null);
|
||||
try {
|
||||
await onTestConfig();
|
||||
setTestOutput(
|
||||
"nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful",
|
||||
);
|
||||
} catch (err: any) {
|
||||
setTestOutput(`nginx -t failed: ${err.message}`);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = async (buildId: string) => {
|
||||
setIsActivating(true);
|
||||
await onActivateRelease(buildId);
|
||||
setIsActivating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Local Canonical Site
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
|
||||
PRD AC-13 & AC-14 Verified
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Hosts the active release locally as the canonical runtime. Performs
|
||||
zero-downtime atomic symlink changes (
|
||||
<code className="text-[#C5A059] font-mono">
|
||||
current -> releases/build-XXXXXX
|
||||
</code>
|
||||
) after configuration validation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isTesting}
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-3.5 h-3.5 ${isTesting ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>
|
||||
{isTesting ? "Validating..." : "Validate Server Configuration"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testOutput && (
|
||||
<div className="p-4 bg-[#0A0A0A] text-neutral-200 rounded-sm font-mono text-xs border border-[#1F1F1F] space-y-1">
|
||||
<div className="text-emerald-400 font-bold flex items-center space-x-1.5">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>Web Server Syntax Test Result</span>
|
||||
</div>
|
||||
<pre className="text-neutral-300 text-[11px] whitespace-pre-wrap">
|
||||
{testOutput}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SYMLINK RELEASES GRID */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* CURRENT SYMLINK */}
|
||||
<div className="bg-[#111111] rounded-sm border-2 border-[#C5A059] p-6 shadow-xs space-y-4 relative">
|
||||
<span className="absolute -top-3 right-4 bg-[#C5A059] text-black px-3 py-0.5 rounded-xs text-[10px] font-bold uppercase tracking-widest">
|
||||
Active Production
|
||||
</span>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Server className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-base font-bold text-white">current Symlink</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-xs space-y-1">
|
||||
<div className="text-neutral-500 text-[10px] uppercase font-bold tracking-widest">
|
||||
Target Release Directory
|
||||
</div>
|
||||
<div className="text-[#C5A059] font-bold text-sm">
|
||||
/var/lib/labyricorn/site/current
|
||||
</div>
|
||||
<div className="text-neutral-400 text-[11px] pt-1 border-t border-[#1F1F1F]">
|
||||
->{" "}
|
||||
<strong className="text-white">{status.activeReleaseId}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-neutral-400 space-y-1">
|
||||
<div>
|
||||
Hostname:{" "}
|
||||
<strong className="font-mono text-white">
|
||||
{status.serverName}
|
||||
</strong>{" "}
|
||||
(Port {status.listeningPort})
|
||||
</div>
|
||||
<div>
|
||||
Last Reloaded:{" "}
|
||||
<span className="font-mono text-neutral-500">
|
||||
{new Date(status.lastReloadTime).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STAGING SYMLINK */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Layers className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-base font-bold text-white">
|
||||
staging Symlink
|
||||
</h2>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 bg-[#1A1A1A] text-[#C5A059] text-[10px] font-bold rounded-xs border border-[#2A2A2A]">
|
||||
Port 8080
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-xs space-y-1">
|
||||
<div className="text-neutral-500 text-[10px] uppercase font-bold tracking-widest">
|
||||
Target Staging Release
|
||||
</div>
|
||||
<div className="text-[#C5A059] font-bold text-sm">
|
||||
/var/lib/labyricorn/site/staging
|
||||
</div>
|
||||
<div className="text-neutral-400 text-[11px] pt-1 border-t border-[#1F1F1F]">
|
||||
->{" "}
|
||||
<strong className="text-white">
|
||||
{status.stagingReleaseId || "None"}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status.stagingReleaseId && (
|
||||
<button
|
||||
onClick={() => handleActivate(status.stagingReleaseId!)}
|
||||
className="w-full py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm text-xs font-semibold shadow-sm transition-colors uppercase tracking-wider"
|
||||
>
|
||||
Promote Staging to Production
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PREVIOUS / ROLLBACK SYMLINK */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RotateCcw className="w-5 h-5 text-amber-500" />
|
||||
<h2 className="text-base font-bold text-white">
|
||||
1-Click Rollback Target
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-xs space-y-1">
|
||||
<div className="text-neutral-500 text-[10px] uppercase font-bold tracking-widest">
|
||||
Retained Previous Release
|
||||
</div>
|
||||
<div className="text-amber-400 font-bold text-sm">
|
||||
/var/lib/labyricorn/site/previous
|
||||
</div>
|
||||
<div className="text-neutral-400 text-[11px] pt-1 border-t border-[#1F1F1F]">
|
||||
->{" "}
|
||||
<strong className="text-white">
|
||||
{previousBuild ? previousBuild.id : "build-000002"}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{previousBuild && (
|
||||
<button
|
||||
onClick={() => handleActivate(previousBuild.id)}
|
||||
disabled={isActivating}
|
||||
className="w-full py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-sm text-xs font-semibold shadow-sm transition-colors flex items-center justify-center space-x-1 uppercase tracking-wider"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
<span>Rollback to {previousBuild.id}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERATED NGINX SERVER BLOCK CONFIGURATION */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<h2 className="text-base font-bold text-white">
|
||||
Generated Web Server Configuration (Nginx)
|
||||
</h2>
|
||||
<div className="bg-[#0A0A0A] text-[#C5A059] font-mono text-xs p-4 rounded-sm border border-[#1F1F1F] overflow-x-auto">
|
||||
<pre>
|
||||
<code>{status.serverBlockConfig}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,262 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
UploadCloud,
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Server,
|
||||
Key,
|
||||
RotateCcw,
|
||||
Activity,
|
||||
} from "lucide-react";
|
||||
import { PushTarget, BuildRecord } from "../types";
|
||||
|
||||
interface PushIntegrationsTabProps {
|
||||
targets: PushTarget[];
|
||||
builds: BuildRecord[];
|
||||
onPushToTarget: (
|
||||
targetId: string,
|
||||
buildId: string,
|
||||
) => Promise<{ success: boolean; logs: string[] }>;
|
||||
onTestTarget: (targetId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const PushIntegrationsTab: React.FC<PushIntegrationsTabProps> = ({
|
||||
targets,
|
||||
builds,
|
||||
onPushToTarget,
|
||||
onTestTarget,
|
||||
}) => {
|
||||
const [selectedTargetId, setSelectedTargetId] = useState<string>(
|
||||
targets[0]?.id || "production-vps",
|
||||
);
|
||||
const [selectedBuildId, setSelectedBuildId] = useState<string>(
|
||||
builds[0]?.id || "build-000003",
|
||||
);
|
||||
const [isPushing, setIsPushing] = useState(false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [pushLogs, setPushLogs] = useState<string[]>(
|
||||
targets[0]?.lastLogs || [],
|
||||
);
|
||||
const [testMsg, setTestMsg] = useState<string | null>(null);
|
||||
|
||||
const target = targets.find((t) => t.id === selectedTargetId) || targets[0];
|
||||
|
||||
const handlePush = async () => {
|
||||
if (!target) return;
|
||||
setIsPushing(true);
|
||||
setTestMsg(null);
|
||||
try {
|
||||
const res = await onPushToTarget(target.id, selectedBuildId);
|
||||
setPushLogs(res.logs || []);
|
||||
} catch (err: any) {
|
||||
setPushLogs([`[ERROR] Push failed: ${err.message}`]);
|
||||
} finally {
|
||||
setIsPushing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!target) return;
|
||||
setIsTesting(true);
|
||||
setTestMsg(null);
|
||||
try {
|
||||
const res = await onTestTarget(target.id);
|
||||
setTestMsg(res.message);
|
||||
} catch (err: any) {
|
||||
setTestMsg(`SSH Test failed: ${err.message}`);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Publishing Targets
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-blue-950/40 text-blue-400 border border-blue-500/20">
|
||||
PRD AC-15 & AC-16 Verified
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Replicate the already-built and approved local projection to remote
|
||||
targets (e.g. GitHub Mirror, Cloudflare Pages).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isTesting}
|
||||
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#262626] text-white rounded-sm text-xs font-semibold transition-all flex items-center space-x-1.5 border border-[#2A2A2A] uppercase tracking-wider"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-3.5 h-3.5 ${isTesting ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>Test SSH Connection</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handlePush}
|
||||
disabled={isPushing}
|
||||
className="px-5 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-2 uppercase tracking-wider"
|
||||
>
|
||||
<UploadCloud
|
||||
className={`w-4 h-4 ${isPushing ? "animate-bounce" : ""}`}
|
||||
/>
|
||||
<span>
|
||||
{isPushing ? "Rsyncing Artifact..." : "Push Selected Artifact"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testMsg && (
|
||||
<div className="p-4 bg-emerald-950/40 border border-emerald-500/20 rounded-sm text-xs text-emerald-400 flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
<span>{testMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TARGET CONFIG & DEPLOYMENT CONTROL */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT COLUMN: TARGET SELECTION & ARTIFACT SELECTOR (5 COLS) */}
|
||||
<div className="lg:col-span-5 space-y-4">
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<h2 className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest">
|
||||
Configured Target
|
||||
</h2>
|
||||
|
||||
{target ? (
|
||||
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-3 font-mono text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-white text-sm">
|
||||
{target.name}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-emerald-950/40 text-emerald-400 border border-emerald-500/20 rounded-xs text-[10px] font-bold uppercase">
|
||||
{target.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{target.explainWhy && target.status === "skipped" && (
|
||||
<div className="mt-2 p-3 bg-amber-950/20 border border-amber-900/40 rounded-sm">
|
||||
<p className="text-amber-400 font-bold mb-1 font-sans">
|
||||
Target skipped
|
||||
</p>
|
||||
<p className="text-neutral-300 font-sans">
|
||||
{target.explainWhy}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 text-neutral-400 text-[11px] mt-3">
|
||||
<div>
|
||||
Host: <strong className="text-white">{target.host}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Remote Path:{" "}
|
||||
<code className="text-[#C5A059]">{target.remotePath}</code>
|
||||
</div>
|
||||
<div>
|
||||
Secret Reference:{" "}
|
||||
<code className="text-amber-400">{target.credential}</code>
|
||||
</div>
|
||||
<div>
|
||||
Symlink:{" "}
|
||||
<code className="text-neutral-300">
|
||||
{target.currentLink}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-[#1F1F1F] text-[11px]">
|
||||
<span>
|
||||
Last Deployed Build:{" "}
|
||||
<strong className="text-[#C5A059] font-bold">
|
||||
{target.lastDeployedBuildId || "None"}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* ARTIFACT SELECTOR */}
|
||||
<div className="space-y-2 pt-2">
|
||||
<label className="block text-[10px] font-bold text-neutral-400 uppercase tracking-widest">
|
||||
Select Local Release to Push
|
||||
</label>
|
||||
<select
|
||||
value={selectedBuildId}
|
||||
onChange={(e) => setSelectedBuildId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[#141414] border border-[#222222] rounded-sm text-xs text-white font-mono font-semibold focus:outline-none focus:border-[#C5A059]"
|
||||
>
|
||||
{builds
|
||||
.filter((b) => b.status !== "failed")
|
||||
.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.id} - SHA: {b.artifactChecksum.substring(0, 12)}... (
|
||||
{b.isActiveLocal ? "Active Local" : "Retained"})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[11px] text-neutral-500">
|
||||
Only already-compiled local releases are pushed. No remote
|
||||
compilation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: RSYNC LOG TERMINAL & REMOTE VERIFICATION (7 COLS) */}
|
||||
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Terminal className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-base font-bold text-white">
|
||||
Target Synchronization Logs
|
||||
</h2>
|
||||
</div>
|
||||
<span className="px-2.5 py-0.5 bg-[#1A1A1A] text-neutral-300 text-xs font-mono font-semibold rounded-xs border border-[#262626]">
|
||||
Protocol: builtin:ssh-rsync
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0A0A0A] text-neutral-200 font-mono text-xs rounded-sm p-4 border border-[#1F1F1F] space-y-2 max-h-[350px] overflow-y-auto">
|
||||
{pushLogs.length > 0 ? (
|
||||
pushLogs.map((log, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="py-0.5 border-b border-[#141414] last:border-0 text-neutral-300"
|
||||
>
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-neutral-500 py-8 text-center">
|
||||
No push execution logs yet. Click 'Push Selected Artifact'.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-[#1F1F1F] flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>
|
||||
Remote Deployed SHA:{" "}
|
||||
<strong className="text-emerald-400 font-mono">
|
||||
{target?.remoteChecksum?.substring(0, 16) || "None"}...
|
||||
</strong>
|
||||
</span>
|
||||
<span className="text-emerald-400 font-medium">
|
||||
✓ AC-16 Verified
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
import React, { useState } from "react";
|
||||
import { GitSource, ContentItem, SiteConfig } from "../types";
|
||||
import {
|
||||
Globe,
|
||||
LayoutTemplate,
|
||||
FileText,
|
||||
Download,
|
||||
MessageSquare,
|
||||
Rocket,
|
||||
ChevronRight,
|
||||
GitBranch,
|
||||
Search,
|
||||
FolderGit2,
|
||||
FolderOpen,
|
||||
} from "lucide-react";
|
||||
|
||||
interface SiteExplorerTabProps {
|
||||
sources: GitSource[];
|
||||
siteConfig: SiteConfig | null;
|
||||
}
|
||||
|
||||
export const SiteExplorerTab: React.FC<SiteExplorerTabProps> = ({
|
||||
sources,
|
||||
siteConfig,
|
||||
}) => {
|
||||
const defaultSections = siteConfig?.navigation || [];
|
||||
const [selectedSection, setSelectedSection] = useState<string>(
|
||||
defaultSections[0]?.id || "home",
|
||||
);
|
||||
|
||||
const getIcon = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case "Globe":
|
||||
return Globe;
|
||||
case "LayoutTemplate":
|
||||
return LayoutTemplate;
|
||||
case "FileText":
|
||||
return FileText;
|
||||
case "FolderOpen":
|
||||
return FolderOpen;
|
||||
case "MessageSquare":
|
||||
return MessageSquare;
|
||||
case "Download":
|
||||
return Download;
|
||||
default:
|
||||
return Globe;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-6 h-[calc(100vh-180px)] min-h-[600px]">
|
||||
{/* SIDEBAR: Website Structure */}
|
||||
<div className="w-full md:w-64 flex flex-col bg-[#111111] border border-[#1F1F1F] rounded-sm overflow-hidden shrink-0">
|
||||
<div className="p-4 border-b border-[#1F1F1F]">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-widest text-white mb-3">
|
||||
Website Structure
|
||||
</h2>
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-neutral-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search sections..."
|
||||
className="w-full bg-[#0A0A0A] border border-[#1F1F1F] text-white text-xs pl-9 pr-3 py-2 rounded-sm focus:outline-none focus:border-[#C5A059]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-1 no-scrollbar">
|
||||
{defaultSections.map((section) => {
|
||||
const Icon = getIcon(section.iconName);
|
||||
return (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => setSelectedSection(section.id as any)}
|
||||
className={`w-full text-left px-3 py-2.5 rounded-sm flex items-center justify-between transition-colors ${
|
||||
selectedSection === section.id
|
||||
? "bg-[#181818] border border-[#C5A059]/30 text-[#C5A059]"
|
||||
: "text-neutral-400 hover:bg-[#141414] hover:text-white border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2 truncate">
|
||||
<Icon
|
||||
className={`w-4 h-4 shrink-0 ${selectedSection === section.id ? "text-[#C5A059]" : "text-neutral-500"}`}
|
||||
/>
|
||||
<span className="text-sm font-medium truncate">
|
||||
{section.label}
|
||||
</span>
|
||||
</div>
|
||||
{selectedSection === section.id && (
|
||||
<ChevronRight className="w-4 h-4 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN CONTENT AREA */}
|
||||
<div className="flex-1 bg-[#111111] border border-[#1F1F1F] rounded-sm flex flex-col overflow-hidden">
|
||||
{/* Section Header */}
|
||||
<div className="p-6 border-b border-[#1F1F1F]">
|
||||
<div className="flex items-center space-x-2 text-xs font-mono text-neutral-500 mb-2">
|
||||
<span>Generated URL</span>
|
||||
<span>•</span>
|
||||
<span className="text-[#C5A059]">
|
||||
{defaultSections.find((s) => s.id === selectedSection)?.route}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-serif italic text-white">
|
||||
{defaultSections.find((s) => s.id === selectedSection)?.label}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Section Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-[#0A0A0A]">
|
||||
<div className="space-y-4 max-w-2xl text-sm text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
This section is projected from artifacts discovered across
|
||||
configured repositories. The layout and content are generated
|
||||
deterministically based on the artifacts'{" "}
|
||||
<code className="bg-[#1A1A1A] px-1 py-0.5 rounded text-[#C5A059] font-mono text-xs">
|
||||
.labyricorn/site.yml
|
||||
</code>{" "}
|
||||
configurations.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 p-4 border border-[#1F1F1F] bg-[#0A0A0A] rounded-sm">
|
||||
<h3 className="text-white font-semibold mb-4 uppercase tracking-widest text-[10px]">
|
||||
Configuration Trace
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4 font-mono text-xs relative before:absolute before:inset-0 before:ml-[11px] before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-[#1F1F1F] before:to-transparent">
|
||||
{/* Trace Step 1: Definition */}
|
||||
<div className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-[#111111] border border-[#1F1F1F] text-[#C5A059] shadow-sm z-10 shrink-0 md:order-1 md:group-odd:-ml-3 md:group-even:-mr-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#C5A059]"></span>
|
||||
</div>
|
||||
<div className="w-[calc(100%-2rem)] md:w-[calc(50%-1.5rem)] p-3 rounded-sm border border-[#1F1F1F] bg-[#111111] shadow-xs">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-neutral-500 text-[10px] uppercase">
|
||||
Defined In
|
||||
</span>
|
||||
<span className="text-neutral-300">Site Definition</span>
|
||||
</div>
|
||||
<div className="font-semibold text-[#C5A059]">
|
||||
Navigation Entry:{" "}
|
||||
{
|
||||
defaultSections.find((s) => s.id === selectedSection)
|
||||
?.label
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trace Step 2: Content Model */}
|
||||
<div className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-[#111111] border border-[#1F1F1F] text-[#C5A059] shadow-sm z-10 shrink-0 md:order-1 md:group-odd:-ml-3 md:group-even:-mr-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#C5A059]"></span>
|
||||
</div>
|
||||
<div className="w-[calc(100%-2rem)] md:w-[calc(50%-1.5rem)] p-3 rounded-sm border border-[#1F1F1F] bg-[#111111] shadow-xs">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-neutral-500 text-[10px] uppercase">
|
||||
Uses Content Model
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-neutral-300">
|
||||
{defaultSections.find((s) => s.id === selectedSection)
|
||||
?.contentModel || "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trace Step 3: Style Config */}
|
||||
<div className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-[#111111] border border-[#1F1F1F] text-[#C5A059] shadow-sm z-10 shrink-0 md:order-1 md:group-odd:-ml-3 md:group-even:-mr-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#C5A059]"></span>
|
||||
</div>
|
||||
<div className="w-[calc(100%-2rem)] md:w-[calc(50%-1.5rem)] p-3 rounded-sm border border-[#1F1F1F] bg-[#111111] shadow-xs">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-neutral-500 text-[10px] uppercase">
|
||||
Uses Style Configuration
|
||||
</span>
|
||||
<span className="text-[#C5A059] text-[10px]">
|
||||
Site Default
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-neutral-300">
|
||||
{siteConfig?.contentModels?.find(
|
||||
(m) =>
|
||||
m.id ===
|
||||
defaultSections.find((s) => s.id === selectedSection)
|
||||
?.contentModel,
|
||||
)?.defaultStyleConfig || "standard-style"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trace Step 4: Template */}
|
||||
<div className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-[#111111] border border-[#1F1F1F] text-[#C5A059] shadow-sm z-10 shrink-0 md:order-1 md:group-odd:-ml-3 md:group-even:-mr-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#C5A059]"></span>
|
||||
</div>
|
||||
<div className="w-[calc(100%-2rem)] md:w-[calc(50%-1.5rem)] p-3 rounded-sm border border-[#1F1F1F] bg-[#111111] shadow-xs">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-neutral-500 text-[10px] uppercase">
|
||||
Uses Template
|
||||
</span>
|
||||
<span className="text-[#C5A059] text-[10px]">
|
||||
Theme Package
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-neutral-300">
|
||||
{defaultSections.find((s) => s.id === selectedSection)
|
||||
?.contentModel || "generic"}
|
||||
/index.html
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trace Step 5: Route */}
|
||||
<div className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-[#C5A059] text-black shadow-sm z-10 shrink-0 md:order-1 md:group-odd:-ml-3 md:group-even:-mr-3">
|
||||
<span className="w-2 h-2 rounded-full bg-black"></span>
|
||||
</div>
|
||||
<div className="w-[calc(100%-2rem)] md:w-[calc(50%-1.5rem)] p-3 rounded-sm border border-[#C5A059]/50 bg-[#C5A059]/10 shadow-xs">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-neutral-300 text-[10px] uppercase font-bold">
|
||||
Generated Route
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[#C5A059] font-bold">
|
||||
{
|
||||
defaultSections.find((s) => s.id === selectedSection)
|
||||
?.route
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 border border-[#1F1F1F] bg-[#111111] rounded-sm">
|
||||
<h3 className="text-white font-semibold mb-2">
|
||||
Publishing Targets
|
||||
</h3>
|
||||
<ul className="space-y-2 font-mono text-xs">
|
||||
<li className="flex justify-between">
|
||||
<span className="text-neutral-500">Local Site</span>
|
||||
<span className="text-emerald-400">Current</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-neutral-500">GitHub Mirror</span>
|
||||
<span className="text-emerald-400">Current</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Sliders,
|
||||
Folder,
|
||||
GitBranch,
|
||||
FileText,
|
||||
Layers,
|
||||
CheckCircle2,
|
||||
Plus,
|
||||
Code,
|
||||
ShieldAlert,
|
||||
Edit2,
|
||||
} from "lucide-react";
|
||||
import { StyleConfigPackage, StyleConfigInstance, GitSource } from "../types";
|
||||
|
||||
interface StyleInstancesTabProps {
|
||||
packages: StyleConfigPackage[];
|
||||
instances: StyleConfigInstance[];
|
||||
sources: GitSource[];
|
||||
onUpdateInstance: (inst: StyleConfigInstance) => void;
|
||||
}
|
||||
|
||||
export const StyleInstancesTab: React.FC<StyleInstancesTabProps> = ({
|
||||
packages,
|
||||
instances,
|
||||
sources,
|
||||
onUpdateInstance,
|
||||
}) => {
|
||||
const [selectedInstanceId, setSelectedInstanceId] = useState<string>(
|
||||
instances[0]?.id || "devlogs",
|
||||
);
|
||||
const selectedInstance = instances.find((i) => i.id === selectedInstanceId);
|
||||
const selectedPackage = packages.find(
|
||||
(p) =>
|
||||
p.id === (selectedInstance?.uses.replace("builtin:", "") || "devlogs"),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Style-Config Packages & Instances
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-[#C5A059]/10 text-[#C5A059] border border-[#C5A059]/30">
|
||||
PRD AC-03 & AC-04 Compliant
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Reusable presentation packages and site-specific instances defining
|
||||
paths (e.g.{" "}
|
||||
<code className="bg-[#1A1A1A] px-1 rounded text-[#C5A059] font-mono">
|
||||
/.devlogs
|
||||
</code>
|
||||
), repository overrides, media namespaces, and route patterns.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* INSTANCE SELECTOR & PACKAGES SUMMARY */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT LIST: STYLE INSTANCES (4 COLS) */}
|
||||
<div className="lg:col-span-4 space-y-3">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest block px-1">
|
||||
Configured Style Instances ({instances.length})
|
||||
</span>
|
||||
|
||||
{instances.map((inst) => {
|
||||
const isSelected = inst.id === selectedInstanceId;
|
||||
const pkg = packages.find(
|
||||
(p) => p.id === inst.uses.replace("builtin:", ""),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={inst.id}
|
||||
onClick={() => setSelectedInstanceId(inst.id)}
|
||||
className={`p-4 rounded-sm border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "bg-[#181818] border-[#C5A059] shadow-xs"
|
||||
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-bold text-white text-sm font-mono">
|
||||
{inst.id}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-[10px] font-semibold bg-[#1A1A1A] text-neutral-300 rounded-xs border border-[#262626]">
|
||||
{inst.uses}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs text-neutral-400 font-mono">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-neutral-500">Default Path:</span>
|
||||
<span className="text-[#C5A059] font-semibold">
|
||||
{inst.defaultPath}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-neutral-500">
|
||||
Participating Repos:
|
||||
</span>
|
||||
<span className="text-neutral-200 font-semibold">
|
||||
{inst.repositories.length} Repos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-[#1F1F1F] flex items-center justify-between text-[11px] text-neutral-500">
|
||||
<span>
|
||||
Routing:{" "}
|
||||
<code className="text-neutral-300 font-mono">
|
||||
{inst.itemPattern}
|
||||
</code>
|
||||
</span>
|
||||
<span className="text-emerald-400 font-medium">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* BUILT-IN PACKAGES REFERENCE */}
|
||||
<div className="mt-6 bg-[#0A0A0A] rounded-sm p-5 text-white border border-[#1F1F1F] space-y-3">
|
||||
<h3 className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest flex items-center space-x-1.5">
|
||||
<Layers className="w-3.5 h-3.5 text-[#C5A059]" />
|
||||
<span>Built-In Standard Library ({packages.length})</span>
|
||||
</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
{packages.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="p-2.5 bg-[#141414] rounded-sm border border-[#222222]"
|
||||
>
|
||||
<div className="flex items-center justify-between font-mono font-semibold text-neutral-200 text-[11px]">
|
||||
<span>{p.name}</span>
|
||||
<code className="text-[#C5A059] font-mono">
|
||||
builtin:{p.id}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-[11px] text-neutral-500 mt-1 line-clamp-1">
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: SELECTED INSTANCE CONFIGURATION DETAILS (8 COLS) */}
|
||||
<div className="lg:col-span-8 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-6">
|
||||
{selectedInstance ? (
|
||||
<>
|
||||
{/* INSTANCE HEADER & PACKAGE REFERENCE */}
|
||||
<div className="border-b border-[#1F1F1F] pb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Sliders className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-lg font-bold text-white font-mono">
|
||||
{selectedInstance.id} Instance
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">
|
||||
Package:{" "}
|
||||
<strong className="text-neutral-200 font-mono">
|
||||
{selectedInstance.uses}
|
||||
</strong>{" "}
|
||||
({selectedPackage?.description})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-3 py-1 bg-[#1A1A1A] text-[#C5A059] rounded-xs text-xs font-mono font-semibold border border-[#2A2A2A]">
|
||||
Route: {selectedInstance.aggregateRoute}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CORE PATHS & ROUTING RULES */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
||||
<span className="font-bold text-neutral-300 block text-[10px] uppercase tracking-widest">
|
||||
Default Content Path
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Folder className="w-4 h-4 text-[#C5A059]" />
|
||||
<code className="font-mono text-[#C5A059] bg-[#141414] px-2 py-1 rounded-xs border border-[#222222] text-xs font-semibold">
|
||||
{selectedInstance.defaultPath}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-neutral-500 text-[11px]">
|
||||
Scans Markdown files under this relative path across
|
||||
participating Git repos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
||||
<span className="font-bold text-neutral-300 block text-[10px] uppercase tracking-widest">
|
||||
Media Public Namespace
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Folder className="w-4 h-4 text-amber-500" />
|
||||
<code className="font-mono text-amber-400 bg-[#141414] px-2 py-1 rounded-xs border border-[#222222] text-xs font-semibold">
|
||||
{selectedInstance.mediaPublicNamespace}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-neutral-500 text-[11px]">
|
||||
Prevents media path collisions across repositories when
|
||||
serving under{" "}
|
||||
<code className="text-neutral-400 font-mono">/media</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PARTICIPATING REPOSITORIES & REPO ENTRY DEFAULTS */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest">
|
||||
Participating Repositories & Entry Defaults
|
||||
</h3>
|
||||
<span className="text-xs text-[#C5A059] font-medium">
|
||||
Configured in Style Instance (AC-04)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{selectedInstance.repositories.map((repo, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-4 rounded-sm border border-[#1F1F1F] bg-[#0A0A0A] space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<GitBranch className="w-4 h-4 text-[#C5A059]" />
|
||||
<span className="font-bold text-white font-mono text-sm">
|
||||
{repo.source}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-neutral-500 font-mono">
|
||||
Path Override:{" "}
|
||||
<code className="text-neutral-200 bg-[#141414] px-1.5 py-0.5 rounded-xs border border-[#222222]">
|
||||
{repo.path || selectedInstance.defaultPath}
|
||||
</code>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* REPO DEFAULTS GRID */}
|
||||
<div className="p-3 bg-[#111111] rounded-sm border border-[#1F1F1F] text-xs space-y-1 font-mono">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest block mb-1">
|
||||
Repository Level Entry Defaults
|
||||
</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 text-neutral-300">
|
||||
<div>
|
||||
<span className="text-neutral-500">
|
||||
project_id:
|
||||
</span>{" "}
|
||||
<strong className="text-[#C5A059]">
|
||||
{repo.defaults?.projectId || repo.source}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-neutral-500">
|
||||
project_name:
|
||||
</span>{" "}
|
||||
<strong className="text-white">
|
||||
{repo.defaults?.projectName || "Default"}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-neutral-500">
|
||||
project_url:
|
||||
</span>{" "}
|
||||
<strong className="text-white">
|
||||
{repo.defaults?.projectUrl || "/"}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SUPPORTED PACKAGE VIEWS */}
|
||||
{selectedPackage && (
|
||||
<div className="border-t border-[#1F1F1F] pt-4 space-y-3">
|
||||
<h3 className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest">
|
||||
Available Views in Package
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
||||
{Object.entries(selectedPackage.views).map(
|
||||
([viewKey, viewVal]: [string, any]) => (
|
||||
<div
|
||||
key={viewKey}
|
||||
className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-1"
|
||||
>
|
||||
<div className="flex items-center justify-between font-mono font-semibold text-white">
|
||||
<span className="text-[#C5A059]">{viewKey}</span>
|
||||
<span className="text-[10px] text-neutral-500">
|
||||
{viewVal.template}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-neutral-400 text-[11px]">
|
||||
{viewVal.description}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-neutral-500 text-xs">
|
||||
Select a style instance to inspect configuration.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Palette,
|
||||
Folder,
|
||||
FileCode,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
Layout,
|
||||
Layers,
|
||||
ShieldCheck,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { ThemeConfig } from "../types";
|
||||
|
||||
interface ThemeTabProps {
|
||||
theme: ThemeConfig;
|
||||
onValidateTheme: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
theme,
|
||||
onValidateTheme,
|
||||
}) => {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||
|
||||
const handleValidate = async () => {
|
||||
setIsValidating(true);
|
||||
await onValidateTheme();
|
||||
setStatusMsg(
|
||||
"Theme /.theme validated successfully. All required templates, CSS resets, and package supports are valid.",
|
||||
);
|
||||
setIsValidating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-xl font-serif italic text-white">
|
||||
Theme Management
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-[#C5A059]/10 text-[#C5A059] border border-[#C5A059]/30">
|
||||
PRD AC-10 Standard Location: /.theme
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Loads active theme from{" "}
|
||||
<code className="text-[#C5A059] font-mono">/.theme</code>, validates{" "}
|
||||
<code className="text-neutral-300 font-mono">theme.yaml</code>,
|
||||
templates, assets, and supported package layouts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
disabled={isValidating}
|
||||
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-3.5 h-3.5 ${isValidating ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>
|
||||
{isValidating ? "Validating Theme..." : "Validate Theme Manifest"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{statusMsg && (
|
||||
<div className="p-4 bg-emerald-950/40 border border-emerald-500/20 rounded-sm text-xs text-emerald-400 flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
<span>{statusMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* THEME STRUCTURE GRID */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* LEFT COLUMN: THEME METADATA & MANIFEST (5 COLS) */}
|
||||
<div className="lg:col-span-5 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Palette className="w-5 h-5 text-[#C5A059]" />
|
||||
<h2 className="text-base font-bold text-white">{theme.name}</h2>
|
||||
</div>
|
||||
<span className="px-2.5 py-0.5 bg-[#1A1A1A] text-neutral-300 rounded-xs font-mono text-xs font-semibold border border-[#262626]">
|
||||
v{theme.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-xs">
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-1 font-mono">
|
||||
<span className="text-[10px] text-neutral-500 font-bold uppercase tracking-widest block">
|
||||
Theme Root Location
|
||||
</span>
|
||||
<span className="text-[#C5A059] font-bold text-xs">
|
||||
{theme.path}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-1 font-mono">
|
||||
<span className="text-[10px] text-neutral-500 font-bold uppercase tracking-widest block">
|
||||
Theme ID
|
||||
</span>
|
||||
<span className="text-white font-semibold">{theme.id}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
|
||||
<span className="text-[10px] text-neutral-500 font-bold uppercase tracking-widest block font-mono">
|
||||
Supported Style Packages ({theme.supportsPackages.length})
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{theme.supportsPackages.map((pkg) => (
|
||||
<span
|
||||
key={pkg}
|
||||
className="px-2 py-0.5 bg-[#1A1A1A] text-[#C5A059] border border-[#2A2A2A] rounded-xs font-mono text-[10px]"
|
||||
>
|
||||
builtin:{pkg}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: TEMPLATES & ASSETS INSPECTOR (7 COLS) */}
|
||||
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-6">
|
||||
<h2 className="text-base font-bold text-white">
|
||||
Theme Assets & Templates Catalog
|
||||
</h2>
|
||||
|
||||
{/* TEMPLATES LIST */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest block">
|
||||
Required Templates
|
||||
</span>
|
||||
<div className="bg-[#0A0A0A] rounded-sm p-4 text-neutral-200 font-mono text-xs space-y-2 border border-[#1F1F1F]">
|
||||
{Object.entries(theme.templates).map(([key, val]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between py-1 border-b border-[#1A1A1A] last:border-0"
|
||||
>
|
||||
<span className="text-[#C5A059] font-semibold">{key}</span>
|
||||
<span className="text-neutral-400 text-[11px]">{val}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STYLES & SCRIPTS */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs font-mono">
|
||||
<div className="p-3.5 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm space-y-1.5">
|
||||
<span className="font-bold text-neutral-400 uppercase tracking-widest text-[10px] block">
|
||||
CSS Style Assets
|
||||
</span>
|
||||
{theme.styles.map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className="text-neutral-300 text-[11px] flex items-center space-x-1.5"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#C5A059]"></span>
|
||||
<span>{s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-3.5 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm space-y-1.5">
|
||||
<span className="font-bold text-neutral-400 uppercase tracking-widest text-[10px] block">
|
||||
JS Script Assets
|
||||
</span>
|
||||
{theme.scripts.map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className="text-neutral-300 text-[11px] flex items-center space-x-1.5"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
|
||||
<span>{s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
export interface GitSource {
|
||||
id: string;
|
||||
type: string; // Extensible, e.g., 'internal', 'content', 'project', etc.
|
||||
repository: string;
|
||||
ref: string;
|
||||
required: boolean;
|
||||
credential?: string;
|
||||
allowWorkingTree?: boolean;
|
||||
|
||||
// Phase 2 Health and State
|
||||
health: "Healthy" | "Warning" | "Invalid" | "Unreachable" | "Disabled";
|
||||
reachability: "Healthy" | "Unreachable";
|
||||
configurationState: "Valid" | "Invalid" | "Not Configured";
|
||||
visibility: "Public in Gitea" | "Private in Gitea";
|
||||
projectionEnabled: boolean;
|
||||
produces: string[];
|
||||
explainWhy?: string;
|
||||
|
||||
status: "connected" | "syncing" | "error" | "offline" | "idle";
|
||||
lastResolvedCommit?: string;
|
||||
lastSyncedAt?: string;
|
||||
branch: string;
|
||||
tag?: string;
|
||||
isMountedLocal?: boolean;
|
||||
}
|
||||
|
||||
export interface StyleConfigPackage {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
defaultPath: string;
|
||||
views: Record<string, { template: string; description: string }>;
|
||||
features: {
|
||||
tableOfContents?: boolean;
|
||||
revisionInfo?: boolean;
|
||||
repositoryLinks?: boolean;
|
||||
breadcrumbs?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RepositoryOverride {
|
||||
source: string;
|
||||
path?: string;
|
||||
mediaPath?: string;
|
||||
defaults?: {
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
projectUrl?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StyleConfigInstance {
|
||||
id: string;
|
||||
uses: string;
|
||||
enabled: boolean;
|
||||
defaultPath: string;
|
||||
includePatterns: string[];
|
||||
excludePatterns: string[];
|
||||
mediaDefaultPath: string;
|
||||
mediaPublicNamespace: string;
|
||||
itemPattern: string;
|
||||
aggregateRoute: string;
|
||||
repositories: RepositoryOverride[];
|
||||
}
|
||||
|
||||
export interface ContentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
published: string;
|
||||
status: "published" | "draft" | "archived";
|
||||
artifactType: string; // Matches ContentModel ID
|
||||
summary: string;
|
||||
featuredImage?: string;
|
||||
tags: string[];
|
||||
presentation?: {
|
||||
listing?: string;
|
||||
standalone?: string;
|
||||
embedded?: string;
|
||||
};
|
||||
aliases: string[];
|
||||
sourceRepo: string;
|
||||
path: string;
|
||||
contentMarkdown: string;
|
||||
mediaReferences: string[];
|
||||
youtubeDirectives: Array<{ title: string; videoId: string }>;
|
||||
wikipediaLinks: Array<{ text: string; url: string }>;
|
||||
validationStatus: "valid" | "warning" | "error";
|
||||
validationMessages: string[];
|
||||
explainWhy?: string;
|
||||
route: string;
|
||||
styleInstanceId: string;
|
||||
metadata?: {
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
projectUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MediaAsset {
|
||||
id: string;
|
||||
sourceRepo: string;
|
||||
originalPath: string;
|
||||
publicNamespacePath: string;
|
||||
filename: string;
|
||||
sizeBytes: number;
|
||||
mimeType: string;
|
||||
isValidated: boolean;
|
||||
}
|
||||
|
||||
export interface ThemeConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
path: string;
|
||||
templates: Record<string, string>;
|
||||
styles: string[];
|
||||
scripts: string[];
|
||||
supportsPackages: string[];
|
||||
isValidated: boolean;
|
||||
validationErrors: string[];
|
||||
}
|
||||
|
||||
export interface ValidationReport {
|
||||
errors: Array<{
|
||||
code: string;
|
||||
message: string;
|
||||
file?: string;
|
||||
category: string;
|
||||
}>;
|
||||
warnings: Array<{
|
||||
code: string;
|
||||
message: string;
|
||||
file?: string;
|
||||
category: string;
|
||||
}>;
|
||||
missingMedia: string[];
|
||||
routeCollisions: string[];
|
||||
htmlPolicyViolations: string[];
|
||||
brokenLinks: string[];
|
||||
summary: {
|
||||
totalErrors: number;
|
||||
totalWarnings: number;
|
||||
passed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BuildLogEntry {
|
||||
timestamp: string;
|
||||
level: "info" | "warn" | "error" | "success";
|
||||
message: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
export interface BuildRecord {
|
||||
id: string;
|
||||
buildNumber: number;
|
||||
timestamp: string;
|
||||
durationMs: number;
|
||||
status:
|
||||
| "queued"
|
||||
| "resolving-sources"
|
||||
| "validating"
|
||||
| "building"
|
||||
| "built"
|
||||
| "staged"
|
||||
| "active-local"
|
||||
| "pushing"
|
||||
| "synchronized"
|
||||
| "failed";
|
||||
siteDefCommit: string;
|
||||
sourceCommits: Record<string, string>;
|
||||
themeVersion: string;
|
||||
builderVersion: string;
|
||||
generatedRoutesCount: number;
|
||||
mediaCount: number;
|
||||
artifactChecksum: string;
|
||||
artifactSizeBytes: number;
|
||||
validationReport: ValidationReport;
|
||||
logs: BuildLogEntry[];
|
||||
stages: {
|
||||
name: string;
|
||||
status:
|
||||
"pending" | "running" | "succeeded" | "warning" | "failed" | "skipped";
|
||||
durationMs?: number;
|
||||
}[];
|
||||
isStaged: boolean;
|
||||
isActiveLocal: boolean;
|
||||
}
|
||||
|
||||
export interface NginxStatus {
|
||||
isRunning: boolean;
|
||||
configValid: boolean;
|
||||
activeReleaseId: string | null;
|
||||
stagingReleaseId: string | null;
|
||||
listeningPort: number;
|
||||
serverName: string;
|
||||
lastReloadTime: string;
|
||||
lastHealthCheckPassed: boolean;
|
||||
serverBlockConfig: string;
|
||||
stagingServerBlockConfig: string;
|
||||
}
|
||||
|
||||
export interface PushTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
uses: "builtin:ssh-rsync";
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
remotePath: string;
|
||||
credential: string;
|
||||
activationStrategy: "symlink" | "copy";
|
||||
currentLink: string;
|
||||
status:
|
||||
"connected" | "uploading" | "verified" | "failed" | "idle" | "skipped";
|
||||
explainWhy?: string;
|
||||
lastDeployedBuildId?: string;
|
||||
lastPushedAt?: string;
|
||||
remoteChecksum?: string;
|
||||
lastLogs?: string[];
|
||||
}
|
||||
|
||||
export interface CredentialRef {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "ssh_key" | "token" | "password";
|
||||
maskedValue: string;
|
||||
createdAt: string;
|
||||
lastUsedAt: string;
|
||||
usageCount: number;
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
user: string;
|
||||
action: string;
|
||||
category: "config" | "source" | "build" | "nginx" | "push" | "credential";
|
||||
details: string;
|
||||
result: "success" | "failure";
|
||||
}
|
||||
|
||||
export interface NavigationEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
route: string;
|
||||
iconName: string;
|
||||
contentModel?: string;
|
||||
styleConfig?: string;
|
||||
}
|
||||
|
||||
export interface ContentModelSemantics {
|
||||
document?: boolean;
|
||||
chronological?: boolean;
|
||||
searchable?: boolean;
|
||||
indexable?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ContentModelRouting {
|
||||
detail?: string;
|
||||
index?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ContentModelPresentation {
|
||||
default_style_config: string;
|
||||
}
|
||||
|
||||
export interface ContentModel {
|
||||
id: string;
|
||||
kind: 'content-model';
|
||||
semantics: ContentModelSemantics;
|
||||
routing?: ContentModelRouting;
|
||||
presentation: ContentModelPresentation;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
protocol: string;
|
||||
site: {
|
||||
id: string;
|
||||
title: string;
|
||||
baseUrl: string;
|
||||
language: string;
|
||||
};
|
||||
navigation: NavigationEntry[];
|
||||
contentModels: ContentModel[];
|
||||
sourcesFile: string;
|
||||
styleInstancesPath: string;
|
||||
pagesPath: string;
|
||||
navigationFile: string;
|
||||
pushIntegrationsPath: string;
|
||||
theme: {
|
||||
source: string;
|
||||
path: string;
|
||||
};
|
||||
markdown: {
|
||||
dialect: string;
|
||||
rawHtmlPolicy: "disabled" | "sanitized";
|
||||
rawHtmlEnabled: boolean;
|
||||
extensions: {
|
||||
tables: boolean;
|
||||
taskLists: boolean;
|
||||
footnotes: boolean;
|
||||
definitionLists: boolean;
|
||||
headingAnchors: boolean;
|
||||
fencedCode: boolean;
|
||||
syntaxHighlighting: boolean;
|
||||
callouts: boolean;
|
||||
youtube: boolean;
|
||||
wikipediaLinks: boolean;
|
||||
};
|
||||
};
|
||||
hosting: {
|
||||
engine: string;
|
||||
production: {
|
||||
enabled: boolean;
|
||||
hostname: string;
|
||||
listen: number;
|
||||
};
|
||||
staging: {
|
||||
enabled: boolean;
|
||||
hostname: string;
|
||||
listen: number;
|
||||
};
|
||||
releases: {
|
||||
retainCount: number;
|
||||
};
|
||||
};
|
||||
buildPolicy: {
|
||||
staging: {
|
||||
enabled: boolean;
|
||||
requireApproval: boolean;
|
||||
};
|
||||
localActivation: {
|
||||
automatic: boolean;
|
||||
};
|
||||
push: {
|
||||
automatic: boolean;
|
||||
};
|
||||
};
|
||||
rawYaml: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import fs from 'fs';
|
||||
import yaml from 'yaml';
|
||||
|
||||
const configs = {
|
||||
'article': {
|
||||
routing: '/articles/{slug}/',
|
||||
chronological: true,
|
||||
caps: ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'devlog': {
|
||||
routing: '/devlogs/{slug}/',
|
||||
chronological: true,
|
||||
caps: ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'discussion': {
|
||||
routing: '/discussions/{slug}/',
|
||||
chronological: true,
|
||||
caps: ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'documentation': {
|
||||
routing: '/docs/{slug}/',
|
||||
chronological: false,
|
||||
caps: ['detail-page', 'index-page', 'search']
|
||||
},
|
||||
'page': {
|
||||
routing: '/{slug}/',
|
||||
chronological: false,
|
||||
caps: ['detail-page', 'search']
|
||||
},
|
||||
'project': {
|
||||
routing: '/projects/{slug}/',
|
||||
chronological: false,
|
||||
caps: ['detail-page', 'index-page', 'search']
|
||||
},
|
||||
'release': {
|
||||
routing: '/releases/{slug}/',
|
||||
chronological: true,
|
||||
caps: ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'wiki': {
|
||||
routing: '/wiki/{slug}/',
|
||||
chronological: false,
|
||||
caps: ['detail-page', 'index-page', 'search']
|
||||
}
|
||||
};
|
||||
|
||||
for (const modelId of Object.keys(configs)) {
|
||||
const p = `packages/content-models/${modelId}/model.yml`;
|
||||
if (!fs.existsSync(p)) continue;
|
||||
|
||||
const file = fs.readFileSync(p, 'utf8');
|
||||
const data = yaml.parse(file);
|
||||
|
||||
const conf = configs[modelId];
|
||||
const newData = {
|
||||
id: data.id,
|
||||
kind: 'content-model',
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
semantics: {
|
||||
document: true,
|
||||
chronological: conf.chronological,
|
||||
searchable: true,
|
||||
indexable: true
|
||||
},
|
||||
routing: {
|
||||
detail: conf.routing
|
||||
},
|
||||
presentation: {
|
||||
default_style_config: data.defaultStyleConfig || 'article'
|
||||
},
|
||||
capabilities: conf.caps
|
||||
};
|
||||
|
||||
fs.writeFileSync(p, yaml.stringify(newData));
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import yaml
|
||||
import os
|
||||
|
||||
configs = {
|
||||
'article': {
|
||||
'routing': '/articles/{slug}/',
|
||||
'chronological': True,
|
||||
'caps': ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'devlog': {
|
||||
'routing': '/devlogs/{slug}/',
|
||||
'chronological': True,
|
||||
'caps': ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'discussion': {
|
||||
'routing': '/discussions/{slug}/',
|
||||
'chronological': True,
|
||||
'caps': ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'documentation': {
|
||||
'routing': '/docs/{slug}/',
|
||||
'chronological': False,
|
||||
'caps': ['detail-page', 'index-page', 'search']
|
||||
},
|
||||
'page': {
|
||||
'routing': '/{slug}/',
|
||||
'chronological': False,
|
||||
'caps': ['detail-page', 'search']
|
||||
},
|
||||
'project': {
|
||||
'routing': '/projects/{slug}/',
|
||||
'chronological': False,
|
||||
'caps': ['detail-page', 'index-page', 'search']
|
||||
},
|
||||
'release': {
|
||||
'routing': '/releases/{slug}/',
|
||||
'chronological': True,
|
||||
'caps': ['detail-page', 'index-page', 'rss', 'search']
|
||||
},
|
||||
'wiki': {
|
||||
'routing': '/wiki/{slug}/',
|
||||
'chronological': False,
|
||||
'caps': ['detail-page', 'index-page', 'search']
|
||||
}
|
||||
}
|
||||
|
||||
for model_id in configs.keys():
|
||||
path = f"packages/content-models/{model_id}/model.yml"
|
||||
if not os.path.exists(path): continue
|
||||
|
||||
with open(path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
new_data = {
|
||||
'id': data['id'],
|
||||
'kind': 'content-model',
|
||||
'name': data['name'],
|
||||
'description': data['description'],
|
||||
'semantics': {
|
||||
'document': True,
|
||||
'chronological': configs[model_id]['chronological'],
|
||||
'searchable': True,
|
||||
'indexable': True
|
||||
},
|
||||
'routing': {
|
||||
'detail': configs[model_id]['routing']
|
||||
},
|
||||
'presentation': {
|
||||
'default_style_config': data.get('defaultStyleConfig', 'article')
|
||||
},
|
||||
'capabilities': configs[model_id]['caps']
|
||||
}
|
||||
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(new_data, f, sort_keys=False)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "."),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== "true",
|
||||
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||
watch: process.env.DISABLE_HMR === "true" ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user