Page MenuHomeDevCentral

D3964.diff
No OneTemporary

D3964.diff

diff --git a/frontend/index.html b/frontend/index.html
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,14 +1,17 @@
<!DOCTYPE html>
-<html lang="en">
+<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Vite App</title>
+ <title>ServPulse</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
-<body>
+<body class="h-full">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,7 +9,6 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.4.0",
- "foundation-sites": "^6.7.5",
"vue": "^3.3.2",
"vue-router": "^4.2.0"
},
@@ -18,14 +17,30 @@
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/test-utils": "^2.3.2",
+ "autoprefixer": "^10.4.24",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.11.0",
"jsdom": "^22.0.0",
+ "postcss": "^8.5.6",
"prettier": "^2.8.8",
+ "tailwindcss": "^3.4.19",
"vite": "^4.3.5",
"vitest": "^0.31.0"
}
},
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -72,13 +87,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@bufbuild/protobuf": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
- "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
- "license": "(Apache-2.0 AND BSD-3-Clause)",
- "peer": true
- },
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@@ -601,12 +609,44 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1409,6 +1449,47 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1432,6 +1513,43 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
+ "node_modules/autoprefixer": {
+ "version": "10.4.24",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
+ "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001766",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
@@ -1450,6 +1568,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/blueimp-md5": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
@@ -1475,6 +1616,53 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -1508,6 +1696,37 @@
"node": ">=6"
}
},
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001770",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/chai": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
@@ -1561,6 +1780,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
"license": "MIT",
"optional": true,
"peer": true,
@@ -1594,13 +1814,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/colorjs.io": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
- "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
- "license": "MIT",
- "peer": true
- },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1809,6 +2022,20 @@
"node": ">=8"
}
},
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -1902,6 +2129,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -2005,6 +2239,16 @@
"@esbuild/win32-x64": "0.18.20"
}
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2247,6 +2491,36 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2271,6 +2545,24 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -2284,6 +2576,19 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2376,18 +2681,18 @@
"node": ">= 6"
}
},
- "node_modules/foundation-sites": {
- "version": "6.9.0",
- "resolved": "https://registry.npmjs.org/foundation-sites/-/foundation-sites-6.9.0.tgz",
- "integrity": "sha512-bZpjL6lxuGViy1UU/NuMddyAAJP0YgJYKNh+8ZtDWhQRtiY1yQISxHmgF4V8EFx7DWHDTdZC1fwFysnMAYkdQg==",
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=18.0"
+ "node": "*"
},
- "peerDependencies": {
- "jquery": ">=3.6.0",
- "motion-ui": "latest",
- "what-input": ">=5.2.10"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fs.realpath": {
@@ -2568,6 +2873,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2681,7 +2987,9 @@
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+ "dev": true,
"license": "MIT",
+ "optional": true,
"peer": true
},
"node_modules/import-fresh": {
@@ -2737,6 +3045,35 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2770,6 +3107,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -2810,12 +3157,15 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
- "node_modules/jquery": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz",
- "integrity": "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg==",
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
"license": "MIT",
- "peer": true
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
},
"node_modules/js-beautify": {
"version": "1.15.4",
@@ -2960,23 +3310,43 @@
"node": ">= 0.8.0"
}
},
- "node_modules/local-pkg": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
- "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
- "url": "https://github.com/sponsors/antfu"
+ "url": "https://github.com/sponsors/antonk52"
}
},
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/local-pkg": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
+ "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3051,6 +3421,43 @@
"node": ">=8"
}
},
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3115,19 +3522,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/motion-ui": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/motion-ui/-/motion-ui-2.0.8.tgz",
- "integrity": "sha512-kFPFUYslkA9NcnrbErCdGlSGGKBxSTfp7FToapj8/wawPIoks88H1bpNmqRh0EzU+u7PydUp2YSmmO3Sv+ZiBQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "sass-embedded": "^1.79.3"
- },
- "peerDependencies": {
- "jquery": ">=3.6.0"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3135,6 +3529,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -3169,6 +3575,13 @@
"optional": true,
"peer": true
},
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
@@ -3185,6 +3598,16 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -3205,6 +3628,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3328,6 +3771,13 @@
"node": ">=8"
}
},
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@@ -3374,8 +3824,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3383,6 +3831,26 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -3430,6 +3898,119 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
@@ -3444,6 +4025,13 @@
"node": ">=4"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3582,10 +4170,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
"license": "MIT",
"optional": true,
"peer": true,
@@ -3604,6 +4203,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3712,16 +4332,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "license": "Apache-2.0",
- "peer": true,
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3733,6 +4343,7 @@
"version": "1.97.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
+ "dev": true,
"license": "MIT",
"optional": true,
"peer": true,
@@ -3751,407 +4362,43 @@
"@parcel/watcher": "^2.4.1"
}
},
- "node_modules/sass-embedded": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.3.tgz",
- "integrity": "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==",
- "license": "MIT",
- "peer": true,
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "@bufbuild/protobuf": "^2.5.0",
- "colorjs.io": "^0.5.0",
- "immutable": "^5.0.2",
- "rxjs": "^7.4.0",
- "supports-color": "^8.1.1",
- "sync-child-process": "^1.0.2",
- "varint": "^6.0.0"
+ "xmlchars": "^2.2.0"
},
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
"bin": {
- "sass": "dist/bin/sass.js"
+ "semver": "bin/semver.js"
},
"engines": {
- "node": ">=16.0.0"
- },
- "optionalDependencies": {
- "sass-embedded-all-unknown": "1.97.3",
- "sass-embedded-android-arm": "1.97.3",
- "sass-embedded-android-arm64": "1.97.3",
- "sass-embedded-android-riscv64": "1.97.3",
- "sass-embedded-android-x64": "1.97.3",
- "sass-embedded-darwin-arm64": "1.97.3",
- "sass-embedded-darwin-x64": "1.97.3",
- "sass-embedded-linux-arm": "1.97.3",
- "sass-embedded-linux-arm64": "1.97.3",
- "sass-embedded-linux-musl-arm": "1.97.3",
- "sass-embedded-linux-musl-arm64": "1.97.3",
- "sass-embedded-linux-musl-riscv64": "1.97.3",
- "sass-embedded-linux-musl-x64": "1.97.3",
- "sass-embedded-linux-riscv64": "1.97.3",
- "sass-embedded-linux-x64": "1.97.3",
- "sass-embedded-unknown-all": "1.97.3",
- "sass-embedded-win32-arm64": "1.97.3",
- "sass-embedded-win32-x64": "1.97.3"
- }
- },
- "node_modules/sass-embedded-all-unknown": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.3.tgz",
- "integrity": "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==",
- "cpu": [
- "!arm",
- "!arm64",
- "!riscv64",
- "!x64"
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "sass": "1.97.3"
+ "node": ">=10"
}
},
- "node_modules/sass-embedded-android-arm": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.3.tgz",
- "integrity": "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==",
- "cpu": [
- "arm"
- ],
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
"engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-android-arm64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.3.tgz",
- "integrity": "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-android-riscv64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.3.tgz",
- "integrity": "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-android-x64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.3.tgz",
- "integrity": "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-darwin-arm64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.3.tgz",
- "integrity": "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-darwin-x64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.3.tgz",
- "integrity": "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-arm": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.3.tgz",
- "integrity": "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-arm64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.3.tgz",
- "integrity": "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-musl-arm": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.3.tgz",
- "integrity": "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-musl-arm64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.3.tgz",
- "integrity": "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-musl-riscv64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.3.tgz",
- "integrity": "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-musl-x64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.3.tgz",
- "integrity": "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-riscv64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.3.tgz",
- "integrity": "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-linux-x64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.3.tgz",
- "integrity": "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-unknown-all": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.3.tgz",
- "integrity": "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==",
- "license": "MIT",
- "optional": true,
- "os": [
- "!android",
- "!darwin",
- "!linux",
- "!win32"
- ],
- "peer": true,
- "dependencies": {
- "sass": "1.97.3"
- }
- },
- "node_modules/sass-embedded-win32-arm64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.3.tgz",
- "integrity": "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded-win32-x64": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.3.tgz",
- "integrity": "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/sass-embedded/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "xmlchars": "^2.2.0"
- },
- "engines": {
- "node": ">=v12.22.7"
- }
- },
- "node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
+ "node": ">=8"
}
},
"node_modules/shebang-regex": {
@@ -4330,6 +4577,39 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4343,6 +4623,19 @@
"node": ">=8"
}
},
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -4350,27 +4643,106 @@
"dev": true,
"license": "MIT"
},
- "node_modules/sync-child-process": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
- "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "sync-message-port": "^1.0.0"
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">= 6"
}
},
- "node_modules/sync-message-port": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz",
- "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==",
+ "node_modules/tailwindcss/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"engines": {
- "node": ">=16.0.0"
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
}
},
"node_modules/text-table": {
@@ -4380,6 +4752,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/time-zone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz",
@@ -4397,6 +4792,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
"node_modules/tinypool": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz",
@@ -4417,6 +4829,19 @@
"node": ">=14.0.0"
}
},
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@@ -4446,12 +4871,12 @@
"node": ">=14"
}
},
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "peer": true
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -4513,6 +4938,37 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -4541,13 +4997,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/varint": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
- "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
- "license": "MIT",
- "peer": true
- },
"node_modules/vite": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
@@ -4808,13 +5257,6 @@
"node": ">=6"
}
},
- "node_modules/what-input": {
- "version": "5.2.12",
- "resolved": "https://registry.npmjs.org/what-input/-/what-input-5.2.12.tgz",
- "integrity": "sha512-3yrSa7nGSXGJS6wZeSkO6VNm95pB1mZ9i3wFzC1hhY7mn4/afue/MvXz04OXNdBC8bfo4AB4RRd3Dem9jXM58Q==",
- "license": "MIT",
- "peer": true
- },
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,7 +12,6 @@
},
"dependencies": {
"axios": "^1.4.0",
- "foundation-sites": "^6.7.5",
"vue": "^3.3.2",
"vue-router": "^4.2.0"
},
@@ -21,10 +20,13 @@
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/test-utils": "^2.3.2",
+ "autoprefixer": "^10.4.24",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.11.0",
"jsdom": "^22.0.0",
+ "postcss": "^8.5.6",
"prettier": "^2.8.8",
+ "tailwindcss": "^3.4.19",
"vite": "^4.3.5",
"vitest": "^0.31.0"
}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,9 +1,15 @@
<script setup>
-import { RouterLink, RouterView } from 'vue-router'
-import NavbarSection from './components/home/NavbarSection.vue'
+import { RouterView } from 'vue-router'
+import AppNavbar from '@/components/AppNavbar.vue'
+import AppFooter from '@/components/AppFooter.vue'
</script>
<template>
- <NavbarSection />
- <RouterView />
+ <div class="min-h-screen flex flex-col">
+ <AppNavbar />
+ <main class="flex-1">
+ <RouterView />
+ </main>
+ <AppFooter />
+ </div>
</template>
diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css
deleted file mode 100644
--- a/frontend/src/assets/base.css
+++ /dev/null
@@ -1,73 +0,0 @@
-/* color palette from <https://github.com/vuejs/theme> */
-:root {
- --vt-c-white: #ffffff;
- --vt-c-white-soft: #f8f8f8;
- --vt-c-white-mute: #f2f2f2;
-
- --vt-c-black: #181818;
- --vt-c-black-soft: #222222;
- --vt-c-black-mute: #282828;
-
- --vt-c-indigo: #2c3e50;
-
- --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
- --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
- --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
- --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
-
- --vt-c-text-light-1: var(--vt-c-indigo);
- --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
- --vt-c-text-dark-1: var(--vt-c-white);
- --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
-}
-
-/* semantic color variables for this project */
-:root {
- --color-background: var(--vt-c-white);
- --color-background-soft: var(--vt-c-white-soft);
- --color-background-mute: var(--vt-c-white-mute);
-
- --color-border: var(--vt-c-divider-light-2);
- --color-border-hover: var(--vt-c-divider-light-1);
-
- --color-heading: var(--vt-c-text-light-1);
- --color-text: var(--vt-c-text-light-1);
-
- --section-gap: 160px;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --color-background: var(--vt-c-black);
- --color-background-soft: var(--vt-c-black-soft);
- --color-background-mute: var(--vt-c-black-mute);
-
- --color-border: var(--vt-c-divider-dark-2);
- --color-border-hover: var(--vt-c-divider-dark-1);
-
- --color-heading: var(--vt-c-text-dark-1);
- --color-text: var(--vt-c-text-dark-2);
- }
-}
-
-*,
-*::before,
-*::after {
- box-sizing: border-box;
- margin: 0;
- font-weight: normal;
-}
-
-body {
- min-height: 100vh;
- color: var(--color-text);
- background: var(--color-background);
- transition: color 0.5s, background-color 0.5s;
- line-height: 1.6;
- font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
- Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
- font-size: 15px;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css
--- a/frontend/src/assets/main.css
+++ b/frontend/src/assets/main.css
@@ -1,35 +1,44 @@
-@import './base.css';
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
-#app {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
-
- font-weight: normal;
+@layer base {
+ body {
+ @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100 antialiased;
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+ Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ }
}
-a,
-.green {
- text-decoration: none;
- color: hsla(160, 100%, 37%, 1);
- transition: 0.4s;
-}
+@layer components {
+ .btn-primary {
+ @apply inline-flex items-center px-4 py-2 bg-brand-600 text-white text-sm font-medium rounded-lg
+ hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
+ transition-colors duration-200;
+ }
-@media (hover: hover) {
- a:hover {
- background-color: hsla(160, 100%, 37%, 0.2);
+ .btn-secondary {
+ @apply inline-flex items-center px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-lg
+ hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600
+ focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2
+ transition-colors duration-200;
}
-}
-@media (min-width: 1024px) {
- body {
- display: flex;
- place-items: center;
+ .btn-danger {
+ @apply inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg
+ hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2
+ transition-colors duration-200;
+ }
+
+ .card {
+ @apply bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800;
}
- #app {
- display: grid;
- grid-template-columns: 1fr 1fr;
- padding: 0 2rem;
+ .input-field {
+ @apply block w-full rounded-lg border border-gray-300 dark:border-gray-600
+ bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-gray-100
+ placeholder-gray-400 dark:placeholder-gray-500
+ focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500
+ transition-colors duration-200;
}
}
diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/AppFooter.vue
@@ -0,0 +1,5 @@
+<template>
+ <footer class="mt-12 pb-8 text-center text-xs text-gray-400 dark:text-gray-500">
+ <p>Powered by <a href="https://devcentral.nasqueron.org/source/servpulse" class="hover:text-gray-600 dark:hover:text-gray-300 transition-colors underline">ServPulse</a></p>
+ </footer>
+</template>
diff --git a/frontend/src/components/AppNavbar.vue b/frontend/src/components/AppNavbar.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/AppNavbar.vue
@@ -0,0 +1,75 @@
+<script setup>
+import { ref, onMounted } from 'vue'
+import { RouterLink } from 'vue-router'
+import { configApi } from '@/plugins/api'
+import { useAuth } from '@/composables/useAuth'
+
+const config = ref(null)
+const { isAuthenticated, logout } = useAuth()
+
+onMounted(async () => {
+ try {
+ config.value = await configApi.getAll()
+ } catch {
+ config.value = { navbar: { title: 'ServPulse', button_left: null, button_right: null } }
+ }
+})
+
+const handleLogout = () => {
+ logout()
+ window.location.href = '/'
+}
+</script>
+
+<template>
+ <nav class="bg-gray-900 text-white shadow-lg">
+ <div class="max-w-5xl mx-auto px-4 sm:px-6">
+ <div class="flex items-center justify-between h-14">
+ <div class="flex items-center gap-4">
+ <a
+ v-if="config?.navbar?.button_left"
+ :href="config.navbar.button_left.link"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ {{ config.navbar.button_left.name }}
+ </a>
+ </div>
+
+ <RouterLink to="/" class="text-sm font-bold tracking-widest uppercase hover:text-gray-300 transition-colors">
+ {{ config?.navbar?.title || 'ServPulse' }}
+ </RouterLink>
+
+ <div class="flex items-center gap-4">
+ <RouterLink
+ v-if="isAuthenticated"
+ to="/admin"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ Dashboard
+ </RouterLink>
+ <button
+ v-if="isAuthenticated"
+ @click="handleLogout"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ Logout
+ </button>
+ <RouterLink
+ v-else
+ to="/admin/login"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ Admin
+ </RouterLink>
+ <a
+ v-if="config?.navbar?.button_right"
+ :href="config.navbar.button_right.link"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ {{ config.navbar.button_right.name }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </nav>
+</template>
diff --git a/frontend/src/components/IncidentTimeline.vue b/frontend/src/components/IncidentTimeline.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/IncidentTimeline.vue
@@ -0,0 +1,48 @@
+<script setup>
+import { computed } from 'vue'
+import { getIncidentStatus, getImpactLevel, formatDate, timeAgo } from '@/utils/status'
+
+const props = defineProps({
+ incident: { type: Object, required: true },
+})
+
+const statusConfig = computed(() => getIncidentStatus(props.incident.status))
+const impactConfig = computed(() => getImpactLevel(props.incident.impact))
+</script>
+
+<template>
+ <div class="card p-5">
+ <div class="flex items-start justify-between gap-4">
+ <div>
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
+ {{ incident.title }}
+ </h3>
+ <div class="mt-1 flex items-center gap-3 text-xs">
+ <span :class="statusConfig.color" class="font-medium">{{ statusConfig.label }}</span>
+ <span class="text-gray-400">·</span>
+ <span :class="impactConfig.color">{{ impactConfig.label }} Impact</span>
+ <span class="text-gray-400">·</span>
+ <span class="text-gray-500 dark:text-gray-400">{{ timeAgo(incident.start_date) }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="incident.updates && incident.updates.length" class="mt-4 ml-3 border-l-2 border-gray-200 dark:border-gray-700 pl-4 space-y-4">
+ <div v-for="update in incident.updates" :key="update.id" class="relative">
+ <div class="absolute -left-[1.35rem] top-1 h-2.5 w-2.5 rounded-full border-2 border-white dark:border-gray-900"
+ :class="{
+ 'bg-red-500': update.status === 'investigating',
+ 'bg-orange-500': update.status === 'identified',
+ 'bg-yellow-500': update.status === 'monitoring',
+ 'bg-green-500': update.status === 'resolved',
+ }"
+ ></div>
+ <p class="text-xs font-medium text-gray-500 dark:text-gray-400">
+ {{ getIncidentStatus(update.status).label }}
+ <span class="font-normal">— {{ formatDate(update.created_at) }}</span>
+ </p>
+ <p class="mt-0.5 text-sm text-gray-700 dark:text-gray-300">{{ update.message }}</p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/frontend/src/components/MaintenanceCard.vue b/frontend/src/components/MaintenanceCard.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/MaintenanceCard.vue
@@ -0,0 +1,50 @@
+<script setup>
+import { computed } from 'vue'
+import { formatDate } from '@/utils/status'
+
+const props = defineProps({
+ maintenance: { type: Object, required: true },
+})
+
+const statusLabel = computed(() => {
+ const labels = {
+ scheduled: 'Scheduled',
+ in_progress: 'In Progress',
+ completed: 'Completed',
+ }
+ return labels[props.maintenance.status] || props.maintenance.status
+})
+
+const statusColor = computed(() => {
+ const colors = {
+ scheduled: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
+ in_progress: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
+ completed: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
+ }
+ return colors[props.maintenance.status] || colors.scheduled
+})
+</script>
+
+<template>
+ <div class="card p-5">
+ <div class="flex items-start justify-between gap-4">
+ <div class="flex-1">
+ <div class="flex items-center gap-2">
+ <span class="text-blue-500">🔧</span>
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
+ {{ maintenance.title }}
+ </h3>
+ </div>
+ <p v-if="maintenance.description" class="mt-1 text-sm text-gray-600 dark:text-gray-400">
+ {{ maintenance.description }}
+ </p>
+ <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
+ {{ formatDate(maintenance.scheduled_start) }} — {{ formatDate(maintenance.scheduled_end) }}
+ </p>
+ </div>
+ <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium" :class="statusColor">
+ {{ statusLabel }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/frontend/src/components/OverallStatus.vue b/frontend/src/components/OverallStatus.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/OverallStatus.vue
@@ -0,0 +1,40 @@
+<script setup>
+import { computed } from 'vue'
+import { getOverallStatus, getStatusConfig } from '@/utils/status'
+
+const props = defineProps({
+ services: { type: Array, default: () => [] },
+})
+
+const overall = computed(() => getOverallStatus(props.services))
+const config = computed(() => getStatusConfig(overall.value))
+
+const bannerClass = computed(() => {
+ const classes = {
+ operational: 'bg-emerald-500 dark:bg-emerald-600',
+ degraded: 'bg-amber-500 dark:bg-amber-600',
+ partial: 'bg-orange-500 dark:bg-orange-600',
+ major: 'bg-red-500 dark:bg-red-600',
+ maintenance: 'bg-blue-500 dark:bg-blue-600',
+ }
+ return classes[overall.value] || classes.operational
+})
+
+const message = computed(() => {
+ const messages = {
+ operational: 'All Systems Operational',
+ degraded: 'Some Systems Experiencing Degraded Performance',
+ partial: 'Partial System Outage',
+ major: 'Major System Outage',
+ maintenance: 'Scheduled Maintenance In Progress',
+ }
+ return messages[overall.value] || messages.operational
+})
+</script>
+
+<template>
+ <div class="rounded-xl p-6 text-white text-center transition-colors duration-500" :class="bannerClass">
+ <p class="text-2xl font-semibold">{{ message }}</p>
+ <p class="mt-1 text-sm opacity-80">{{ config.icon }} {{ config.label }}</p>
+ </div>
+</template>
diff --git a/frontend/src/components/ServiceGroup.vue b/frontend/src/components/ServiceGroup.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/ServiceGroup.vue
@@ -0,0 +1,33 @@
+<script setup>
+import StatusBadge from '@/components/StatusBadge.vue'
+
+defineProps({
+ groupName: { type: String, required: true },
+ services: { type: Array, required: true },
+})
+</script>
+
+<template>
+ <div class="card overflow-hidden">
+ <div class="px-5 py-3 border-b border-gray-200 dark:border-gray-800">
+ <h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
+ {{ groupName }}
+ </h3>
+ </div>
+ <ul class="divide-y divide-gray-100 dark:divide-gray-800">
+ <li
+ v-for="service in services"
+ :key="service.id"
+ class="flex items-center justify-between px-5 py-3.5 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
+ >
+ <div>
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ service.name }}</p>
+ <p v-if="service.description" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
+ {{ service.description }}
+ </p>
+ </div>
+ <StatusBadge :status="service.status" />
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/frontend/src/components/StatusBadge.vue b/frontend/src/components/StatusBadge.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/StatusBadge.vue
@@ -0,0 +1,35 @@
+<script setup>
+import { computed } from 'vue'
+import { getStatusConfig } from '@/utils/status'
+
+const props = defineProps({
+ status: { type: String, required: true },
+ size: { type: String, default: 'sm' },
+})
+
+const config = computed(() => getStatusConfig(props.status))
+
+const dotClass = computed(() => {
+ const colors = {
+ operational: 'bg-status-operational',
+ degraded: 'bg-status-degraded',
+ partial: 'bg-status-partial',
+ major: 'bg-status-major',
+ maintenance: 'bg-status-maintenance',
+ }
+ return colors[props.status] || colors.operational
+})
+
+const sizeClass = computed(() => props.size === 'lg' ? 'text-sm px-3 py-1.5' : 'text-xs px-2 py-1')
+const dotSize = computed(() => props.size === 'lg' ? 'h-2.5 w-2.5' : 'h-2 w-2')
+</script>
+
+<template>
+ <span
+ class="inline-flex items-center gap-1.5 rounded-full font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300"
+ :class="sizeClass"
+ >
+ <span class="rounded-full" :class="[dotClass, dotSize]"></span>
+ {{ config.label }}
+ </span>
+</template>
diff --git a/frontend/src/components/home/NavbarSection.vue b/frontend/src/components/home/NavbarSection.vue
deleted file mode 100644
--- a/frontend/src/components/home/NavbarSection.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
- <!-- Used: https://get.foundation/building-blocks/blocks/topbar-center-logo.html -->
- <main>
- <div class="top-bar topbar-center-logo" id="topbar-center-logo">
- <div class="top-bar-left">
- <ul class="menu vertical medium-horizontal">
- <li><a :href="apiResponse.navbar.button_left.link" class="button">{{ apiResponse.navbar.button_left.name }}</a></li>
- </ul>
- </div>
- <div class="top-bar-center">
- <span class="navbar-brand">{{ apiResponse.navbar.title }}</span>
- </div>
- <div class="top-bar-right">
- <ul class="menu vertical medium-horizontal">
- <li><a :href="apiResponse.navbar.button_right.link" class="button">{{ apiResponse.navbar.button_right.name }}</a></li>
- </ul>
- </div>
- </div>
- </main>
-</template>
-
-<script>
-import { fetchConfigData } from "../../plugins/api";
-
-export default {
- data() {
- return {
- apiResponse: '',
- };
- },
- async created() {
- this.apiResponse = await fetchConfigData();
- },
-};
-</script>
-
-
-<style>
-.topbar-center-logo {
- background: #2c3840;
-}
-
-.topbar-center-logo .menu {
- background: #2c3840;
-}
-
-.topbar-center-logo .menu a {
- color: #fefefe;
-}
-
-.topbar-center-logo .top-bar-center {
- -webkit-flex: 1 0 auto;
- -ms-flex: 1 0 auto;
- flex: 1 0 auto;
-}
-
-.top-bar-center {
- color: #fefefe;
- font-weight: 700;
- text-transform: uppercase;
- word-spacing: 1px; letter-spacing:2px;
-}
-
-@media screen and (max-width: 39.9375em) {
- .topbar-center-logo .top-bar-center {
- display: none;
- }
-}
-</style>
diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useAuth.js
@@ -0,0 +1,21 @@
+import { ref, computed } from 'vue'
+
+const TOKEN_KEY = 'servpulse_token'
+
+export function useAuth() {
+ const token = ref(localStorage.getItem(TOKEN_KEY))
+
+ const isAuthenticated = computed(() => !!token.value)
+
+ const login = (newToken) => {
+ token.value = newToken
+ localStorage.setItem(TOKEN_KEY, newToken)
+ }
+
+ const logout = () => {
+ token.value = null
+ localStorage.removeItem(TOKEN_KEY)
+ }
+
+ return { token, isAuthenticated, login, logout }
+}
diff --git a/frontend/src/composables/useIncidents.js b/frontend/src/composables/useIncidents.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useIncidents.js
@@ -0,0 +1,30 @@
+import { ref } from 'vue'
+import { incidentsApi } from '@/plugins/api'
+
+export function useIncidents() {
+ const incidents = ref([])
+ const loading = ref(false)
+ const error = ref(null)
+
+ const fetchIncidents = async () => {
+ loading.value = true
+ error.value = null
+ try {
+ incidents.value = await incidentsApi.getAll()
+ } catch (err) {
+ error.value = err.message
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const activeIncidents = () => {
+ return incidents.value.filter((i) => i.status !== 'resolved')
+ }
+
+ const resolvedIncidents = () => {
+ return incidents.value.filter((i) => i.status === 'resolved')
+ }
+
+ return { incidents, loading, error, fetchIncidents, activeIncidents, resolvedIncidents }
+}
diff --git a/frontend/src/composables/useMaintenances.js b/frontend/src/composables/useMaintenances.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useMaintenances.js
@@ -0,0 +1,33 @@
+import { ref } from 'vue'
+import { maintenancesApi } from '@/plugins/api'
+
+export function useMaintenances() {
+ const maintenances = ref([])
+ const loading = ref(false)
+ const error = ref(null)
+
+ const fetchMaintenances = async () => {
+ loading.value = true
+ error.value = null
+ try {
+ maintenances.value = await maintenancesApi.getAll()
+ } catch (err) {
+ error.value = err.message
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const upcoming = () => {
+ const now = new Date()
+ return maintenances.value.filter(
+ (m) => m.status === 'scheduled' && new Date(m.scheduled_start) > now
+ )
+ }
+
+ const inProgress = () => {
+ return maintenances.value.filter((m) => m.status === 'in_progress')
+ }
+
+ return { maintenances, loading, error, fetchMaintenances, upcoming, inProgress }
+}
diff --git a/frontend/src/composables/useServices.js b/frontend/src/composables/useServices.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useServices.js
@@ -0,0 +1,32 @@
+import { ref } from 'vue'
+import { servicesApi } from '@/plugins/api'
+
+export function useServices() {
+ const services = ref([])
+ const loading = ref(false)
+ const error = ref(null)
+
+ const fetchServices = async () => {
+ loading.value = true
+ error.value = null
+ try {
+ services.value = await servicesApi.getAll()
+ } catch (err) {
+ error.value = err.message
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const groupedServices = () => {
+ const groups = {}
+ for (const service of services.value) {
+ const group = service.group || 'Uncategorized'
+ if (!groups[group]) groups[group] = []
+ groups[group].push(service)
+ }
+ return groups
+ }
+
+ return { services, loading, error, fetchServices, groupedServices }
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,12 +1,11 @@
-// import './assets/main.css'; // Customize CSS
-import 'foundation-sites/dist/css/foundation.css';
+import './assets/main.css'
-import { createApp } from 'vue';
-import App from './App.vue';
-import router from './router';
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
-const app = createApp(App);
+const app = createApp(App)
-app.use(router);
+app.use(router)
-app.mount('#app');
+app.mount('#app')
diff --git a/frontend/src/plugins/api.js b/frontend/src/plugins/api.js
--- a/frontend/src/plugins/api.js
+++ b/frontend/src/plugins/api.js
@@ -1,12 +1,45 @@
-import axios from 'axios';
+import axios from 'axios'
-const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
+const apiClient = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
+ headers: { 'Content-Type': 'application/json' },
+})
-export async function fetchConfigData() {
- try {
- const response = await axios.get(`${apiUrl}/config/getAll`);
- return response.data;
- } catch (error) {
- throw new Error('Failed to fetch the config data');
- }
+apiClient.interceptors.request.use((config) => {
+ const token = localStorage.getItem('servpulse_token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+})
+
+export const configApi = {
+ getAll: () => apiClient.get('/config/getAll').then((r) => r.data),
+}
+
+export const servicesApi = {
+ getAll: () => apiClient.get('/services').then((r) => r.data),
+ getById: (id) => apiClient.get(`/services/${id}`).then((r) => r.data),
+ create: (data) => apiClient.post('/services', data).then((r) => r.data),
+ update: (id, data) => apiClient.put(`/services/${id}`, data).then((r) => r.data),
+ delete: (id) => apiClient.delete(`/services/${id}`).then((r) => r.data),
+}
+
+export const incidentsApi = {
+ getAll: () => apiClient.get('/incidents').then((r) => r.data),
+ getById: (id) => apiClient.get(`/incidents/${id}`).then((r) => r.data),
+ create: (data) => apiClient.post('/incidents', data).then((r) => r.data),
+ update: (id, data) => apiClient.put(`/incidents/${id}`, data).then((r) => r.data),
+ resolve: (id, message) =>
+ apiClient.put(`/incidents/${id}/resolve`, { message }).then((r) => r.data),
+}
+
+export const maintenancesApi = {
+ getAll: () => apiClient.get('/maintenances').then((r) => r.data),
+ getById: (id) => apiClient.get(`/maintenances/${id}`).then((r) => r.data),
+ create: (data) => apiClient.post('/maintenances', data).then((r) => r.data),
+ update: (id, data) => apiClient.put(`/maintenances/${id}`, data).then((r) => r.data),
+ delete: (id) => apiClient.delete(`/maintenances/${id}`).then((r) => r.data),
}
+
+export default apiClient
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -1,15 +1,35 @@
import { createRouter, createWebHistory } from 'vue-router'
-import HomeView from '../views/HomeView.vue'
+import StatusPage from '@/views/StatusPage.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
- name: 'home',
- component: HomeView
+ name: 'status',
+ component: StatusPage,
+ },
+ {
+ path: '/admin/login',
+ name: 'admin-login',
+ component: () => import('@/views/AdminLogin.vue'),
+ },
+ {
+ path: '/admin',
+ name: 'admin',
+ component: () => import('@/views/AdminDashboard.vue'),
+ meta: { requiresAuth: true },
+ },
+ ],
+})
+
+router.beforeEach((to) => {
+ if (to.meta.requiresAuth) {
+ const token = localStorage.getItem('servpulse_token')
+ if (!token) {
+ return { name: 'admin-login' }
}
- ]
+ }
})
export default router
diff --git a/frontend/src/utils/status.js b/frontend/src/utils/status.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/utils/status.js
@@ -0,0 +1,66 @@
+export const STATUS_CONFIG = {
+ operational: { label: 'Operational', color: 'status-operational', icon: '✓' },
+ degraded: { label: 'Degraded Performance', color: 'status-degraded', icon: '!' },
+ partial: { label: 'Partial Outage', color: 'status-partial', icon: '⚠' },
+ major: { label: 'Major Outage', color: 'status-major', icon: '✕' },
+ maintenance: { label: 'Under Maintenance', color: 'status-maintenance', icon: '🔧' },
+}
+
+export const INCIDENT_STATUS = {
+ investigating: { label: 'Investigating', color: 'text-red-500' },
+ identified: { label: 'Identified', color: 'text-orange-500' },
+ monitoring: { label: 'Monitoring', color: 'text-yellow-500' },
+ resolved: { label: 'Resolved', color: 'text-green-500' },
+}
+
+export const IMPACT_LEVELS = {
+ none: { label: 'None', color: 'text-gray-500' },
+ minor: { label: 'Minor', color: 'text-yellow-500' },
+ major: { label: 'Major', color: 'text-orange-500' },
+ critical: { label: 'Critical', color: 'text-red-500' },
+}
+
+export function getStatusConfig(status) {
+ return STATUS_CONFIG[status] || STATUS_CONFIG.operational
+}
+
+export function getIncidentStatus(status) {
+ return INCIDENT_STATUS[status] || INCIDENT_STATUS.investigating
+}
+
+export function getImpactLevel(impact) {
+ return IMPACT_LEVELS[impact] || IMPACT_LEVELS.none
+}
+
+export function getOverallStatus(services) {
+ if (!services || services.length === 0) return 'operational'
+ if (services.some((s) => s.status === 'major')) return 'major'
+ if (services.some((s) => s.status === 'partial')) return 'partial'
+ if (services.some((s) => s.status === 'degraded')) return 'degraded'
+ if (services.some((s) => s.status === 'maintenance')) return 'maintenance'
+ return 'operational'
+}
+
+export function formatDate(dateString) {
+ if (!dateString) return ''
+ const date = new Date(dateString)
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+}
+
+export function timeAgo(dateString) {
+ if (!dateString) return ''
+ const now = new Date()
+ const date = new Date(dateString)
+ const seconds = Math.floor((now - date) / 1000)
+
+ if (seconds < 60) return 'just now'
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
+ return `${Math.floor(seconds / 86400)}d ago`
+}
diff --git a/frontend/src/views/AdminDashboard.vue b/frontend/src/views/AdminDashboard.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/AdminDashboard.vue
@@ -0,0 +1,383 @@
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useServices } from '@/composables/useServices'
+import { useIncidents } from '@/composables/useIncidents'
+import { useMaintenances } from '@/composables/useMaintenances'
+import { servicesApi, incidentsApi, maintenancesApi } from '@/plugins/api'
+import StatusBadge from '@/components/StatusBadge.vue'
+import { formatDate } from '@/utils/status'
+
+const activeTab = ref('services')
+const tabs = [
+ { key: 'services', label: 'Services' },
+ { key: 'incidents', label: 'Incidents' },
+ { key: 'maintenance', label: 'Maintenance' },
+]
+
+// Services
+const { services, fetchServices } = useServices()
+const showServiceForm = ref(false)
+const editingService = ref(null)
+const serviceForm = ref({ name: '', group: '', description: '', status: 'operational', order: 0 })
+
+const openServiceForm = (service = null) => {
+ if (service) {
+ editingService.value = service.id
+ serviceForm.value = { ...service }
+ } else {
+ editingService.value = null
+ serviceForm.value = { name: '', group: '', description: '', status: 'operational', order: 0 }
+ }
+ showServiceForm.value = true
+}
+
+const saveService = async () => {
+ try {
+ if (editingService.value) {
+ await servicesApi.update(editingService.value, serviceForm.value)
+ } else {
+ await servicesApi.create(serviceForm.value)
+ }
+ showServiceForm.value = false
+ await fetchServices()
+ } catch (err) {
+ alert('Error saving service: ' + err.message)
+ }
+}
+
+const deleteService = async (id) => {
+ if (!confirm('Delete this service?')) return
+ try {
+ await servicesApi.delete(id)
+ await fetchServices()
+ } catch (err) {
+ alert('Error deleting service: ' + err.message)
+ }
+}
+
+// Incidents
+const { incidents, fetchIncidents } = useIncidents()
+const showIncidentForm = ref(false)
+const editingIncident = ref(null)
+const incidentForm = ref({ title: '', status: 'investigating', impact: 'none', message: '' })
+
+const openIncidentForm = (incident = null) => {
+ if (incident) {
+ editingIncident.value = incident.id
+ incidentForm.value = { title: incident.title, status: incident.status, impact: incident.impact, message: '' }
+ } else {
+ editingIncident.value = null
+ incidentForm.value = { title: '', status: 'investigating', impact: 'none', message: '' }
+ }
+ showIncidentForm.value = true
+}
+
+const saveIncident = async () => {
+ try {
+ if (editingIncident.value) {
+ await incidentsApi.update(editingIncident.value, incidentForm.value)
+ } else {
+ await incidentsApi.create(incidentForm.value)
+ }
+ showIncidentForm.value = false
+ await fetchIncidents()
+ } catch (err) {
+ alert('Error saving incident: ' + err.message)
+ }
+}
+
+const resolveIncident = async (id) => {
+ const message = prompt('Resolution message:')
+ if (message === null) return
+ try {
+ await incidentsApi.resolve(id, message || 'Incident resolved')
+ await fetchIncidents()
+ } catch (err) {
+ alert('Error resolving incident: ' + err.message)
+ }
+}
+
+// Maintenance
+const { maintenances, fetchMaintenances } = useMaintenances()
+const showMaintenanceForm = ref(false)
+const editingMaintenance = ref(null)
+const maintenanceForm = ref({ title: '', description: '', scheduled_start: '', scheduled_end: '', status: 'scheduled' })
+
+const openMaintenanceForm = (m = null) => {
+ if (m) {
+ editingMaintenance.value = m.id
+ maintenanceForm.value = {
+ title: m.title,
+ description: m.description || '',
+ scheduled_start: m.scheduled_start ? m.scheduled_start.slice(0, 16) : '',
+ scheduled_end: m.scheduled_end ? m.scheduled_end.slice(0, 16) : '',
+ status: m.status,
+ }
+ } else {
+ editingMaintenance.value = null
+ maintenanceForm.value = { title: '', description: '', scheduled_start: '', scheduled_end: '', status: 'scheduled' }
+ }
+ showMaintenanceForm.value = true
+}
+
+const saveMaintenance = async () => {
+ try {
+ if (editingMaintenance.value) {
+ await maintenancesApi.update(editingMaintenance.value, maintenanceForm.value)
+ } else {
+ await maintenancesApi.create(maintenanceForm.value)
+ }
+ showMaintenanceForm.value = false
+ await fetchMaintenances()
+ } catch (err) {
+ alert('Error saving maintenance: ' + err.message)
+ }
+}
+
+const deleteMaintenance = async (id) => {
+ if (!confirm('Delete this maintenance?')) return
+ try {
+ await maintenancesApi.delete(id)
+ await fetchMaintenances()
+ } catch (err) {
+ alert('Error deleting maintenance: ' + err.message)
+ }
+}
+
+onMounted(() => {
+ fetchServices()
+ fetchIncidents()
+ fetchMaintenances()
+})
+
+const statusOptions = ['operational', 'degraded', 'partial', 'major', 'maintenance']
+const incidentStatusOptions = ['investigating', 'identified', 'monitoring', 'resolved']
+const impactOptions = ['none', 'minor', 'major', 'critical']
+const maintenanceStatusOptions = ['scheduled', 'in_progress', 'completed']
+</script>
+
+<template>
+ <div class="max-w-5xl mx-auto px-4 sm:px-6 py-8">
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">Admin Dashboard</h1>
+
+ <!-- Tabs -->
+ <div class="flex gap-1 mb-6 border-b border-gray-200 dark:border-gray-700">
+ <button
+ v-for="tab in tabs"
+ :key="tab.key"
+ @click="activeTab = tab.key"
+ class="px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px"
+ :class="activeTab === tab.key
+ ? 'border-brand-500 text-brand-600 dark:text-brand-400'
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'"
+ >
+ {{ tab.label }}
+ </button>
+ </div>
+
+ <!-- Services Tab -->
+ <div v-show="activeTab === 'services'">
+ <div class="flex justify-between items-center mb-4">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Services</h2>
+ <button @click="openServiceForm()" class="btn-primary">+ Add Service</button>
+ </div>
+
+ <!-- Service Form -->
+ <div v-if="showServiceForm" class="card p-5 mb-4">
+ <h3 class="text-sm font-semibold mb-3">{{ editingService ? 'Edit' : 'New' }} Service</h3>
+ <form @submit.prevent="saveService" class="grid grid-cols-1 sm:grid-cols-2 gap-3">
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name</label>
+ <input v-model="serviceForm.name" class="input-field" required />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Group</label>
+ <input v-model="serviceForm.group" class="input-field" />
+ </div>
+ <div class="sm:col-span-2">
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Description</label>
+ <input v-model="serviceForm.description" class="input-field" />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Status</label>
+ <select v-model="serviceForm.status" class="input-field">
+ <option v-for="s in statusOptions" :key="s" :value="s">{{ s }}</option>
+ </select>
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Order</label>
+ <input v-model.number="serviceForm.order" type="number" class="input-field" />
+ </div>
+ <div class="sm:col-span-2 flex gap-2">
+ <button type="submit" class="btn-primary">Save</button>
+ <button type="button" @click="showServiceForm = false" class="btn-secondary">Cancel</button>
+ </div>
+ </form>
+ </div>
+
+ <!-- Services List -->
+ <div class="card overflow-hidden">
+ <table class="w-full text-sm">
+ <thead class="bg-gray-50 dark:bg-gray-800/50">
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Name</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Group</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
+ <tr v-for="service in services" :key="service.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ service.name }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ service.group || '—' }}</td>
+ <td class="px-4 py-2.5"><StatusBadge :status="service.status" /></td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openServiceForm(service)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button @click="deleteService(service.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <!-- Incidents Tab -->
+ <div v-show="activeTab === 'incidents'">
+ <div class="flex justify-between items-center mb-4">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Incidents</h2>
+ <button @click="openIncidentForm()" class="btn-primary">+ New Incident</button>
+ </div>
+
+ <!-- Incident Form -->
+ <div v-if="showIncidentForm" class="card p-5 mb-4">
+ <h3 class="text-sm font-semibold mb-3">{{ editingIncident ? 'Update' : 'New' }} Incident</h3>
+ <form @submit.prevent="saveIncident" class="grid grid-cols-1 sm:grid-cols-2 gap-3">
+ <div class="sm:col-span-2">
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Title</label>
+ <input v-model="incidentForm.title" class="input-field" required />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Status</label>
+ <select v-model="incidentForm.status" class="input-field">
+ <option v-for="s in incidentStatusOptions" :key="s" :value="s">{{ s }}</option>
+ </select>
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Impact</label>
+ <select v-model="incidentForm.impact" class="input-field">
+ <option v-for="i in impactOptions" :key="i" :value="i">{{ i }}</option>
+ </select>
+ </div>
+ <div v-if="editingIncident" class="sm:col-span-2">
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Update Message</label>
+ <textarea v-model="incidentForm.message" class="input-field" rows="2" placeholder="Describe the update..."></textarea>
+ </div>
+ <div class="sm:col-span-2 flex gap-2">
+ <button type="submit" class="btn-primary">Save</button>
+ <button type="button" @click="showIncidentForm = false" class="btn-secondary">Cancel</button>
+ </div>
+ </form>
+ </div>
+
+ <!-- Incidents List -->
+ <div class="card overflow-hidden">
+ <table class="w-full text-sm">
+ <thead class="bg-gray-50 dark:bg-gray-800/50">
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Impact</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Started</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
+ <tr v-for="incident in incidents" :key="incident.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ incident.title }}</td>
+ <td class="px-4 py-2.5">
+ <span class="text-xs font-medium" :class="{
+ 'text-red-500': incident.status === 'investigating',
+ 'text-orange-500': incident.status === 'identified',
+ 'text-yellow-500': incident.status === 'monitoring',
+ 'text-green-500': incident.status === 'resolved',
+ }">{{ incident.status }}</span>
+ </td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ incident.impact }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(incident.start_date) }}</td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openIncidentForm(incident)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button v-if="incident.status !== 'resolved'" @click="resolveIncident(incident.id)" class="text-green-600 hover:text-green-700 text-xs font-medium">Resolve</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <!-- Maintenance Tab -->
+ <div v-show="activeTab === 'maintenance'">
+ <div class="flex justify-between items-center mb-4">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Scheduled Maintenance</h2>
+ <button @click="openMaintenanceForm()" class="btn-primary">+ Schedule</button>
+ </div>
+
+ <!-- Maintenance Form -->
+ <div v-if="showMaintenanceForm" class="card p-5 mb-4">
+ <h3 class="text-sm font-semibold mb-3">{{ editingMaintenance ? 'Edit' : 'New' }} Maintenance</h3>
+ <form @submit.prevent="saveMaintenance" class="grid grid-cols-1 sm:grid-cols-2 gap-3">
+ <div class="sm:col-span-2">
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Title</label>
+ <input v-model="maintenanceForm.title" class="input-field" required />
+ </div>
+ <div class="sm:col-span-2">
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Description</label>
+ <textarea v-model="maintenanceForm.description" class="input-field" rows="2"></textarea>
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Start</label>
+ <input v-model="maintenanceForm.scheduled_start" type="datetime-local" class="input-field" required />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">End</label>
+ <input v-model="maintenanceForm.scheduled_end" type="datetime-local" class="input-field" required />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Status</label>
+ <select v-model="maintenanceForm.status" class="input-field">
+ <option v-for="s in maintenanceStatusOptions" :key="s" :value="s">{{ s }}</option>
+ </select>
+ </div>
+ <div class="sm:col-span-2 flex gap-2">
+ <button type="submit" class="btn-primary">Save</button>
+ <button type="button" @click="showMaintenanceForm = false" class="btn-secondary">Cancel</button>
+ </div>
+ </form>
+ </div>
+
+ <!-- Maintenance List -->
+ <div class="card overflow-hidden">
+ <table class="w-full text-sm">
+ <thead class="bg-gray-50 dark:bg-gray-800/50">
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Scheduled</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
+ <tr v-for="m in maintenances" :key="m.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ m.title }}</td>
+ <td class="px-4 py-2.5 text-xs font-medium text-gray-500">{{ m.status }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(m.scheduled_start) }}</td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openMaintenanceForm(m)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button @click="deleteMaintenance(m.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/frontend/src/views/AdminLogin.vue b/frontend/src/views/AdminLogin.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/AdminLogin.vue
@@ -0,0 +1,53 @@
+<script setup>
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useAuth } from '@/composables/useAuth'
+
+const router = useRouter()
+const { login } = useAuth()
+
+const token = ref('')
+const error = ref('')
+
+const handleLogin = () => {
+ if (!token.value.trim()) {
+ error.value = 'Please enter a valid token'
+ return
+ }
+ login(token.value.trim())
+ router.push('/admin')
+}
+</script>
+
+<template>
+ <div class="min-h-[70vh] flex items-center justify-center px-4">
+ <div class="card p-8 w-full max-w-sm">
+ <h1 class="text-xl font-bold text-gray-900 dark:text-gray-100 text-center mb-6">Admin Login</h1>
+
+ <form @submit.prevent="handleLogin" class="space-y-4">
+ <div>
+ <label for="token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+ API Token
+ </label>
+ <input
+ id="token"
+ v-model="token"
+ type="password"
+ placeholder="Paste your JWT token"
+ class="input-field"
+ />
+ </div>
+
+ <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
+
+ <button type="submit" class="btn-primary w-full justify-center">
+ Sign In
+ </button>
+ </form>
+
+ <p class="mt-4 text-xs text-gray-500 dark:text-gray-400 text-center">
+ Generate a token using the admin CLI or environment configuration.
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
deleted file mode 100644
--- a/frontend/src/views/HomeView.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-<script setup>
-// Sample
-</script>
-
-<template>
- <main>
-<!-- Sample -->
- </main>
-</template>
diff --git a/frontend/src/views/StatusPage.vue b/frontend/src/views/StatusPage.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/StatusPage.vue
@@ -0,0 +1,93 @@
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { useServices } from '@/composables/useServices'
+import { useIncidents } from '@/composables/useIncidents'
+import { useMaintenances } from '@/composables/useMaintenances'
+import OverallStatus from '@/components/OverallStatus.vue'
+import ServiceGroup from '@/components/ServiceGroup.vue'
+import IncidentTimeline from '@/components/IncidentTimeline.vue'
+import MaintenanceCard from '@/components/MaintenanceCard.vue'
+import { incidentsApi } from '@/plugins/api'
+
+const { services, loading: servicesLoading, fetchServices, groupedServices } = useServices()
+const { incidents, loading: incidentsLoading, fetchIncidents, activeIncidents, resolvedIncidents } = useIncidents()
+const { maintenances, loading: maintenancesLoading, fetchMaintenances, upcoming, inProgress } = useMaintenances()
+
+const detailedIncidents = ref([])
+
+onMounted(async () => {
+ await Promise.all([fetchServices(), fetchIncidents(), fetchMaintenances()])
+
+ // Fetch detailed incident data with updates for active incidents
+ const active = activeIncidents()
+ const detailed = await Promise.all(
+ active.map((incident) => incidentsApi.getById(incident.id).catch(() => incident))
+ )
+ detailedIncidents.value = detailed
+})
+
+const groups = computed(() => groupedServices())
+const scheduledMaintenances = computed(() => upcoming())
+const activeMaintenances = computed(() => inProgress())
+const recentResolved = computed(() => resolvedIncidents().slice(0, 5))
+
+const isLoading = computed(() => servicesLoading.value || incidentsLoading.value || maintenancesLoading.value)
+</script>
+
+<template>
+ <div class="max-w-3xl mx-auto px-4 sm:px-6 py-8 space-y-6">
+ <!-- Loading -->
+ <div v-if="isLoading" class="flex justify-center py-20">
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-500"></div>
+ </div>
+
+ <template v-else>
+ <!-- Overall Status Banner -->
+ <OverallStatus :services="services" />
+
+ <!-- Active Maintenances -->
+ <section v-if="activeMaintenances.length">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Active Maintenance</h2>
+ <div class="space-y-3">
+ <MaintenanceCard v-for="m in activeMaintenances" :key="m.id" :maintenance="m" />
+ </div>
+ </section>
+
+ <!-- Service Groups -->
+ <section>
+ <div class="space-y-4">
+ <ServiceGroup
+ v-for="(groupServices, name) in groups"
+ :key="name"
+ :group-name="name"
+ :services="groupServices"
+ />
+ </div>
+ </section>
+
+ <!-- Active Incidents -->
+ <section v-if="detailedIncidents.length">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Active Incidents</h2>
+ <div class="space-y-3">
+ <IncidentTimeline v-for="i in detailedIncidents" :key="i.id" :incident="i" />
+ </div>
+ </section>
+
+ <!-- Scheduled Maintenance -->
+ <section v-if="scheduledMaintenances.length">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Scheduled Maintenance</h2>
+ <div class="space-y-3">
+ <MaintenanceCard v-for="m in scheduledMaintenances" :key="m.id" :maintenance="m" />
+ </div>
+ </section>
+
+ <!-- Recent Resolved Incidents -->
+ <section v-if="recentResolved.length">
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Past Incidents</h2>
+ <div class="space-y-3">
+ <IncidentTimeline v-for="i in recentResolved" :key="i.id" :incident="i" />
+ </div>
+ </section>
+ </template>
+ </div>
+</template>
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,35 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './index.html',
+ './src/**/*.{vue,js,ts,jsx,tsx}',
+ ],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ colors: {
+ brand: {
+ 50: '#eef7ff',
+ 100: '#d9edff',
+ 200: '#bce0ff',
+ 300: '#8eccff',
+ 400: '#59aeff',
+ 500: '#338dff',
+ 600: '#1a6df5',
+ 700: '#1357e1',
+ 800: '#1646b6',
+ 900: '#183e8f',
+ 950: '#142757',
+ },
+ status: {
+ operational: '#10b981',
+ degraded: '#f59e0b',
+ partial: '#f97316',
+ major: '#ef4444',
+ maintenance: '#3b82f6',
+ },
+ },
+ },
+ },
+ plugins: [],
+}

File Metadata

Mime Type
text/plain
Expires
Tue, Feb 17, 03:17 (19 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3454226
Default Alt Text
D3964.diff (115 KB)

Event Timeline