feat(app): management foundation - client exposure, manage hub, shared shell

Groundwork for the full admin surface on mobile: the API client gains
the user-account and Claude-management types (skills, connectors,
plugins, permissions, users/auth, owner fields) plus an unauthenticated
authApi helper; AppState exposes the client and the new management
screen names; ManageShell/Field/ErrorNotice and a useResource hook give
the subscreens one shared page/fetch pattern; Settings gains Manage and
Account rows leading to the new hub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
This commit is contained in:
Claude
2026-08-13 13:48:50 +00:00
parent 25cf6c9f5b
commit 804baadddd
7 changed files with 447 additions and 4 deletions
+40
View File
@@ -12,9 +12,12 @@ import {
SectionLabel,
StatusDot,
} from "../components/primitives";
import { Icon } from "../components/Icon";
import { TabBar } from "../components/TabBar";
import { useAppState } from "../state/AppState";
import { useServerConfig } from "../state/ServerConfig";
import { createClient } from "../api/client";
import { Pressable } from "react-native";
type Ping =
| { state: "checking" }
@@ -25,6 +28,7 @@ export function SettingsScreen() {
const { colors } = useTheme();
const insets = useSafeAreaInsets();
const { config, clear } = useServerConfig();
const { go } = useAppState();
// Notification toggles stay local (no server-side counterpart yet).
const [pushWait, setPushWait] = useState(true);
@@ -94,6 +98,21 @@ export function SettingsScreen() {
</View>
</Card>
<SectionLabel style={{ marginBottom: 10 }}>Control</SectionLabel>
<Card style={{ marginBottom: 20 }}>
<NavRow
title="Manage"
subtitle="Models, skills, repos, users — the full admin surface"
onPress={() => go("manage")}
/>
<Divider />
<NavRow
title="Account"
subtitle="Who you're signed in as, change password"
onPress={() => go("account")}
/>
</Card>
<SectionLabel style={{ marginBottom: 10 }}>Notifications</SectionLabel>
<Card style={{ marginBottom: 20 }}>
<ToggleRow
@@ -125,6 +144,27 @@ export function SettingsScreen() {
);
}
function NavRow({
title,
subtitle,
onPress,
}: {
title: string;
subtitle: string;
onPress: () => void;
}) {
const { colors } = useTheme();
return (
<Pressable style={[styles.infoRow, { paddingVertical: 12 }]} onPress={onPress}>
<View style={{ flex: 1, gap: 2 }}>
<Text style={[text.label, { color: colors.textHeading }]}>{title}</Text>
<Text style={[text.caption, { color: colors.textMuted }]}>{subtitle}</Text>
</View>
<Icon name="chevronRight" size={16} color={colors.textMuted} />
</Pressable>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
const { colors } = useTheme();
return (
+86
View File
@@ -0,0 +1,86 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { text } from "../../theme/tokens";
import { useTheme } from "../../theme/useTheme";
import { Icon } from "../../components/Icon";
import { ManageShell } from "../../components/ManageShell";
import { Card, Divider, SectionLabel } from "../../components/primitives";
import { useAppState, type Screen } from "../../state/AppState";
/**
* The management hub (Settings → Manage): the mobile counterpart of the web
* dashboard's admin surface. Most actions here need the admin token (or an
* admin account session); non-admin sessions get inline 403s on writes.
*/
interface Row {
screen: Screen;
title: string;
subtitle: string;
}
const CLAUDE_ROWS: Row[] = [
{ screen: "models", title: "Models", subtitle: "Model backends for spawns + schedules" },
{ screen: "skills", title: "Skills", subtitle: "Managed skills synced to every worker" },
{ screen: "connectors", title: "Connectors", subtitle: "MCP servers agents may reach" },
{ screen: "plugins", title: "Plugins", subtitle: "Marketplace plugins installed on boot" },
{ screen: "permissions", title: "Permissions", subtitle: "Default mode + allow/deny/ask rules" },
{ screen: "claudeLogin", title: "Claude login", subtitle: "Sign the worker into Claude Code" },
];
const SERVER_ROWS: Row[] = [
{ screen: "repositories", title: "Repositories", subtitle: "Register + sync project repos" },
{ screen: "gitServers", title: "Git servers", subtitle: "Forge hosts, tokens, deploy keys" },
{ screen: "approvals", title: "Approvals", subtitle: "Approve or reject protected branches" },
{ screen: "shared", title: "Shared context", subtitle: "Key/value context all agents see" },
{ screen: "users", title: "Users", subtitle: "Invite, promote, disable, reset" },
];
export function ManageScreen() {
return (
<ManageShell
title="Manage"
subtitle="The full control surface — everything the web dashboard can do."
backTo="settings"
>
<SectionLabel style={{ marginBottom: 8 }}>Claude</SectionLabel>
<RowsCard rows={CLAUDE_ROWS} />
<SectionLabel style={{ marginTop: 20, marginBottom: 8 }}>Server</SectionLabel>
<RowsCard rows={SERVER_ROWS} />
</ManageShell>
);
}
function RowsCard({ rows }: { rows: Row[] }) {
const { colors } = useTheme();
const { go } = useAppState();
return (
<Card>
{rows.map((r, i) => (
<View key={r.screen}>
{i > 0 && <Divider />}
<Pressable style={styles.row} onPress={() => go(r.screen)}>
<View style={{ flex: 1, gap: 2 }}>
<Text style={[text.label, { color: colors.textHeading }]}>{r.title}</Text>
<Text style={[text.caption, { color: colors.textMuted }]}>
{r.subtitle}
</Text>
</View>
<Icon name="chevronRight" size={16} color={colors.textMuted} />
</Pressable>
</View>
))}
</Card>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingVertical: 12,
paddingHorizontal: 16,
minHeight: 44,
},
});