Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F24371652
D3964.id10273.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
115 KB
Referenced Files
None
Subscribers
None
D3964.id10273.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Tue, Feb 17, 08:17 (21 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3454226
Default Alt Text
D3964.id10273.diff (115 KB)
Attached To
Mode
D3964: Phase 3: Frontend redesign with Tailwind CSS and Vue 3 component architecture
Attached
Detach File
Event Timeline
Log In to Comment