@@ -106,7 +115,7 @@ export function DataTable
({
{columns.map((col) => (
- {col.header}
+ {renderHeaderLabel(col)}
))}
@@ -136,7 +145,7 @@ export function DataTable({
{columns.map((col) => (
- {col.header}
+ {renderHeaderLabel(col)}
))}
@@ -189,7 +198,7 @@ export function DataTable({
)}
) : (
- col.header
+ renderHeaderLabel(col)
)}
);
diff --git a/src/components/layout/__tests__/app-sidebar.test.tsx b/src/components/layout/__tests__/app-sidebar.test.tsx
index c069f317..70045597 100644
--- a/src/components/layout/__tests__/app-sidebar.test.tsx
+++ b/src/components/layout/__tests__/app-sidebar.test.tsx
@@ -96,6 +96,7 @@ vi.mock("lucide-react", () => {
FolderSearch: icon,
ClipboardCheck: icon,
Gauge: icon,
+ Hourglass: icon,
};
});
diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx
index 7f82648e..d131543c 100644
--- a/src/components/layout/app-sidebar.tsx
+++ b/src/components/layout/app-sidebar.tsx
@@ -36,6 +36,7 @@ import {
FolderSearch,
ClipboardCheck,
Gauge,
+ Hourglass,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/providers/auth-provider";
@@ -94,6 +95,7 @@ const securityItems: NavItem[] = [
const operationsItems: NavItem[] = [
{ title: "Analytics", href: "/analytics", icon: BarChart3 },
{ title: "Approvals", href: "/approvals", icon: ClipboardCheck },
+ { title: "Age Gate", href: "/age-gate", icon: Hourglass },
{ title: "Health", href: "/system-health", icon: HeartPulse },
{ title: "Lifecycle", href: "/lifecycle", icon: Recycle },
{ title: "Monitoring", href: "/monitoring", icon: Activity },
diff --git a/src/lib/api/age-gate.ts b/src/lib/api/age-gate.ts
new file mode 100644
index 00000000..f4c50820
--- /dev/null
+++ b/src/lib/api/age-gate.ts
@@ -0,0 +1,96 @@
+import { apiFetch } from '@/lib/api/fetch';
+
+export interface AgeGateReview {
+ id: string;
+ repository_key: string;
+ package_name: string;
+ package_version: string;
+ upstream_published_at?: string | null;
+ status: 'pending' | 'approved' | 'rejected';
+ requested_at: string;
+ reviewed_by?: string | null;
+ reviewed_at?: string | null;
+ review_reason?: string | null;
+ request_count: number;
+ last_requested_at: string;
+}
+
+export interface AgeGateReviewListResponse {
+ items: AgeGateReview[];
+ pagination: {
+ page: number;
+ per_page: number;
+ total: number;
+ total_pages: number;
+ };
+}
+
+export interface AgeGateConfig {
+ repository_key: string;
+ enabled: boolean;
+ min_age_days: number;
+}
+
+const ageGateApi = {
+ listReviews: async (params?: {
+ repository_key?: string;
+ status?: string;
+ page?: number;
+ per_page?: number;
+ }): Promise => {
+ const searchParams = new URLSearchParams();
+ if (params?.repository_key) searchParams.set('repository_key', params.repository_key);
+ if (params?.status) searchParams.set('status', params.status);
+ if (params?.page) searchParams.set('page', String(params.page));
+ if (params?.per_page) searchParams.set('per_page', String(params.per_page));
+ const qs = searchParams.toString();
+ return apiFetch(
+ `/api/v1/admin/age-gate/reviews${qs ? `?${qs}` : ''}`,
+ );
+ },
+
+ getReview: async (id: string): Promise => {
+ return apiFetch(
+ `/api/v1/admin/age-gate/reviews/${encodeURIComponent(id)}`,
+ );
+ },
+
+ approve: async (id: string, reason?: string): Promise => {
+ return apiFetch(
+ `/api/v1/admin/age-gate/reviews/${encodeURIComponent(id)}/approve`,
+ {
+ method: 'POST',
+ body: JSON.stringify({ reason }),
+ },
+ );
+ },
+
+ reject: async (id: string, reason?: string): Promise => {
+ return apiFetch(
+ `/api/v1/admin/age-gate/reviews/${encodeURIComponent(id)}/reject`,
+ {
+ method: 'POST',
+ body: JSON.stringify({ reason }),
+ },
+ );
+ },
+
+ getRepoConfig: async (repoKey: string): Promise => {
+ return apiFetch(`/api/v1/repositories/${encodeURIComponent(repoKey)}/age-gate`);
+ },
+
+ updateRepoConfig: async (
+ repoKey: string,
+ config: Pick,
+ ): Promise => {
+ return apiFetch(
+ `/api/v1/repositories/${encodeURIComponent(repoKey)}/age-gate`,
+ {
+ method: 'PUT',
+ body: JSON.stringify(config),
+ },
+ );
+ },
+};
+
+export default ageGateApi;
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 98fd44af..e83a1080 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -19,6 +19,7 @@ export { default as lifecycleApi } from './lifecycle';
export { default as telemetryApi } from './telemetry';
export { default as monitoringApi } from './monitoring';
export { default as qualityGatesApi } from './quality-gates';
+export { default as ageGateApi } from './age-gate';
export type { LoginCredentials } from './auth';
export type { ListRepositoriesParams } from './repositories';
diff --git a/src/lib/api/webhooks.ts b/src/lib/api/webhooks.ts
index 824996fe..55a23894 100644
--- a/src/lib/api/webhooks.ts
+++ b/src/lib/api/webhooks.ts
@@ -34,7 +34,10 @@ export type WebhookEvent =
| 'user_deleted'
| 'build_started'
| 'build_completed'
- | 'build_failed';
+ | 'build_failed'
+ | 'age_gate_queued'
+ | 'age_gate_approved'
+ | 'age_gate_rejected';
const WEBHOOK_EVENTS = new Set([
'artifact_uploaded',
@@ -46,6 +49,9 @@ const WEBHOOK_EVENTS = new Set([
'build_started',
'build_completed',
'build_failed',
+ 'age_gate_queued',
+ 'age_gate_approved',
+ 'age_gate_rejected',
]);
export interface Webhook {