Skip to content

Commit 5e3f8ec

Browse files
author
Stan Misiurev
committed
Teams feature + orchestrator availability UX
- Team-scoped data model and authorization: instances, shared folders, backup schedules, and LLM providers are now associated with teams; handlers enforce team membership on all relevant routes - Frontend: TeamSelector + InstanceTeamPicker, Teams/Members/Providers settings tabs, team-aware queries across pages - Backend unavailable handling: orchestrator watcher hook, OrchestratorDownToast, BackendUnavailablePage, and shared `utils/http` helper - Browser image and agent Dockerfile updates (rename to `claworc/<browser>-browser`), helm chart and CI workflow tweaks - Misc UI polish on Backups, SharedFolders, Skills, Login, Settings, Sidebar, Layout, and Usage pages
1 parent f2f7b64 commit 5e3f8ec

52 files changed

Lines changed: 1835 additions & 356 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/control-plane.yml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ on:
77
- '.github/workflows/control-plane.yml'
88

99
jobs:
10+
migration-check:
11+
name: Migration Drift Check
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0 # need history for the git-diff guard below
17+
18+
- uses: actions/setup-go@v5
19+
with:
20+
go-version-file: control-plane/go.mod
21+
cache-dependency-path: control-plane/go.sum
22+
23+
- name: Run migrationcheck (verify migrations cover all models)
24+
working-directory: control-plane
25+
run: go run ./cmd/migrationcheck
26+
27+
- name: Guard against model changes without a new migration
28+
if: github.event_name == 'pull_request' || github.event_name == 'push'
29+
run: |
30+
set -euo pipefail
31+
# Compare against the merge base on PRs; against the previous commit on push.
32+
if [ -n "${GITHUB_BASE_REF:-}" ]; then
33+
base="origin/${GITHUB_BASE_REF}"
34+
git fetch --no-tags --depth=1 origin "${GITHUB_BASE_REF}"
35+
else
36+
base="HEAD~1"
37+
fi
38+
changed_models=$(git diff --name-only "$base"...HEAD -- 'control-plane/internal/database/models/*.go' || true)
39+
new_migrations=$(git diff --name-only --diff-filter=A "$base"...HEAD -- 'control-plane/internal/database/migrations/migration_*.go' || true)
40+
if [ -n "$changed_models" ] && [ -z "$new_migrations" ]; then
41+
echo "ERROR: models.go changed but no new migration_*.go was added."
42+
echo " Run 'make migration' from control-plane/ to author one."
43+
exit 1
44+
fi
45+
1046
unit:
1147
name: Unit Tests
1248
runs-on: ubuntu-latest
@@ -112,7 +148,7 @@ jobs:
112148
push-manifest:
113149
name: Push Manifest
114150
if: github.ref == 'refs/heads/main'
115-
needs: [unit, integration, build-amd64, build-arm64]
151+
needs: [migration-check, unit, integration, build-amd64, build-arm64]
116152
runs-on: ubuntu-latest
117153
steps:
118154
- uses: docker/setup-buildx-action@v3

agent/browser/Dockerfile.base

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ ARG NOVNC_VERSION=1.4.0
88
ARG WEBSOCKIFY_VERSION=0.12.0
99

1010
# Browser-only base image: Xvfb + TigerVNC + noVNC + openbox + stealth
11-
# extension + sshd. NO OpenClaw, NO cron — those live in glukw/claworc-agent.
11+
# extension + sshd. NO OpenClaw, NO cron — those live in claworc/openclaw.
1212
#
1313
# CDP listens on 127.0.0.1:9222 and noVNC on 127.0.0.1:3000 (loopback only —
1414
# Chromium 134+ silently refuses non-loopback DevTools binding). Externally,
1515
# the only reachable port is sshd on 22; the control plane SSHes in and uses
1616
# ssh.Client.Dial to reach 127.0.0.1:9222 / 127.0.0.1:3000.
1717
#
18-
# Published as glukw/claworc-browser-base:latest. The variant images
19-
# (glukw/claworc-browser-{chromium,chrome,brave}) FROM this and add the
18+
# Published as claworc/base-browser:latest. The variant images
19+
# (claworc/{chromium,chrome,brave}-browser) FROM this and add the
2020
# specific browser package.
2121

2222
RUN useradd -m -u 1000 -s /bin/bash claworc

agent/browser/Dockerfile.brave

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG BASE_IMAGE=glukw/claworc-browser-base:latest
1+
ARG BASE_IMAGE=claworc/base-browser:latest
22
FROM ${BASE_IMAGE}
33

44
# brave-browser is amd64-only on Linux. Fail fast on other arches.

agent/browser/Dockerfile.chrome

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG BASE_IMAGE=glukw/claworc-browser-base:latest
1+
ARG BASE_IMAGE=claworc/base-browser:latest
22
FROM ${BASE_IMAGE}
33

44
# google-chrome-stable is amd64-only on Linux. Fail fast on other arches so

agent/browser/Dockerfile.chromium

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG BASE_IMAGE=glukw/claworc-browser-base:latest
1+
ARG BASE_IMAGE=claworc/base-browser:latest
22
FROM ${BASE_IMAGE}
33

44
RUN apt-get update && \

agent/instance/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ ARG USE_CHINA_MIRRORS=false
66
ARG OPENCLAW_VERSION=latest
77

88
# Slim agent image: OpenClaw + sshd + cron only. The browser (Chromium /
9-
# Chrome / Brave) and the VNC stack live in glukw/claworc-browser-* images
9+
# Chrome / Brave) and the VNC stack live in claworc/<browser>-browser images
1010
# launched on demand by the control plane; this image has none of those
1111
# packages.
1212
#
13-
# Published as glukw/claworc-agent:latest. New instances created via the
13+
# Published as claworc/openclaw:latest. New instances created via the
1414
# control-plane API default to this image; legacy instances continue to pull
1515
# the old combined glukw/openclaw-vnc-* images already in the registry.
1616

control-plane/frontend/src/App.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import CreateInstancePage from "./pages/CreateInstancePage";
55
import InstanceDetailPage from "./pages/InstanceDetailPage";
66
import SettingsPage from "./pages/SettingsPage";
77
import LoginPage from "./pages/LoginPage";
8+
import BackendUnavailablePage from "./pages/BackendUnavailablePage";
89
import UsersPage from "./pages/UsersPage";
910
import TeamsPage from "./pages/TeamsPage";
1011
import UsagePage from "./pages/UsagePage";
@@ -18,30 +19,40 @@ import KanbanPage from "./pages/KanbanPage";
1819
import { useAuth } from "./contexts/AuthContext";
1920

2021
function ProtectedRoute({ children }: { children: React.ReactNode }) {
21-
const { user, isLoading } = useAuth();
22+
const { user, isLoading, isBackendUnavailable } = useAuth();
2223
if (isLoading) return null;
24+
if (isBackendUnavailable) return <BackendUnavailablePage />;
2325
if (!user) return <Navigate to="/login" replace />;
2426
return <>{children}</>;
2527
}
2628

2729
function AdminRoute({ children }: { children: React.ReactNode }) {
28-
const { isAdmin, isLoading } = useAuth();
30+
const { isAdmin, isLoading, isBackendUnavailable } = useAuth();
2931
if (isLoading) return null;
32+
if (isBackendUnavailable) return <BackendUnavailablePage />;
3033
if (!isAdmin) return <Navigate to="/" replace />;
3134
return <>{children}</>;
3235
}
3336

37+
function LoginRoute() {
38+
const { isBackendUnavailable, isLoading } = useAuth();
39+
if (isLoading) return null;
40+
if (isBackendUnavailable) return <BackendUnavailablePage />;
41+
return <LoginPage />;
42+
}
43+
3444
function InstanceCreatorRoute({ children }: { children: React.ReactNode }) {
35-
const { canCreateInstances, isLoading } = useAuth();
45+
const { canCreateInstances, isLoading, isBackendUnavailable } = useAuth();
3646
if (isLoading) return null;
47+
if (isBackendUnavailable) return <BackendUnavailablePage />;
3748
if (!canCreateInstances) return <Navigate to="/" replace />;
3849
return <>{children}</>;
3950
}
4051

4152
export default function App() {
4253
return (
4354
<Routes>
44-
<Route path="/login" element={<LoginPage />} />
55+
<Route path="/login" element={<LoginRoute />} />
4556
<Route
4657
path="/instances/:id/vnc"
4758
element={
@@ -113,9 +124,9 @@ export default function App() {
113124
<Route
114125
path="/skills"
115126
element={
116-
<AdminRoute>
127+
<ProtectedRoute>
117128
<SkillsPage />
118-
</AdminRoute>
129+
</ProtectedRoute>
119130
}
120131
/>
121132
<Route

control-plane/frontend/src/api/backups.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ export async function fetchInstanceBackups(
2828
return data;
2929
}
3030

31-
export async function fetchAllBackups(): Promise<Backup[]> {
32-
const { data } = await client.get<Backup[]>("/backups");
31+
export interface BackupsPage {
32+
backups: Backup[];
33+
total: number;
34+
limit: number;
35+
offset: number;
36+
}
37+
38+
export async function fetchAllBackups(
39+
params: { limit: number; offset: number; instance?: string },
40+
): Promise<BackupsPage> {
41+
const { data } = await client.get<BackupsPage>("/backups", { params });
3342
return data;
3443
}
3544

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
11
import axios from "axios";
22

3-
interface HealthResponse {
3+
export type OrchestratorReason =
4+
| "daemon_unreachable"
5+
| "client_error"
6+
| "config_missing"
7+
| "namespace_missing"
8+
| "invalid_setting"
9+
| "unavailable"
10+
| "unknown"
11+
| "";
12+
13+
export interface BackendAttempt {
14+
backend: string;
15+
ok: boolean;
16+
reason?: OrchestratorReason;
17+
message?: string;
18+
at: string;
19+
}
20+
21+
export interface OrchestratorStatus {
22+
backend: "kubernetes" | "docker" | "none";
23+
available: boolean;
24+
last_attempt: string;
25+
attempts?: BackendAttempt[];
26+
}
27+
28+
export interface HealthResponse {
429
status: string;
530
orchestrator: "connected" | "disconnected";
631
orchestrator_backend: "kubernetes" | "docker" | "none";
32+
orchestrator_status?: OrchestratorStatus;
733
database: string;
834
build_date?: string;
935
}
@@ -12,3 +38,10 @@ export async function fetchHealth(): Promise<HealthResponse> {
1238
const { data } = await axios.get<HealthResponse>("/health");
1339
return data;
1440
}
41+
42+
export async function reinitializeOrchestrator(): Promise<OrchestratorStatus> {
43+
const { data } = await axios.post<OrchestratorStatus>(
44+
"/api/v1/orchestrator/reinitialize",
45+
);
46+
return data;
47+
}

control-plane/frontend/src/api/sharedFolders.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface SharedFolder {
66
mount_path: string;
77
owner_id: number;
88
instance_ids: number[];
9+
team_ids: number[];
910
created_at: string;
1011
}
1112

@@ -29,7 +30,12 @@ export async function getSharedFolder(id: number): Promise<SharedFolder> {
2930

3031
export async function updateSharedFolder(
3132
id: number,
32-
data: { name?: string; mount_path?: string; instance_ids?: number[] },
33+
data: {
34+
name?: string;
35+
mount_path?: string;
36+
instance_ids?: number[];
37+
team_ids?: number[];
38+
},
3339
): Promise<void> {
3440
await client.put(`/shared-folders/${id}`, data);
3541
}

0 commit comments

Comments
 (0)