Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
"navAnnouncements": "Announcements",
"navOrgChart": "Org chart",
"navEmployees": "Employees",
"navGovernance": "Governance",
"announcementsTitle": "Announcements",
"announcementsBody": "Publish notices and track who has read them. This page lives in the host — no sidecar or second port.",
"openAnnouncements": "Open announcements",
Expand All @@ -193,7 +194,10 @@
"openOrgChart": "Open org chart",
"employeesTitle": "Employees",
"employeesBody": "Directory of people and AI colleagues on the host org tree. Same store as the org chart — no sidecar or second port.",
"openEmployees": "Open employees"
"openEmployees": "Open employees",
"governanceTitle": "Governance",
"governanceBody": "Pending approvals and the host audit log. High-risk tools stay blocked until a human decides — no sidecar or second port.",
"openGovernance": "Open governance"
},
"systemSettings": {
"subtitle": "Workbench, remote desktop, and host administration in one place."
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
"navAnnouncements": "通知公告",
"navOrgChart": "组织架构",
"navEmployees": "员工目录",
"navGovernance": "治理",
"announcementsTitle": "通知公告",
"announcementsBody": "发布公告并跟踪已读。此页跑在宿主内,不需要边车或第二端口。",
"openAnnouncements": "打开通知公告",
Expand All @@ -193,7 +194,10 @@
"openOrgChart": "打开组织架构",
"employeesTitle": "员工目录",
"employeesBody": "宿主组织树上的人机员工名册。与组织架构共用同一份数据,不需要边车或第二端口。",
"openEmployees": "打开员工目录"
"openEmployees": "打开员工目录",
"governanceTitle": "治理",
"governanceBody": "待人工复核与宿主审计日志。高风险工具在有人拍板前保持拦截,不需要边车或第二端口。",
"openGovernance": "打开治理"
},
"systemSettings": {
"subtitle": "工作台、远程桌面与主机管理集中在这里。"
Expand Down
7 changes: 7 additions & 0 deletions dashboard/src/org-ui/api/createClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ describe("createOrgApiClient", () => {
],
};
}
if (path === "/org-module/governance/pauses") {
return {
pauses: [{ pause_id: "p1", status: "pending", tool_name: "delete" }],
enabled: true,
};
}
throw new Error(`unexpected ${path}`);
});
const client = createOrgApiClient({ fetchJson });
Expand All @@ -53,6 +59,7 @@ describe("createOrgApiClient", () => {
expect(await client.announcements.unread()).toEqual({ count: 2 });
expect((await client.org.tree())[0]?.name).toBe("HQ");
expect((await client.employees.list())[0]?.name).toBe("Ada");
expect((await client.governance.pauses()).pauses[0]?.pause_id).toBe("p1");
expect(fetchJson).toHaveBeenCalled();
});

Expand Down
92 changes: 91 additions & 1 deletion dashboard/src/org-ui/api/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,77 @@ export interface OrgEmployeesClient {
remove(id: number): Promise<void>;
}

export type GovernancePauseStatus =
| "pending"
| "approved"
| "rejected"
| "expired";

export interface GovernancePause {
pause_id: string;
tool_name: string;
category: string;
action: string;
actor_id: string;
tenant_id: string;
args_digest: string;
reason: string;
status: GovernancePauseStatus | string;
created_at: number;
resolved_at: number | null;
ttl_seconds: number;
}

export interface GovernanceAuditEvent {
audit_id?: string;
event?: string;
tool_name?: string;
category?: string;
action?: string;
actor_id?: string;
tenant_id?: string;
args_digest?: string;
result?: string;
execute?: boolean;
reason?: string;
pause_id?: string;
rule_source?: string;
ts?: number;
[key: string]: unknown;
}

export interface GovernanceDecision {
status: string;
execute: boolean;
blocked: boolean;
reason: string;
category?: string;
pause_id?: string;
rule_source?: string;
sidecar_reached?: boolean;
audit_id?: string;
}

export interface GovernancePauseList {
pauses: GovernancePause[];
enabled?: boolean;
}

export interface GovernanceAuditList {
events: GovernanceAuditEvent[];
}

export interface OrgGovernanceClient {
pauses(status?: string): Promise<GovernancePauseList>;
audit(limit?: number): Promise<GovernanceAuditList>;
resolve(pauseId: string, approve: boolean): Promise<GovernanceDecision>;
}

export interface OrgApiClient {
announcements: OrgAnnouncementsClient;
org: OrgChartClient;
employees: OrgEmployeesClient;
governance: OrgGovernanceClient;
}

async function unwrap<T>(payload: OrgEnvelope<T>): Promise<T> {
Expand All @@ -170,9 +237,11 @@ export function createOrgApiClient(opts: {
fetchJson: OrgFetcher;
announcementsPrefix?: string;
orgPrefix?: string;
governancePrefix?: string;
}): OrgApiClient {
const prefix = opts.announcementsPrefix ?? "/org-module/announcements";
const orgPrefix = opts.orgPrefix ?? "/org-module/org";
const governancePrefix = opts.governancePrefix ?? "/org-module/governance";
const fetchJson = opts.fetchJson;

const announcements: OrgAnnouncementsClient = {
Expand Down Expand Up @@ -375,5 +444,26 @@ export function createOrgApiClient(opts: {
},
};

return { announcements, org, employees };
const governance: OrgGovernanceClient = {
async pauses(status) {
const query = status ? `?status=${encodeURIComponent(status)}` : "";
return fetchJson<GovernancePauseList>(
`${governancePrefix}/pauses${query}`,
);
},
async audit(limit = 50) {
return fetchJson<GovernanceAuditList>(
`${governancePrefix}/audit?limit=${limit}`,
);
},
async resolve(pauseId, approve) {
return fetchJson<GovernanceDecision>(`${governancePrefix}/resolve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pause_id: pauseId, approve }),
});
},
};

return { announcements, org, employees, governance };
}
1 change: 1 addition & 0 deletions dashboard/src/org-ui/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const SHARED_ORG_UI_MODULES = [
"announcements",
"organization",
"employees",
"governance",
] as const;

export type SharedOrgUiModule = (typeof SHARED_ORG_UI_MODULES)[number];
9 changes: 9 additions & 0 deletions dashboard/src/org-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ export type {
EmployeeListParams,
EmployeeStats,
EmployeeWrite,
GovernanceAuditEvent,
GovernanceAuditList,
GovernanceDecision,
GovernancePause,
GovernancePauseList,
GovernancePauseStatus,
OrgAnnouncementsClient,
OrgApiClient,
OrgChartClient,
OrgDepartment,
OrgEmployee,
OrgEmployeesClient,
OrgEnvelope,
OrgGovernanceClient,
} from "./api/createClient";
export { AnnouncementPage } from "./pages/announcements/AnnouncementPage";
export type { AnnouncementPageProps } from "./pages/announcements/AnnouncementPage";
Expand All @@ -25,6 +32,8 @@ export { EmployeesPage } from "./pages/employees/EmployeesPage";
export type { EmployeesPageProps } from "./pages/employees/EmployeesPage";
export { EmployeeDetailPage } from "./pages/employees/EmployeeDetailPage";
export type { EmployeeDetailPageProps } from "./pages/employees/EmployeeDetailPage";
export { GovernancePage } from "./pages/governance/GovernancePage";
export type { GovernancePageProps } from "./pages/governance/GovernancePage";
export type {
IdentityBridge,
OrgFetcher,
Expand Down
121 changes: 121 additions & 0 deletions dashboard/src/org-ui/pages/governance/GovernancePage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
.page {
max-width: 1040px;
margin: 0 auto;
padding: 8px 4px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}

.header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}

.titleRow {
display: flex;
align-items: center;
gap: 10px;
}

.title {
margin: 0;
font-size: 20px;
font-weight: 650;
}

.subtitle {
margin: 4px 0 0;
color: var(--fn-text-secondary, #667085);
font-size: 13px;
}

.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
}

.stat {
border: 1px solid var(--fn-border, #e5e7eb);
border-radius: 10px;
padding: 12px 14px;
background: var(--fn-bg-container, #fff);
}

.statValue {
font-size: 22px;
font-weight: 650;
line-height: 1.2;
}

.statLabel {
margin-top: 4px;
font-size: 12px;
color: var(--fn-text-secondary, #667085);
}

.hint {
margin: 0;
color: var(--fn-text-secondary, #667085);
font-size: 13px;
line-height: 1.5;
}

.empty {
text-align: center;
padding: 48px 12px;
color: var(--fn-text-secondary, #667085);
}

.tableWrap {
overflow-x: auto;
border: 1px solid var(--fn-border, #e5e7eb);
border-radius: 12px;
background: var(--fn-bg-container, #fff);
}

.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
}

.reason {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.detailGrid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}

.row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid var(--fn-border, #f0f0f0);
}

.row:last-child {
border-bottom: none;
}

.muted {
color: var(--fn-text-secondary, #667085);
}

.body {
margin: 0;
white-space: pre-wrap;
line-height: 1.6;
font-size: 13px;
}
Loading
Loading