Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
# Platform Owner Control Panel
Primary Web/Admin shell for Secure Access Fabric platform-owner operations.
This UI is for product owner / platform owner scope. It is not the future
organization admin panel and must not expose full internal mesh topology to
organization users.
Architecture boundary:
- WEB is HTTP/HTTPS ingress and presentation.
- Cluster configuration belongs to Control Plane APIs.
- PostgreSQL remains the source of truth.
- Dynamic admin pages must be safe, scoped, schema-driven projections.
- Secrets, internal topology, peer caches, route caches, and raw credentials
must not be embedded in pages.
See:
- `docs/architecture/WEB_INGRESS_AND_ADMIN_UI_MODEL.md`
- `docs/architecture/CLUSTER_NODE_ADMIN_FOUNDATION.md`
## Current Scope
Implemented platform-owner sections:
- command overview
- installation status and first-owner bootstrap screen backed by signed
Product Root activation manifests
- multi-cluster health posture
- cluster creation
- cluster authority/mutation guard with cluster key fingerprint visibility
- node inventory
- node membership disable and identity revoke boundaries
- join token creation with signed authority envelope visibility
- join request approve/reject with signed approval envelope visibility
- role assignment
- workload desired-state setting
- workload status readout
- mesh preparation observations
- node-scoped synthetic mesh config visibility, including endpoint candidate,
peer directory, recovery seed, rendezvous lease counts, and C17X advisory
scoring boundary, plus cluster authority signature status
- C17Z-C17Z18 production `fabric.control` forwarding/config/runtime,
rendezvous/relay control-plane boundary, rendezvous lease telemetry, and
lease refresh plus relay replacement policy, route/path decision visibility,
route generation tracker status visibility, and synthetic route-health
effective-path visibility
- QoS foundation readout
- VPN/IP tunnel control-plane desired state
- stale VPN lease cleanup action
- organization-safe summary preview
- cluster audit
Not implemented:
- organization admin UI as a separate tenant product surface
- production mesh runtime
- VPN/IP tunnel runtime
- TUN/TAP, route, DNS, firewall, or QoS execution
- RDP runtime changes
- arbitrary plugin JavaScript or dynamic executable pages
- direct database mutation from WEB
## Build
```powershell
cd web-admin
npm install
npm run build
```
## Run Locally
```powershell
cd web-admin
npm run dev -- --port 5173
```
Open:
```text
http://127.0.0.1:5173
```
The admin console should run on a dedicated admin host/port. It is not intended
to be the public product landing page on generic `80/443` web ingress.
Default backend API inside the panel:
```text
/api/v1
```
The local Vite dev server proxies `/api` to the remote test backend
`http://192.168.200.61:8080` by default, avoiding browser CORS issues while
keeping the Control Plane API unchanged. Override the proxy target when needed:
```powershell
$env:RAP_ADMIN_API_PROXY = "http://192.168.200.61:8080"
npm run dev -- --port 5173
```
The start screen shows installation authority status. If the backend reports an
unbootstrapped installation, it switches to the first-owner form and accepts a
signed activation manifest plus signature. Otherwise it shows only login and
password fields; it does not expose API URLs or language/settings to
unauthenticated users.
After authentication the panel verifies platform-owner/platform-admin access
through Control Plane APIs before opening the console. Users without product-owner
scope must not see this panel. Organization admins and organization users require
separate scoped panels.
Language selection is available only after login in the profile area. It is stored
as a user-scoped browser preference for this MVP. Backend user-profile persistence
for language/locale is a later Control Plane profile setting.
The panel shows real Control Plane data only. If cluster counts are zero, the
cluster has no approved node-agent nodes, roles, workloads, VPN records, or mesh
observations yet.
## Safety Rules
- The console is platform-owner/platform-admin only.
- Capabilities are technical facts only.
- Roles are explicit policy assignments.
- Organization topology is intentionally not shown.
- VPN records are desired state only; no runtime tunnel is started here.
- Synthetic mesh config visibility is platform-owner scope only. Endpoint
candidates and health-aware scoring remain advisory and do not start
service traffic forwarding.
- Rendezvous leases are control-plane-only route metadata for relay health
paths; they do not enable payload forwarding.
- Relay replacement policy summaries are platform-owner control-plane
visibility only; they do not publish organization topology or start payload
forwarding.
- Route/path decision summaries are also platform-owner control-plane
visibility only in synthetic config and recent heartbeat tables; effective
hops are planning metadata, not payload routing.
- Route-health rows in Fabric links show synthetic control-plane probes,
selected relay, expected/observed hops, and drift status. They are not
service payload routes.
- C17Z production forwarding is limited to route-bound `fabric.control` direct
next-hop delivery behind an explicit node-agent gate. RDP, VPN, file, video,
and service workload payloads remain out of scope.
- High-risk actions should require backend authorization, audit, and future
step-up authentication.
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Панель Secure Access Fabric</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+962
View File
@@ -0,0 +1,962 @@
{
"name": "@rap/web-admin",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@rap/web-admin",
"version": "0.1.0",
"dependencies": {
"@vitejs/plugin-react": "latest",
"react": "latest",
"react-dom": "latest",
"typescript": "latest",
"vite": "latest"
},
"devDependencies": {
"@types/react": "latest",
"@types/react-dom": "latest"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@oxc-project/types": {
"version": "0.127.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.7"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"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",
"optional": true
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"tinyglobby": "^0.2.16"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@rap/web-admin",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "tsc -b && vite build",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"@vitejs/plugin-react": "latest",
"typescript": "latest",
"vite": "latest",
"react": "latest",
"react-dom": "latest"
},
"devDependencies": {
"@types/react": "latest",
"@types/react-dom": "latest"
}
}
File diff suppressed because it is too large Load Diff
+609
View File
@@ -0,0 +1,609 @@
import type {
AuditEvent,
AuthResult,
BootstrapOwnerResult,
Cluster,
ClusterNodeGroup,
ClusterAdminSummary,
ClusterAuthorityState,
ClusterNode,
CreatedJoinToken,
FabricEntryPoint,
FabricEntryPointNode,
FabricEgressPool,
FabricEgressPoolNode,
FabricTestingFlag,
InstallationStatus,
JoinRequest,
MeshLink,
NodeHeartbeat,
NodeSyntheticMeshConfig,
NodeTelemetryObservation,
NodeWorkloadDesiredState,
OrganizationAdminSummary,
QoSPolicy,
RoleAssignment,
VPNConnection,
VPNConnectionLease,
WorkloadStatus,
} from "../types";
export type AdminClientConfig = {
baseUrl: string;
actorUserId: string;
};
type ApiErrorPayload = {
error?: {
code?: string;
message_key?: string;
fallback_message?: string;
trace_id?: string;
};
};
export type CreateClusterPayload = {
slug: string;
name: string;
region?: string | null;
};
export type UpdateClusterPayload = {
name: string;
status: string;
region?: string | null;
metadata?: Record<string, unknown>;
};
export type CreateVPNConnectionPayload = {
organizationId: string;
name: string;
protocolFamily: string;
targetEndpoint: Record<string, unknown>;
credentialRef?: string | null;
desiredState: string;
allowedNodePolicy: Record<string, unknown>;
routingUsage: unknown[];
routePolicy: Record<string, unknown>;
qosPolicy: Record<string, unknown>;
placementPolicy: Record<string, unknown>;
};
export type CreateFabricEntryPointPayload = {
name: string;
endpointType: string;
publicEndpoint?: string | null;
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type CreateFabricEgressPoolPayload = {
name: string;
description?: string | null;
routeScope?: Record<string, unknown>;
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type SetFabricEndpointNodePayload = {
status?: string;
priority?: number;
metadata?: Record<string, unknown>;
};
export class AdminApiClient {
private readonly baseUrl: string;
private readonly actorUserId: string;
constructor(config: AdminClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/$/, "");
this.actorUserId = config.actorUserId.trim();
}
async login(input: { email: string; password: string; deviceLabel: string; trustDevice: boolean }): Promise<AuthResult> {
return this.post<AuthResult>("/auth/login", {
email: input.email,
password: input.password,
device_fingerprint: browserDeviceFingerprint(),
device_label: input.deviceLabel,
trust_device: input.trustDevice,
});
}
async getInstallationStatus(): Promise<InstallationStatus> {
const payload = await this.request<{ installation: InstallationStatus }>("/installation/status", {
method: "GET",
});
return payload.installation;
}
async bootstrapOwner(input: {
email: string;
password: string;
activationPayload?: unknown;
activationSignature?: string;
}): Promise<BootstrapOwnerResult> {
return this.post<BootstrapOwnerResult>("/installation/bootstrap-owner", {
email: input.email,
password: input.password,
activation_payload: input.activationPayload,
activation_signature: input.activationSignature || "",
});
}
async revokeAuthSession(input: { userId: string; authSessionId: string; reason: string }): Promise<void> {
await this.post("/auth/sessions/revoke", {
user_id: input.userId,
auth_session_id: input.authSessionId,
reason: input.reason,
});
}
async listClusters(): Promise<Cluster[]> {
const payload = await this.get<{ clusters: Cluster[] }>("/clusters");
return payload.clusters ?? [];
}
async createCluster(input: CreateClusterPayload): Promise<Cluster> {
const payload = await this.post<{ cluster: Cluster }>("/clusters/", {
actor_user_id: this.actorUserId,
slug: input.slug,
name: input.name,
region: input.region || null,
metadata: {},
});
return payload.cluster;
}
async updateCluster(clusterId: string, input: UpdateClusterPayload): Promise<Cluster> {
const payload = await this.put<{ cluster: Cluster }>(`/clusters/${clusterId}`, {
actor_user_id: this.actorUserId,
name: input.name,
status: input.status,
region: input.region || null,
metadata: input.metadata || {},
});
return payload.cluster;
}
async listClusterSummaries(): Promise<ClusterAdminSummary[]> {
const payload = await this.get<{ cluster_summaries: ClusterAdminSummary[] }>("/cluster-admin-summaries");
return payload.cluster_summaries ?? [];
}
async getClusterAuthority(clusterId: string): Promise<ClusterAuthorityState> {
const payload = await this.get<{ authority_state: ClusterAuthorityState }>(`/clusters/${clusterId}/authority`);
return payload.authority_state;
}
async updateClusterAuthority(
clusterId: string,
input: { authorityState: string; mutationMode: string; notes?: string | null },
): Promise<ClusterAuthorityState> {
const payload = await this.put<{ authority_state: ClusterAuthorityState }>(`/clusters/${clusterId}/authority`, {
actor_user_id: this.actorUserId,
authority_state: input.authorityState,
mutation_mode: input.mutationMode,
notes: input.notes || null,
});
return payload.authority_state;
}
async listNodes(clusterId: string): Promise<ClusterNode[]> {
const payload = await this.get<{ nodes: ClusterNode[] }>(`/clusters/${clusterId}/nodes`);
return payload.nodes ?? [];
}
async listNodeGroups(clusterId: string): Promise<ClusterNodeGroup[]> {
const payload = await this.get<{ node_groups: ClusterNodeGroup[] }>(`/clusters/${clusterId}/node-groups`);
return payload.node_groups ?? [];
}
async createNodeGroup(
clusterId: string,
input: { name: string; parentGroupId?: string | null; description?: string | null; sortOrder?: number },
): Promise<ClusterNodeGroup> {
const payload = await this.post<{ node_group: ClusterNodeGroup }>(`/clusters/${clusterId}/node-groups`, {
actor_user_id: this.actorUserId,
parent_group_id: input.parentGroupId || null,
name: input.name,
description: input.description || null,
sort_order: input.sortOrder || 0,
metadata: {},
});
return payload.node_group;
}
async assignNodeGroup(clusterId: string, nodeId: string, groupId?: string | null): Promise<ClusterNode> {
const payload = await this.put<{ node: ClusterNode }>(`/clusters/${clusterId}/nodes/${nodeId}/group`, {
actor_user_id: this.actorUserId,
group_id: groupId || null,
});
return payload.node;
}
async disableMembership(clusterId: string, nodeId: string, reason: string): Promise<void> {
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/membership/disable`, {
actor_user_id: this.actorUserId,
reason,
});
}
async attachExistingNode(clusterId: string, nodeId: string, roles: string[]): Promise<ClusterNode> {
const payload = await this.post<{ node: ClusterNode }>(`/clusters/${clusterId}/nodes/${nodeId}/membership/attach`, {
actor_user_id: this.actorUserId,
roles,
});
return payload.node;
}
async revokeNodeIdentity(clusterId: string, nodeId: string, reason: string): Promise<void> {
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/identity/revoke`, {
actor_user_id: this.actorUserId,
reason,
});
}
async listJoinRequests(clusterId: string): Promise<JoinRequest[]> {
const payload = await this.get<{ join_requests: JoinRequest[] }>(`/clusters/${clusterId}/join-requests`);
return payload.join_requests ?? [];
}
async createJoinToken(clusterId: string, input: { maxUses: number; ttlHours: number; scope: Record<string, unknown> }): Promise<CreatedJoinToken> {
const expiresAt = new Date(Date.now() + Math.max(input.ttlHours, 1) * 60 * 60 * 1000).toISOString();
const payload = await this.post<{ join_token: CreatedJoinToken }>(`/clusters/${clusterId}/join-tokens`, {
actor_user_id: this.actorUserId,
scope: input.scope,
expires_at: expiresAt,
max_uses: Math.max(input.maxUses, 1),
});
return payload.join_token;
}
async approveJoinRequest(clusterId: string, requestId: string): Promise<void> {
await this.post(`/clusters/${clusterId}/join-requests/${requestId}/approve`, {
actor_user_id: this.actorUserId,
ownership_type: "platform_managed",
});
}
async rejectJoinRequest(clusterId: string, requestId: string, reason: string): Promise<void> {
await this.post(`/clusters/${clusterId}/join-requests/${requestId}/reject`, {
actor_user_id: this.actorUserId,
reason,
});
}
async listNodeRoles(clusterId: string, nodeId: string): Promise<RoleAssignment[]> {
const payload = await this.get<{ role_assignments: RoleAssignment[] }>(
`/clusters/${clusterId}/nodes/${nodeId}/roles`,
);
return payload.role_assignments ?? [];
}
async assignRole(clusterId: string, nodeId: string, role: string, organizationId?: string): Promise<void> {
await this.setRoleStatus(clusterId, nodeId, role, "active", organizationId);
}
async setRoleStatus(clusterId: string, nodeId: string, role: string, status: "active" | "disabled" | "revoked", organizationId?: string): Promise<void> {
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/roles`, {
actor_user_id: this.actorUserId,
organization_id: organizationId || null,
role,
status,
policy: {},
});
}
async listWorkloadStatuses(clusterId: string, nodeId: string): Promise<WorkloadStatus[]> {
const payload = await this.get<{ workload_statuses: WorkloadStatus[] }>(
`/clusters/${clusterId}/nodes/${nodeId}/workloads/status`,
);
return payload.workload_statuses ?? [];
}
async listDesiredWorkloads(clusterId: string, nodeId: string): Promise<NodeWorkloadDesiredState[]> {
const payload = await this.get<{ desired_workloads: NodeWorkloadDesiredState[] }>(
`/clusters/${clusterId}/nodes/${nodeId}/workloads/desired`,
);
return payload.desired_workloads ?? [];
}
async listNodeHeartbeats(clusterId: string, nodeId: string, limit = 100): Promise<NodeHeartbeat[]> {
const payload = await this.get<{ heartbeats: NodeHeartbeat[] }>(
`/clusters/${clusterId}/nodes/${nodeId}/heartbeats?limit=${limit}`,
);
return payload.heartbeats ?? [];
}
async listNodeTelemetry(clusterId: string, nodeId: string, limit = 120): Promise<NodeTelemetryObservation[]> {
const payload = await this.get<{ telemetry: NodeTelemetryObservation[] }>(
`/clusters/${clusterId}/nodes/${nodeId}/telemetry?limit=${limit}`,
);
return payload.telemetry ?? [];
}
async listFabricTestingFlags(): Promise<FabricTestingFlag[]> {
const payload = await this.get<{ testing_flags: FabricTestingFlag[] }>("/fabric/testing-flags");
return payload.testing_flags ?? [];
}
async updateFabricTestingFlag(input: {
scopeType: string;
scopeId?: string | null;
clusterId?: string | null;
enabled: boolean;
telemetryEnabled: boolean;
syntheticLinksEnabled: boolean;
historyRetentionHours: number;
metadata?: Record<string, unknown>;
}): Promise<FabricTestingFlag> {
const payload = await this.put<{ testing_flag: FabricTestingFlag }>("/fabric/testing-flags", {
actor_user_id: this.actorUserId,
scope_type: input.scopeType,
scope_id: input.scopeId || null,
cluster_id: input.clusterId || null,
enabled: input.enabled,
telemetry_enabled: input.telemetryEnabled,
synthetic_links_enabled: input.syntheticLinksEnabled,
history_retention_hours: input.historyRetentionHours,
metadata: input.metadata || {},
});
return payload.testing_flag;
}
async setDesiredWorkload(
clusterId: string,
nodeId: string,
serviceType: string,
input: { desiredState: string; runtimeMode: string; version?: string; config: Record<string, unknown>; environment: Record<string, unknown> },
): Promise<void> {
await this.put(`/clusters/${clusterId}/nodes/${nodeId}/workloads/${serviceType}/desired`, {
actor_user_id: this.actorUserId,
desired_state: input.desiredState,
version: input.version || null,
runtime_mode: input.runtimeMode,
artifact_ref: null,
config: input.config,
environment: input.environment,
});
}
async listMeshLinks(clusterId: string): Promise<MeshLink[]> {
const payload = await this.get<{ mesh_links: MeshLink[] }>(`/clusters/${clusterId}/mesh/links`);
return payload.mesh_links ?? [];
}
async getNodeSyntheticMeshConfig(clusterId: string, nodeId: string): Promise<NodeSyntheticMeshConfig> {
const payload = await this.get<{ synthetic_mesh_config: NodeSyntheticMeshConfig }>(
`/clusters/${clusterId}/nodes/${nodeId}/mesh/synthetic-config`,
);
return payload.synthetic_mesh_config;
}
async listQoSPolicies(clusterId: string): Promise<QoSPolicy[]> {
const payload = await this.get<{ qos_policies: QoSPolicy[] }>(`/clusters/${clusterId}/mesh/qos-policies`);
return payload.qos_policies ?? [];
}
async listFabricEntryPoints(clusterId: string): Promise<FabricEntryPoint[]> {
const payload = await this.get<{ entry_points: FabricEntryPoint[] }>(`/clusters/${clusterId}/fabric/entry-points`);
return payload.entry_points ?? [];
}
async createFabricEntryPoint(clusterId: string, input: CreateFabricEntryPointPayload): Promise<FabricEntryPoint> {
const payload = await this.post<{ entry_point: FabricEntryPoint }>(`/clusters/${clusterId}/fabric/entry-points`, {
actor_user_id: this.actorUserId,
name: input.name,
status: "active",
endpoint_type: input.endpointType || "client_access",
public_endpoint: input.publicEndpoint || null,
policy: input.policy || {},
metadata: input.metadata || {},
});
return payload.entry_point;
}
async listFabricEntryPointNodes(clusterId: string, entryPointId: string): Promise<FabricEntryPointNode[]> {
const payload = await this.get<{ entry_point_nodes: FabricEntryPointNode[] }>(
`/clusters/${clusterId}/fabric/entry-points/${entryPointId}/nodes`,
);
return payload.entry_point_nodes ?? [];
}
async setFabricEntryPointNode(
clusterId: string,
entryPointId: string,
nodeId: string,
input: SetFabricEndpointNodePayload = {},
): Promise<FabricEntryPointNode> {
const payload = await this.put<{ entry_point_node: FabricEntryPointNode }>(
`/clusters/${clusterId}/fabric/entry-points/${entryPointId}/nodes/${nodeId}`,
{
actor_user_id: this.actorUserId,
status: input.status || "active",
priority: input.priority || 100,
metadata: input.metadata || {},
},
);
return payload.entry_point_node;
}
async listFabricEgressPools(clusterId: string): Promise<FabricEgressPool[]> {
const payload = await this.get<{ egress_pools: FabricEgressPool[] }>(`/clusters/${clusterId}/fabric/egress-pools`);
return payload.egress_pools ?? [];
}
async createFabricEgressPool(clusterId: string, input: CreateFabricEgressPoolPayload): Promise<FabricEgressPool> {
const payload = await this.post<{ egress_pool: FabricEgressPool }>(`/clusters/${clusterId}/fabric/egress-pools`, {
actor_user_id: this.actorUserId,
name: input.name,
status: "active",
description: input.description || null,
route_scope: input.routeScope || {},
policy: input.policy || {},
metadata: input.metadata || {},
});
return payload.egress_pool;
}
async listFabricEgressPoolNodes(clusterId: string, egressPoolId: string): Promise<FabricEgressPoolNode[]> {
const payload = await this.get<{ egress_pool_nodes: FabricEgressPoolNode[] }>(
`/clusters/${clusterId}/fabric/egress-pools/${egressPoolId}/nodes`,
);
return payload.egress_pool_nodes ?? [];
}
async setFabricEgressPoolNode(
clusterId: string,
egressPoolId: string,
nodeId: string,
input: SetFabricEndpointNodePayload = {},
): Promise<FabricEgressPoolNode> {
const payload = await this.put<{ egress_pool_node: FabricEgressPoolNode }>(
`/clusters/${clusterId}/fabric/egress-pools/${egressPoolId}/nodes/${nodeId}`,
{
actor_user_id: this.actorUserId,
status: input.status || "active",
priority: input.priority || 100,
metadata: input.metadata || {},
},
);
return payload.egress_pool_node;
}
async listVPNConnections(clusterId: string): Promise<VPNConnection[]> {
const payload = await this.get<{ vpn_connections: VPNConnection[] }>(`/clusters/${clusterId}/vpn-connections`);
return payload.vpn_connections ?? [];
}
async createVPNConnection(clusterId: string, input: CreateVPNConnectionPayload): Promise<VPNConnection> {
const payload = await this.post<{ vpn_connection: VPNConnection }>(`/clusters/${clusterId}/vpn-connections`, {
actor_user_id: this.actorUserId,
organization_id: input.organizationId,
name: input.name,
target_endpoint: input.targetEndpoint,
protocol_family: input.protocolFamily,
credential_ref: input.credentialRef || null,
mode: "single_active",
desired_state: input.desiredState,
allowed_node_policy: input.allowedNodePolicy,
routing_usage: input.routingUsage,
route_policy: input.routePolicy,
qos_policy: input.qosPolicy,
placement_policy: input.placementPolicy,
metadata: {},
});
return payload.vpn_connection;
}
async updateVPNConnectionDesiredState(clusterId: string, vpnConnectionId: string, desiredState: string): Promise<VPNConnection> {
const payload = await this.put<{ vpn_connection: VPNConnection }>(
`/clusters/${clusterId}/vpn-connections/${vpnConnectionId}/desired-state`,
{
actor_user_id: this.actorUserId,
desired_state: desiredState,
},
);
return payload.vpn_connection;
}
async getActiveVPNLease(clusterId: string, vpnConnectionId: string): Promise<VPNConnectionLease | null> {
try {
const payload = await this.get<{ lease: VPNConnectionLease }>(
`/clusters/${clusterId}/vpn-connections/${vpnConnectionId}/leases/active`,
);
return payload.lease;
} catch {
return null;
}
}
async expireStaleVPNLeases(clusterId: string): Promise<VPNConnectionLease[]> {
const payload = await this.post<{ expired_leases: VPNConnectionLease[] }>(
`/clusters/${clusterId}/vpn-connection-leases/expire-stale`,
{
actor_user_id: this.actorUserId,
},
);
return payload.expired_leases ?? [];
}
async listAudit(clusterId: string): Promise<AuditEvent[]> {
const payload = await this.get<{ audit_events: AuditEvent[] }>(`/clusters/${clusterId}/audit?limit=100`);
return payload.audit_events ?? [];
}
async getOrganizationAdminSummary(organizationId: string): Promise<OrganizationAdminSummary> {
const payload = await this.get<{ admin_summary: OrganizationAdminSummary }>(
`/organizations/${organizationId}/admin-summary`,
);
return payload.admin_summary;
}
private async get<T>(path: string): Promise<T> {
const separator = path.includes("?") ? "&" : "?";
return this.request<T>(`${path}${separator}actor_user_id=${encodeURIComponent(this.actorUserId)}`, {
method: "GET",
});
}
private async post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
private async put<T>(path: string, body: unknown): Promise<T> {
return this.request<T>(path, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
private async request<T>(path: string, init: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, init);
if (!response.ok) {
let message = `Запрос завершился ошибкой HTTP ${response.status}`;
try {
const payload = (await response.json()) as ApiErrorPayload;
message = payload.error?.fallback_message || payload.error?.code || message;
} catch {
// Keep generic HTTP message if backend did not return JSON.
}
throw new Error(message);
}
return (await response.json()) as T;
}
}
function browserDeviceFingerprint(): string {
const key = "rap.webAdmin.deviceFingerprint";
const existing = localStorage.getItem(key);
if (existing) {
return existing;
}
const value = `web-admin-${createBrowserIdentifier()}`;
localStorage.setItem(key, value);
return value;
}
function createBrowserIdentifier(): string {
if (typeof globalThis.crypto?.randomUUID === "function") {
return globalThis.crypto.randomUUID();
}
if (typeof globalThis.crypto?.getRandomValues === "function") {
const bytes = new Uint8Array(16);
globalThis.crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./styles.css";
createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
File diff suppressed because it is too large Load Diff
+529
View File
@@ -0,0 +1,529 @@
export type Cluster = {
id: string;
slug: string;
name: string;
status: string;
region?: string | null;
metadata?: Record<string, unknown>;
created_at: string;
updated_at?: string;
};
export type AuthUser = {
ID?: string;
id?: string;
Email?: string;
email?: string;
};
export type AuthSession = {
ID?: string;
id?: string;
};
export type AuthTokens = {
access_token: string;
access_token_expires_at: string;
refresh_token: string;
refresh_token_expires_at: string;
};
export type AuthResult = {
user: AuthUser;
auth_session: AuthSession;
tokens: AuthTokens;
};
export type InstallationStatus = {
bootstrapped: boolean;
authority_state: string;
install_id?: string;
bootstrapped_owner_email?: string;
bootstrapped_at?: string;
authority_mode: string;
strict_authority: boolean;
root_fingerprint?: string;
insecure_bootstrap_allowed: boolean;
};
export type BootstrapOwnerResult = {
installation: InstallationStatus;
user: AuthUser;
platform_role: string;
};
export type ClusterAuthorityState = {
cluster_id: string;
authority_state: string;
mutation_mode: string;
term: number;
notes?: string | null;
updated_by_user_id?: string | null;
updated_at: string;
};
export type ClusterNode = {
id: string;
owner_organization_id?: string | null;
node_key: string;
name: string;
ownership_type: string;
registration_status: string;
health_status: string;
version_state: string;
partition_state: string;
reported_version?: string | null;
last_seen_at?: string | null;
membership_status: string;
membership_metadata?: Record<string, unknown>;
node_group_id?: string | null;
node_group_name?: string | null;
created_at?: string;
updated_at?: string;
};
export type ClusterNodeGroup = {
id: string;
cluster_id: string;
parent_group_id?: string | null;
name: string;
description?: string | null;
sort_order: number;
metadata?: Record<string, unknown>;
created_by_user_id?: string | null;
created_at: string;
updated_at?: string;
};
export type JoinRequest = {
id: string;
cluster_id: string;
node_name: string;
node_fingerprint: string;
public_key?: string;
reported_capabilities: Record<string, unknown>;
reported_facts: Record<string, unknown>;
requested_roles: unknown[];
status: string;
reviewed_at?: string | null;
approved_node_id?: string | null;
rejection_reason?: string | null;
created_at: string;
updated_at?: string;
approval_payload?: Record<string, unknown>;
approval_signature?: ClusterSignature;
};
export type ClusterSignature = {
schema_version: string;
algorithm: string;
key_fingerprint: string;
signature: string;
signed_at: string;
};
export type ClusterAuthorityDescriptor = {
schema_version: string;
cluster_id: string;
authority_state: string;
key_algorithm: string;
public_key: string;
public_key_fingerprint: string;
created_at: string;
updated_at: string;
};
export type CreatedJoinToken = {
id: string;
cluster_id: string;
scope: Record<string, unknown>;
expires_at: string;
max_uses: number;
used_count: number;
status: string;
created_at: string;
revoked_at?: string | null;
authority_payload?: Record<string, unknown>;
authority_signature?: ClusterSignature;
token: string;
};
export type RoleAssignment = {
id: string;
cluster_id: string;
node_id: string;
organization_id?: string | null;
role: string;
status: string;
policy?: Record<string, unknown>;
assigned_at: string;
revoked_at?: string | null;
};
export type AuditEvent = {
id: string;
cluster_id?: string | null;
actor_user_id?: string | null;
event_type: string;
target_type: string;
target_id?: string | null;
payload: Record<string, unknown>;
created_at: string;
};
export type WorkloadStatus = {
id: string;
cluster_id: string;
node_id: string;
service_type: string;
reported_state: string;
runtime_mode: string;
version?: string | null;
status_payload?: Record<string, unknown>;
observed_at: string;
};
export type NodeWorkloadDesiredState = {
cluster_id: string;
node_id: string;
service_type: string;
desired_state: string;
version?: string | null;
runtime_mode: string;
artifact_ref?: string | null;
config?: Record<string, unknown>;
environment?: Record<string, unknown>;
updated_by_user_id?: string | null;
updated_at: string;
};
export type NodeHeartbeat = {
id: string;
cluster_id: string;
node_id: string;
health_status: string;
reported_version?: string | null;
capabilities?: Record<string, unknown>;
service_states?: Record<string, unknown>;
metadata?: Record<string, unknown>;
observed_at: string;
};
export type FabricTestingFlag = {
id: string;
scope_type: string;
scope_id?: string | null;
cluster_id?: string | null;
enabled: boolean;
telemetry_enabled: boolean;
synthetic_links_enabled: boolean;
history_retention_hours: number;
metadata?: Record<string, unknown>;
updated_by_user_id?: string | null;
updated_at: string;
};
export type NodeTelemetryObservation = {
id: string;
cluster_id: string;
node_id: string;
cpu_percent?: number | null;
memory_used_bytes?: number | null;
memory_total_bytes?: number | null;
disk_used_bytes?: number | null;
disk_total_bytes?: number | null;
network_rx_bytes?: number | null;
network_tx_bytes?: number | null;
process_count?: number | null;
payload?: Record<string, unknown>;
observed_at: string;
};
export type MeshLink = {
id: string;
cluster_id: string;
source_node_id: string;
target_node_id: string;
link_status: string;
latency_ms?: number | null;
quality_score?: number | null;
metadata?: Record<string, unknown>;
observed_at: string;
};
export type PeerEndpointCandidate = {
endpoint_id: string;
node_id: string;
transport: string;
address: string;
address_family?: string | null;
reachability: string;
nat_type?: string | null;
connectivity_mode: string;
region?: string | null;
priority: number;
policy_tags?: string[];
last_verified_at?: string | null;
metadata?: Record<string, unknown>;
};
export type PeerDirectoryEntry = {
node_id: string;
route_ids?: string[];
endpoint_count: number;
candidate_count: number;
connectivity_modes?: string[];
recovery_seed: boolean;
};
export type PeerRecoverySeed = {
node_id: string;
endpoint: string;
transport: string;
connectivity_mode?: string | null;
region?: string | null;
priority: number;
last_verified_at?: string | null;
metadata?: Record<string, unknown>;
};
export type PeerRendezvousLease = {
lease_id: string;
peer_node_id: string;
relay_node_id: string;
relay_endpoint: string;
transport: string;
connectivity_mode?: string | null;
route_ids?: string[];
allowed_channels?: string[];
priority: number;
control_plane_only: boolean;
issued_at: string;
expires_at: string;
reason?: string | null;
metadata?: Record<string, unknown>;
};
export type RendezvousRelayPolicyDecision = {
route_id?: string;
peer_node_id: string;
withdrawn_lease_id?: string;
stale_relay_node_id?: string;
selected_relay_id?: string;
selected_endpoint?: string;
score?: number;
reason: string;
score_reasons?: string[];
reporter_node_id?: string;
};
export type RendezvousRelayPolicyReport = {
schema_version: string;
scoring_mode: string;
feedback_max_age_seconds: number;
stale_relay_count: number;
withdrawn_lease_count: number;
replacement_lease_count: number;
decisions?: RendezvousRelayPolicyDecision[];
};
export type RoutePathDecision = {
decision_id: string;
route_id: string;
cluster_id: string;
local_node_id: string;
source_node_id: string;
destination_node_id: string;
original_hops: string[];
effective_hops: string[];
previous_hop_id?: string;
next_hop_id?: string;
local_role: string;
selected_relay_id?: string;
selected_relay_endpoint?: string;
stale_relay_node_id?: string;
rendezvous_lease_id?: string;
rendezvous_lease_reason?: string;
decision_source: string;
generation: string;
path_score?: number;
score_reasons?: string[];
control_plane_only: boolean;
production_forwarding: boolean;
expires_at: string;
};
export type RoutePathDecisionReport = {
schema_version: string;
decision_mode: string;
generation: string;
decision_count: number;
replacement_decision_count: number;
control_plane_only: boolean;
production_forwarding: boolean;
decisions?: RoutePathDecision[];
};
export type SyntheticMeshRoute = {
route_id: string;
cluster_id: string;
source_node_id: string;
destination_node_id: string;
hops: string[];
allowed_channels: string[];
expires_at: string;
max_ttl: number;
max_hops: number;
route_version?: string;
policy_version?: string;
peer_directory_version?: string;
};
export type NodeSyntheticMeshConfig = {
enabled: boolean;
schema_version: string;
cluster_id: string;
local_node_id: string;
authority_required: boolean;
cluster_authority?: ClusterAuthorityDescriptor;
authority_payload?: Record<string, unknown>;
authority_signature?: ClusterSignature;
config_version?: string;
peer_directory_version?: string;
policy_version?: string;
peer_endpoints: Record<string, string>;
peer_endpoint_candidates?: Record<string, PeerEndpointCandidate[]>;
peer_directory?: PeerDirectoryEntry[];
recovery_seeds?: PeerRecoverySeed[];
rendezvous_leases?: PeerRendezvousLease[];
rendezvous_relay_policy?: RendezvousRelayPolicyReport;
route_path_decisions?: RoutePathDecisionReport;
routes: SyntheticMeshRoute[];
production_forwarding: boolean;
};
export type FabricEntryPoint = {
id: string;
cluster_id: string;
name: string;
status: string;
endpoint_type: string;
public_endpoint?: string | null;
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
created_by_user_id?: string | null;
created_at: string;
updated_at?: string;
};
export type FabricEntryPointNode = {
entry_point_id: string;
cluster_id: string;
node_id: string;
status: string;
priority: number;
metadata?: Record<string, unknown>;
added_by_user_id?: string | null;
added_at: string;
};
export type FabricEgressPool = {
id: string;
cluster_id: string;
name: string;
status: string;
description?: string | null;
route_scope?: Record<string, unknown>;
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
created_by_user_id?: string | null;
created_at: string;
updated_at?: string;
};
export type FabricEgressPoolNode = {
egress_pool_id: string;
cluster_id: string;
node_id: string;
status: string;
priority: number;
metadata?: Record<string, unknown>;
added_by_user_id?: string | null;
added_at: string;
};
export type QoSPolicy = {
id: string;
cluster_id: string;
service_class: string;
priority: number;
reliability_mode: string;
drop_policy: string;
bandwidth_policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type ClusterAdminSummary = {
cluster_id: string;
slug: string;
name: string;
status: string;
region?: string | null;
authority_state: string;
mutation_mode: string;
cluster_key_algorithm?: string | null;
cluster_key_fingerprint?: string | null;
node_count: number;
healthy_node_count: number;
pending_join_count: number;
active_role_assignment_count: number;
last_node_seen_at?: string | null;
};
export type OrganizationAdminSummary = {
organization_id: string;
resource_count: number;
active_session_count: number;
service_endpoints: Array<{ protocol: string; count: number }>;
connector_status: Record<string, unknown>;
topology_exposure: string;
};
export type VPNConnection = {
id: string;
cluster_id: string;
organization_id: string;
name: string;
target_endpoint: Record<string, unknown>;
protocol_family: string;
credential_ref?: string | null;
mode: string;
desired_state: string;
allowed_node_policy: Record<string, unknown>;
routing_usage: unknown[];
route_policy: Record<string, unknown>;
qos_policy: Record<string, unknown>;
placement_policy: Record<string, unknown>;
status: string;
metadata: Record<string, unknown>;
created_by_user_id?: string | null;
updated_by_user_id?: string | null;
created_at: string;
updated_at: string;
};
export type VPNConnectionLease = {
id: string;
vpn_connection_id: string;
cluster_id: string;
owner_node_id: string;
lease_generation: number;
fencing_token: string;
status: string;
acquired_at: string;
renewed_at: string;
expires_at: string;
released_at?: string | null;
fenced_at?: string | null;
metadata: Record<string, unknown>;
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": []
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/api/client.ts"],"version":"6.0.3"}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
const backendTarget = process.env.RAP_ADMIN_API_PROXY || "http://192.168.200.61:8080";
export default defineConfig({
plugins: [react()],
server: {
host: "0.0.0.0",
proxy: {
"/api": {
target: backendTarget,
changeOrigin: true,
},
},
},
});