diff --git a/web/.env.example b/web/.env.example
index 27994cb..d268230 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -11,3 +11,7 @@
# Default: https://pipely.onixbyte.com/
# Development: http://localhost:8080
VITE_API_BASE_URL=https://pipely.onixbyte.com/
+
+# Redux Persistent storage location
+# Options: `local` (default), `session`
+VITE_REDUX_STORAGE=local
diff --git a/web/package.json b/web/package.json
index 67b7c87..a72281c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -14,12 +14,16 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.2.0",
+ "@reduxjs/toolkit": "^2.12.0",
"@tailwindcss/vite": "^4.3.2",
"axios": "^1.18.1",
"dayjs": "^1.11.21",
"react": "^19.2.7",
"react-dom": "^19.2.7",
+ "react-redux": "^9.3.0",
+ "react-router": "^8.1.0",
"react-router-dom": "^7.18.1",
+ "redux-persist": "^6.0.0",
"tailwindcss": "^4.3.2"
},
"devDependencies": {
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index de47a1a..7b3e3dd 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@mui/material':
specifier: ^9.2.0
version: 9.2.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@reduxjs/toolkit':
+ specifier: ^2.12.0
+ version: 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)
'@tailwindcss/vite':
specifier: ^4.3.2
version: 4.3.2(vite@8.1.3(@types/node@24.13.2)(jiti@2.7.0))
@@ -35,9 +38,18 @@ importers:
react-dom:
specifier: ^19.2.7
version: 19.2.7(react@19.2.7)
+ react-redux:
+ specifier: ^9.3.0
+ version: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
+ react-router:
+ specifier: ^8.1.0
+ version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-router-dom:
specifier: ^7.18.1
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ redux-persist:
+ specifier: ^6.0.0
+ version: 6.0.0(react@19.2.7)(redux@5.0.1)
tailwindcss:
specifier: ^4.3.2
version: 4.3.2
@@ -499,6 +511,17 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+ '@reduxjs/toolkit@2.12.0':
+ resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@rolldown/binding-android-arm64@1.1.4':
resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -604,6 +627,12 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@tailwindcss/node@4.3.2':
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
@@ -735,6 +764,9 @@ packages:
'@types/react@19.2.17':
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
'@types/validate-npm-package-name@4.0.2':
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
@@ -985,6 +1017,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-es@3.1.1:
+ resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
+
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
@@ -1445,6 +1480,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ immer@11.1.11:
+ resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -2000,6 +2038,18 @@ packages:
react-is@19.2.7:
resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==}
+ react-redux@9.3.0:
+ resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react-router-dom@7.18.1:
resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==}
engines: {node: '>=20.0.0'}
@@ -2017,6 +2067,16 @@ packages:
react-dom:
optional: true
+ react-router@8.1.0:
+ resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ react: '>=19.2.7'
+ react-dom: '>=19.2.7'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -2031,10 +2091,30 @@ packages:
resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==}
engines: {node: '>= 4'}
+ redux-persist@6.0.0:
+ resolution: {integrity: sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==}
+ peerDependencies:
+ react: '>=16'
+ redux: '>4.0.0'
+ peerDependenciesMeta:
+ react:
+ optional: true
+
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ reselect@5.2.0:
+ resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2286,6 +2366,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2917,6 +3002,18 @@ snapshots:
'@popperjs/core@2.11.8': {}
+ '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.11
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.2.0
+ optionalDependencies:
+ react: 19.2.7
+ react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
+
'@rolldown/binding-android-arm64@1.1.4':
optional: true
@@ -2972,6 +3069,10 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
+ '@standard-schema/spec@1.1.0': {}
+
+ '@standard-schema/utils@0.3.0': {}
+
'@tailwindcss/node@4.3.2':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -3077,6 +3178,8 @@ snapshots:
dependencies:
csstype: 3.2.3
+ '@types/use-sync-external-store@0.0.6': {}
+
'@types/validate-npm-package-name@4.0.2': {}
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
@@ -3343,6 +3446,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-es@3.1.1: {}
+
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
@@ -3822,6 +3927,8 @@ snapshots:
ignore@7.0.5: {}
+ immer@11.1.11: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -4258,6 +4365,15 @@ snapshots:
react-is@19.2.7: {}
+ react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ redux: 5.0.1
+
react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
react: 19.2.7
@@ -4272,6 +4388,13 @@ snapshots:
optionalDependencies:
react-dom: 19.2.7(react@19.2.7)
+ react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ cookie-es: 3.1.1
+ react: 19.2.7
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+
react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@babel/runtime': 7.29.7
@@ -4291,8 +4414,22 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
+ redux-persist@6.0.0(react@19.2.7)(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+ optionalDependencies:
+ react: 19.2.7
+
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
require-from-string@2.0.2: {}
+ reselect@5.2.0: {}
+
resolve-from@4.0.0: {}
resolve@1.22.12:
@@ -4584,6 +4721,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-sync-external-store@1.6.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
util-deprecate@1.0.2: {}
validate-npm-package-name@7.0.2: {}
diff --git a/web/src/App.tsx b/web/src/App.tsx
deleted file mode 100644
index f82b494..0000000
--- a/web/src/App.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"
-import Layout from "@/components/Layout"
-import Login from "@/pages/Login"
-import Dashboard from "@/pages/Dashboard"
-import Artefacts from "@/pages/Artefacts"
-import Deployments from "@/pages/Deployments"
-import Users from "@/pages/Users"
-
-function RequireAuth({ children }: { children: React.ReactNode }) {
- const token = localStorage.getItem("token")
- if (!token) return