@flame0510/project-aether 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { maybeSendAlert } from '@/lib/alerts';
|
|
4
|
+
import { summarizeBilling } from '@/lib/billing';
|
|
5
|
+
import { listOpenClawCronJobs } from '@/lib/openclaw-cron';
|
|
6
|
+
import type { ModelCost } from '@/lib/types';
|
|
7
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
type Health = 'ok' | 'warning' | 'error';
|
|
12
|
+
type Severity = 'info' | 'warning' | 'critical';
|
|
13
|
+
|
|
14
|
+
interface SystemCheck {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
health: Health;
|
|
18
|
+
value: string | number;
|
|
19
|
+
details?: string;
|
|
20
|
+
source: 'runtime' | 'cron' | 'cost' | 'trello';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Recommendation {
|
|
24
|
+
id: string;
|
|
25
|
+
severity: Severity;
|
|
26
|
+
source: SystemCheck['source'];
|
|
27
|
+
title: string;
|
|
28
|
+
details: string;
|
|
29
|
+
actionHref?: string;
|
|
30
|
+
dismissible: boolean;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
trelloCardId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function topHealth(values: Health[]): Health {
|
|
36
|
+
if (values.includes('error')) return 'error';
|
|
37
|
+
if (values.includes('warning')) return 'warning';
|
|
38
|
+
return 'ok';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSeconds(value: unknown): number {
|
|
42
|
+
const n = Number(value ?? 0);
|
|
43
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
44
|
+
return n > 10_000_000_000 ? Math.floor(n / 1000) : n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getEnvNumber(name: string, fallback: number): number {
|
|
48
|
+
const value = Number(process.env[name] ?? fallback);
|
|
49
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tableExists(db: ReturnType<typeof openDb>, tableName: string): boolean {
|
|
53
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
|
|
54
|
+
return Boolean(row);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function columnExists(db: ReturnType<typeof openDb>, tableName: string, columnName: string): boolean {
|
|
58
|
+
return (db.prepare(`PRAGMA table_info(${tableName})`).all() as { name: string }[]).some((column) => column.name === columnName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
62
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
63
|
+
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const createdAt = now.toISOString();
|
|
66
|
+
const checks: SystemCheck[] = [];
|
|
67
|
+
const recommendations: Recommendation[] = [];
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const db = openDb();
|
|
71
|
+
const sessionCount = (db.prepare('SELECT COUNT(*) AS total FROM sessions').get() as { total: number }).total;
|
|
72
|
+
const latestSessionRow = db.prepare('SELECT MAX(COALESCE(updated_at, started_at, 0)) AS latest FROM sessions').get() as { latest: number | null };
|
|
73
|
+
const latestSessionTs = getSeconds(latestSessionRow.latest);
|
|
74
|
+
const latestSessionAgeSeconds = latestSessionTs ? Math.floor(Date.now() / 1000) - latestSessionTs : null;
|
|
75
|
+
const staleThresholdSeconds = getEnvNumber('REV4A_ALERT_STALE_SECONDS', 120);
|
|
76
|
+
|
|
77
|
+
checks.push({
|
|
78
|
+
id: 'runtime.sessions',
|
|
79
|
+
label: 'Runtime sessions',
|
|
80
|
+
health: 'ok',
|
|
81
|
+
value: sessionCount,
|
|
82
|
+
details: latestSessionAgeSeconds === null ? 'No session detected' : `last session activity ${Math.round(latestSessionAgeSeconds / 60)}m ago`,
|
|
83
|
+
source: 'runtime',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const hasSystemMetrics = tableExists(db, 'system_metrics');
|
|
87
|
+
const latestMetricRow = hasSystemMetrics
|
|
88
|
+
? (db.prepare('SELECT MAX(ts) AS latest FROM system_metrics').get() as { latest: number | null })
|
|
89
|
+
: null;
|
|
90
|
+
const latestMetricTs = getSeconds(latestMetricRow?.latest);
|
|
91
|
+
const latestMetricAgeSeconds = latestMetricTs ? Math.floor(Date.now() / 1000) - latestMetricTs : null;
|
|
92
|
+
const runtimeFreshnessHealth: Health = !hasSystemMetrics
|
|
93
|
+
? 'ok'
|
|
94
|
+
: latestMetricAgeSeconds === null
|
|
95
|
+
? 'warning'
|
|
96
|
+
: latestMetricAgeSeconds > staleThresholdSeconds
|
|
97
|
+
? 'warning'
|
|
98
|
+
: 'ok';
|
|
99
|
+
|
|
100
|
+
checks.push({
|
|
101
|
+
id: 'runtime.ingestion',
|
|
102
|
+
label: 'Runtime ingestion',
|
|
103
|
+
health: runtimeFreshnessHealth,
|
|
104
|
+
value: hasSystemMetrics ? (latestMetricTs ? 'fresh' : 'missing') : 'n/d',
|
|
105
|
+
details: !hasSystemMetrics
|
|
106
|
+
? 'Heartbeat metrics unavailable in this DB'
|
|
107
|
+
: latestMetricAgeSeconds === null
|
|
108
|
+
? 'No heartbeat metrics detected'
|
|
109
|
+
: `ultimo heartbeat metrics ${latestMetricAgeSeconds}s fa`,
|
|
110
|
+
source: 'runtime',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (hasSystemMetrics && runtimeFreshnessHealth !== 'ok') {
|
|
114
|
+
recommendations.push({
|
|
115
|
+
id: 'runtime.check-freshness',
|
|
116
|
+
severity: 'warning',
|
|
117
|
+
source: 'runtime',
|
|
118
|
+
title: 'Check Rev4a runtime heartbeat',
|
|
119
|
+
details: latestMetricAgeSeconds === null
|
|
120
|
+
? 'No heartbeat metrics available from runtime.'
|
|
121
|
+
: `Heartbeat metrics stuck for ${latestMetricAgeSeconds}s (soglia ${staleThresholdSeconds}s).`,
|
|
122
|
+
actionHref: '/lineage',
|
|
123
|
+
dismissible: false,
|
|
124
|
+
createdAt,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await maybeSendAlert({
|
|
129
|
+
key: 'system-health.db-freshness',
|
|
130
|
+
kind: 'db-freshness',
|
|
131
|
+
title: 'Rev4a DB freshness',
|
|
132
|
+
message: !hasSystemMetrics
|
|
133
|
+
? 'system_metrics table unavailable: freshness runtime not verifiable.'
|
|
134
|
+
: latestMetricAgeSeconds === null
|
|
135
|
+
? 'No heartbeat metrics detected.'
|
|
136
|
+
: `Last heartbeat metrics ${latestMetricAgeSeconds}s fa (soglia ${staleThresholdSeconds}s).`,
|
|
137
|
+
resolvedMessage: 'DB Rev4a fresh again.',
|
|
138
|
+
stale: hasSystemMetrics ? (latestMetricAgeSeconds === null ? true : latestMetricAgeSeconds > staleThresholdSeconds) : false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const eventExpr = tableExists(db, 'events')
|
|
142
|
+
? [columnExists(db, 'events', 'type') ? 'type' : null, columnExists(db, 'events', 'event') ? 'event' : null].filter(Boolean).join(", ''), COALESCE(")
|
|
143
|
+
: '';
|
|
144
|
+
const eventErrorCount = eventExpr
|
|
145
|
+
? (db.prepare(`SELECT COUNT(*) AS total FROM events WHERE LOWER(COALESCE(${eventExpr}, '')) LIKE '%error%' AND ts >= ?`).get(Math.floor(Date.now() / 1000) - 86400) as { total: number }).total
|
|
146
|
+
: 0;
|
|
147
|
+
checks.push({
|
|
148
|
+
id: 'runtime.errors24h',
|
|
149
|
+
label: 'Errors last 24h',
|
|
150
|
+
health: eventErrorCount > 0 ? 'warning' : 'ok',
|
|
151
|
+
value: eventErrorCount,
|
|
152
|
+
source: 'runtime',
|
|
153
|
+
});
|
|
154
|
+
if (eventErrorCount > 0) {
|
|
155
|
+
recommendations.push({
|
|
156
|
+
id: 'runtime.review-errors',
|
|
157
|
+
severity: 'warning',
|
|
158
|
+
source: 'runtime',
|
|
159
|
+
title: 'Review recent errors',
|
|
160
|
+
details: `${eventErrorCount} error events nelle ultime 24h.`,
|
|
161
|
+
actionHref: '/lineage',
|
|
162
|
+
dismissible: true,
|
|
163
|
+
createdAt,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const startOfDay = new Date();
|
|
168
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
169
|
+
const todayModels = db.prepare(
|
|
170
|
+
`SELECT model,
|
|
171
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
172
|
+
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
173
|
+
COALESCE(SUM(tokens_out), 0) AS tokens_out,
|
|
174
|
+
COUNT(*) AS sessions
|
|
175
|
+
FROM sessions WHERE started_at >= ?
|
|
176
|
+
GROUP BY model ORDER BY cost_usd DESC`,
|
|
177
|
+
).all(Math.floor(startOfDay.getTime() / 1000)) as ModelCost[];
|
|
178
|
+
const billing = summarizeBilling(todayModels);
|
|
179
|
+
checks.push({
|
|
180
|
+
id: 'cost.usageBasedToday',
|
|
181
|
+
label: 'Usage-based cost today',
|
|
182
|
+
health: billing.usageBasedCost > 10 ? 'warning' : 'ok',
|
|
183
|
+
value: `$${billing.usageBasedCost.toFixed(2)}`,
|
|
184
|
+
details: `DB estimate $${billing.dbEstimatedCost.toFixed(2)}; fixed ${billing.buckets.fixed.sessions} sessioni; credits ${billing.buckets.credits.sessions} sessioni`,
|
|
185
|
+
source: 'cost',
|
|
186
|
+
});
|
|
187
|
+
if (billing.usageBasedCost > 10) {
|
|
188
|
+
recommendations.push({
|
|
189
|
+
id: 'cost.review-usage-based',
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
source: 'cost',
|
|
192
|
+
title: 'High usage-based cost',
|
|
193
|
+
details: `Provider usage-based today: $${billing.usageBasedCost.toFixed(2)}. Include OpenRouter e altri provider pay-per-use, non i piani fissi.`,
|
|
194
|
+
actionHref: '/gateway',
|
|
195
|
+
dismissible: true,
|
|
196
|
+
createdAt,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (billing.unknownModels.length > 0 && billing.buckets.unknown.cost_usd > 1) {
|
|
200
|
+
recommendations.push({
|
|
201
|
+
id: 'cost.classify-unknown-models',
|
|
202
|
+
severity: 'warning',
|
|
203
|
+
source: 'cost',
|
|
204
|
+
title: 'Unclassified models in billing',
|
|
205
|
+
details: `${billing.unknownModels.length} unclassified models have DB estimate $${billing.buckets.unknown.cost_usd.toFixed(2)}.`,
|
|
206
|
+
actionHref: '/gateway',
|
|
207
|
+
dismissible: true,
|
|
208
|
+
createdAt,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
db.close();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
checks.push({ id: 'runtime.db', label: 'Rev4a DB', health: 'error', value: 'error', details: (error as Error).message, source: 'runtime' });
|
|
214
|
+
recommendations.push({ id: 'runtime.db-error', severity: 'critical', source: 'runtime', title: 'Rev4a DB unreadable', details: (error as Error).message, dismissible: false, createdAt });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const cron = await listOpenClawCronJobs();
|
|
219
|
+
const enabled = cron.jobs.filter((job) => job.enabled !== false).length;
|
|
220
|
+
const hostCount = cron.jobs.filter((j) => j.source === 'host').length;
|
|
221
|
+
const containerCount = cron.jobs.filter((j) => j.source === 'container').length;
|
|
222
|
+
checks.push({ id: 'cron.jobs', label: 'Cron jobs', health: enabled > 0 ? 'ok' : 'warning', value: enabled, details: `${hostCount} host · ${containerCount} container`, source: 'cron' });
|
|
223
|
+
if (enabled === 0) {
|
|
224
|
+
recommendations.push({ id: 'cron.enable-watchdog', severity: 'warning', source: 'cron', title: 'No enabled cron detected', details: 'No cron jobs are enabled on the host or any agent container.', actionHref: '/crons', dismissible: false, createdAt });
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
checks.push({ id: 'cron.openclaw', label: 'Cron jobs', health: 'warning', value: 'warning', details: (error as Error).message, source: 'cron' });
|
|
228
|
+
recommendations.push({ id: 'cron.openclaw-unavailable', severity: 'warning', source: 'cron', title: 'Cron list unavailable', details: 'Failed to list cron jobs.', actionHref: '/crons', dismissible: false, createdAt });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const health = topHealth(checks.map((check) => check.health));
|
|
232
|
+
return NextResponse.json({ health, checks, recommendations, generatedAt: createdAt });
|
|
233
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
const { searchParams } = new URL(request.url);
|
|
10
|
+
const limit = Math.min(Number.parseInt(searchParams.get('limit') ?? '50', 10), 200);
|
|
11
|
+
const sessionId = searchParams.get('session_id');
|
|
12
|
+
try {
|
|
13
|
+
const db = openDb();
|
|
14
|
+
const rows = sessionId
|
|
15
|
+
? db
|
|
16
|
+
.prepare('SELECT * FROM tool_calls WHERE session_id = ? ORDER BY ts DESC LIMIT ?')
|
|
17
|
+
.all(sessionId, limit)
|
|
18
|
+
: db.prepare('SELECT * FROM tool_calls ORDER BY ts DESC LIMIT ?').all(limit);
|
|
19
|
+
db.close();
|
|
20
|
+
return NextResponse.json(rows);
|
|
21
|
+
} catch {
|
|
22
|
+
return NextResponse.json([], { status: 200 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
const CONFIG_PATH = '/data/.openclaw/openclaw.json';
|
|
9
|
+
const REV4A_SETTINGS_PATH = '/data/rev4a/settings.json';
|
|
10
|
+
|
|
11
|
+
type JsonObject = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
function readConfig(): JsonObject {
|
|
14
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as JsonObject;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatBackupTimestamp(date: Date): string {
|
|
18
|
+
const pad = (v: number) => String(v).padStart(2, '0');
|
|
19
|
+
return [
|
|
20
|
+
date.getFullYear(),
|
|
21
|
+
pad(date.getMonth() + 1),
|
|
22
|
+
pad(date.getDate()),
|
|
23
|
+
pad(date.getHours()),
|
|
24
|
+
pad(date.getMinutes()),
|
|
25
|
+
pad(date.getSeconds()),
|
|
26
|
+
].join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeConfig(config: JsonObject): void {
|
|
30
|
+
const dir = path.dirname(CONFIG_PATH);
|
|
31
|
+
const base = path.basename(CONFIG_PATH);
|
|
32
|
+
const backup = path.join(dir, `${base}.bak-${formatBackupTimestamp(new Date())}`);
|
|
33
|
+
const tmp = path.join(dir, `${base}.tmp-${process.pid}-${Date.now()}`);
|
|
34
|
+
const data = `${JSON.stringify(config, null, 2)}\n`;
|
|
35
|
+
|
|
36
|
+
fs.copyFileSync(CONFIG_PATH, backup);
|
|
37
|
+
|
|
38
|
+
let fd: number | undefined;
|
|
39
|
+
let dfd: number | undefined;
|
|
40
|
+
try {
|
|
41
|
+
fd = fs.openSync(tmp, 'w', 0o600);
|
|
42
|
+
fs.writeFileSync(fd, data, 'utf8');
|
|
43
|
+
fs.fsyncSync(fd);
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
fd = undefined;
|
|
46
|
+
fs.renameSync(tmp, CONFIG_PATH);
|
|
47
|
+
dfd = fs.openSync(dir, 'r');
|
|
48
|
+
fs.fsyncSync(dfd);
|
|
49
|
+
fs.closeSync(dfd);
|
|
50
|
+
dfd = undefined;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
53
|
+
if (dfd !== undefined) fs.closeSync(dfd);
|
|
54
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getAudio(config: JsonObject): JsonObject {
|
|
60
|
+
const tools = (config.tools ?? {}) as JsonObject;
|
|
61
|
+
const media = (tools.media ?? {}) as JsonObject;
|
|
62
|
+
return (media.audio ?? {}) as JsonObject;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readRev4aSettings(): JsonObject {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(REV4A_SETTINGS_PATH, 'utf8')) as JsonObject;
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeRev4aSettings(settings: JsonObject): void {
|
|
74
|
+
const dir = path.dirname(REV4A_SETTINGS_PATH);
|
|
75
|
+
const tmp = path.join(dir, `.settings.json.tmp-${process.pid}-${Date.now()}`);
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
fs.writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
78
|
+
fs.renameSync(tmp, REV4A_SETTINGS_PATH);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getTimezone(): string {
|
|
82
|
+
const settings = readRev4aSettings();
|
|
83
|
+
const tz = settings.timezone;
|
|
84
|
+
return typeof tz === 'string' && tz ? tz : 'Europe/Rome';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
88
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
89
|
+
try {
|
|
90
|
+
const config = readConfig();
|
|
91
|
+
const audio = getAudio(config);
|
|
92
|
+
const timezone = getTimezone();
|
|
93
|
+
return NextResponse.json({ audio, timezone });
|
|
94
|
+
} catch (e: unknown) {
|
|
95
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface AudioInput {
|
|
100
|
+
enabled?: boolean;
|
|
101
|
+
timeoutSeconds?: number;
|
|
102
|
+
maxBytes?: number;
|
|
103
|
+
models?: JsonObject[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const VALID_TIMEZONES = [
|
|
107
|
+
'Europe/Rome', 'UTC', 'Europe/London', 'Europe/Berlin', 'Europe/Paris',
|
|
108
|
+
'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
|
|
109
|
+
'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Dubai', 'Asia/Kolkata',
|
|
110
|
+
'Australia/Sydney', 'Pacific/Auckland',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export async function PUT(request: NextRequest): Promise<NextResponse> {
|
|
114
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
115
|
+
try {
|
|
116
|
+
const body = (await request.json()) as { audio?: AudioInput; timezone?: string };
|
|
117
|
+
|
|
118
|
+
const config = readConfig();
|
|
119
|
+
if (!config.tools) config.tools = {};
|
|
120
|
+
const tools = config.tools as JsonObject;
|
|
121
|
+
let timezone = getTimezone();
|
|
122
|
+
|
|
123
|
+
if (body.timezone !== undefined) {
|
|
124
|
+
if (typeof body.timezone !== 'string' || !body.timezone)
|
|
125
|
+
throw new Error('timezone must be a non-empty string');
|
|
126
|
+
if (!VALID_TIMEZONES.includes(body.timezone))
|
|
127
|
+
throw new Error('timezone is not supported');
|
|
128
|
+
timezone = body.timezone;
|
|
129
|
+
writeRev4aSettings({ ...readRev4aSettings(), timezone });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const input = body?.audio;
|
|
133
|
+
if (input) {
|
|
134
|
+
if (typeof input !== 'object') throw new Error('audio must be an object');
|
|
135
|
+
if (input.enabled !== undefined && typeof input.enabled !== 'boolean')
|
|
136
|
+
throw new Error('enabled must be boolean');
|
|
137
|
+
if (input.timeoutSeconds !== undefined && (typeof input.timeoutSeconds !== 'number' || !Number.isFinite(input.timeoutSeconds)))
|
|
138
|
+
throw new Error('timeoutSeconds must be a number');
|
|
139
|
+
if (input.maxBytes !== undefined && (typeof input.maxBytes !== 'number' || !Number.isFinite(input.maxBytes)))
|
|
140
|
+
throw new Error('maxBytes must be a number');
|
|
141
|
+
if (input.models !== undefined && !Array.isArray(input.models))
|
|
142
|
+
throw new Error('models must be an array');
|
|
143
|
+
|
|
144
|
+
if (!tools.media) tools.media = {};
|
|
145
|
+
const media = tools.media as JsonObject;
|
|
146
|
+
const existing = (media.audio ?? {}) as JsonObject;
|
|
147
|
+
|
|
148
|
+
const next: JsonObject = { ...existing };
|
|
149
|
+
if (input.enabled !== undefined) next.enabled = input.enabled;
|
|
150
|
+
if (input.timeoutSeconds !== undefined) next.timeoutSeconds = input.timeoutSeconds;
|
|
151
|
+
if (input.maxBytes !== undefined) next.maxBytes = input.maxBytes;
|
|
152
|
+
if (input.models !== undefined) next.models = input.models;
|
|
153
|
+
|
|
154
|
+
media.audio = next;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeConfig(config);
|
|
158
|
+
|
|
159
|
+
return NextResponse.json({ ok: true, timezone, audio: config.tools && (config.tools as JsonObject).media ? ((config.tools as JsonObject).media as JsonObject).audio : {} });
|
|
160
|
+
} catch (e: unknown) {
|
|
161
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 400 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { jwtVerify } from 'jose';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
7
|
+
|
|
8
|
+
function getPackageInfo() {
|
|
9
|
+
try {
|
|
10
|
+
// Resolve relative to the project root (two levels above .next)
|
|
11
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
12
|
+
const raw = readFileSync(pkgPath, 'utf-8');
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
return { version: '0.0.0', name: '@flame0510/project-aether' };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CachedResult {
|
|
20
|
+
latest: string | null;
|
|
21
|
+
updateAvailable: boolean;
|
|
22
|
+
checkedAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cache: CachedResult | null = null;
|
|
26
|
+
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET /api/update-check
|
|
30
|
+
*/
|
|
31
|
+
export async function GET(request: Request): Promise<NextResponse> {
|
|
32
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
const check = url.searchParams.get('check') === '1';
|
|
35
|
+
|
|
36
|
+
const pkg = getPackageInfo();
|
|
37
|
+
const installed = pkg.version || '0.0.0';
|
|
38
|
+
let latest: string | null = null;
|
|
39
|
+
let updateAvailable = false;
|
|
40
|
+
|
|
41
|
+
if (check) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
44
|
+
latest = cache.latest;
|
|
45
|
+
updateAvailable = cache.updateAvailable;
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
const pkgName = pkg.name || '@flame0510/project-aether';
|
|
49
|
+
latest = execSync(
|
|
50
|
+
`npm view ${pkgName} version --registry https://registry.npmjs.org 2>/dev/null`,
|
|
51
|
+
{ stdio: 'pipe', timeout: 10_000 },
|
|
52
|
+
).toString().trim();
|
|
53
|
+
|
|
54
|
+
if (latest && latest !== installed) {
|
|
55
|
+
updateAvailable = true;
|
|
56
|
+
}
|
|
57
|
+
cache = { latest, updateAvailable, checkedAt: now };
|
|
58
|
+
} catch {
|
|
59
|
+
if (cache) {
|
|
60
|
+
latest = cache.latest;
|
|
61
|
+
updateAvailable = cache.updateAvailable;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return NextResponse.json({ installed, latest, updateAvailable, package: getPackageInfo().name });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function verifyAuth(request: Request): Promise<boolean> {
|
|
71
|
+
const raw = request.headers.get('cookie') || '';
|
|
72
|
+
const match = raw.split(';').map(c => c.trim()).find(c => c.startsWith('rev4a_token='));
|
|
73
|
+
if (!match) return false;
|
|
74
|
+
|
|
75
|
+
const cookieToken = decodeURIComponent(match.split('=')[1]);
|
|
76
|
+
try {
|
|
77
|
+
const secret = new TextEncoder().encode(
|
|
78
|
+
process.env.REV4A_JWT_SECRET || (() => { throw new Error('REV4A_JWT_SECRET not set'); })(),
|
|
79
|
+
);
|
|
80
|
+
await jwtVerify(cookieToken, secret);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* POST /api/update-check
|
|
89
|
+
*
|
|
90
|
+
* Spawns rev4a update as a detached background process.
|
|
91
|
+
* rev4a update installs the latest version and writes .restart-flag,
|
|
92
|
+
* which rev4a serve detects and exits with code 42, then auto-restarts
|
|
93
|
+
* with the newly installed binary. No systemd required.
|
|
94
|
+
*/
|
|
95
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
96
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
97
|
+
if (!(await verifyAuth(request))) {
|
|
98
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Spawn rev4a update in background, then write restart flag directly
|
|
102
|
+
// from this process so rev4a serve detects it and restarts.
|
|
103
|
+
const { spawn } = require('child_process');
|
|
104
|
+
const { join } = require('path');
|
|
105
|
+
const { writeFileSync } = require('fs');
|
|
106
|
+
|
|
107
|
+
const dataDir = process.env.REV4A_DATA_DIR ||
|
|
108
|
+
join(process.env.HOME || '/root', '.config', 'rev4a', 'data');
|
|
109
|
+
|
|
110
|
+
const child = spawn('rev4a', ['update'], {
|
|
111
|
+
stdio: 'ignore',
|
|
112
|
+
detached: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// When rev4a update finishes, write the restart flag so rev4a serve
|
|
116
|
+
// picks it up and exits with code 42, triggering the auto-restart loop.
|
|
117
|
+
child.on('exit', () => {
|
|
118
|
+
try { writeFileSync(join(dataDir, '.restart-flag'), ''); } catch {}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
child.unref();
|
|
122
|
+
|
|
123
|
+
return NextResponse.json({
|
|
124
|
+
success: true,
|
|
125
|
+
message: 'Update started. The dashboard will restart once complete.',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
let pkg: { version: string } = { version: '0.0.0' };
|
|
6
|
+
try {
|
|
7
|
+
pkg = require('../../../package.json');
|
|
8
|
+
} catch {
|
|
9
|
+
/* fallback */
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/version
|
|
16
|
+
*
|
|
17
|
+
* Returns the installed Rev4a version from package.json.
|
|
18
|
+
* No auth required.
|
|
19
|
+
*/
|
|
20
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
21
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
22
|
+
return NextResponse.json({
|
|
23
|
+
version: pkg.version,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/wizard/complete
|
|
3
|
+
*
|
|
4
|
+
* Sets wizard as complete in a persistent file (cross-browser).
|
|
5
|
+
* Also sets the httpOnly cookie for backwards compatibility.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from 'next/server';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { REV4A_DATA } from '@/lib/rev4a-paths';
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
const WIZARD_FILE = REV4A_DATA + '/data/wizard.json';
|
|
15
|
+
|
|
16
|
+
export function POST(): NextResponse {
|
|
17
|
+
// Write persistent marker
|
|
18
|
+
const dir = path.dirname(WIZARD_FILE);
|
|
19
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(WIZARD_FILE, JSON.stringify({ done: true }, null, 2));
|
|
21
|
+
|
|
22
|
+
const response = NextResponse.json({ done: true });
|
|
23
|
+
// Also set cookie for backwards compat / existing clients
|
|
24
|
+
response.cookies.set('wizard_done', '1', {
|
|
25
|
+
httpOnly: true,
|
|
26
|
+
sameSite: 'lax',
|
|
27
|
+
secure: process.env.NODE_ENV === 'production',
|
|
28
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
29
|
+
path: '/',
|
|
30
|
+
});
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/wizard/reset
|
|
3
|
+
*
|
|
4
|
+
* Clears the wizard marker so the wizard runs again.
|
|
5
|
+
* Removes both the persistent file and the cookie.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from 'next/server';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { REV4A_DATA } from '@/lib/rev4a-paths';
|
|
10
|
+
|
|
11
|
+
export const dynamic = 'force-dynamic';
|
|
12
|
+
|
|
13
|
+
const WIZARD_FILE = REV4A_DATA + '/data/wizard.json';
|
|
14
|
+
|
|
15
|
+
export function POST(): NextResponse {
|
|
16
|
+
// Remove persistent marker
|
|
17
|
+
try { fs.unlinkSync(WIZARD_FILE); } catch { /* ignore */ }
|
|
18
|
+
|
|
19
|
+
const response = NextResponse.json({ done: false });
|
|
20
|
+
response.cookies.set('wizard_done', '', {
|
|
21
|
+
httpOnly: true,
|
|
22
|
+
secure: process.env.NODE_ENV === 'production',
|
|
23
|
+
sameSite: 'lax',
|
|
24
|
+
maxAge: 0,
|
|
25
|
+
path: '/',
|
|
26
|
+
});
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/wizard/status
|
|
3
|
+
*
|
|
4
|
+
* Returns whether the wizard is complete.
|
|
5
|
+
* Checks a persistent file so it survives browser changes and cache clears.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from 'next/server';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { REV4A_DATA } from '@/lib/rev4a-paths';
|
|
10
|
+
|
|
11
|
+
export const dynamic = 'force-dynamic';
|
|
12
|
+
|
|
13
|
+
const WIZARD_FILE = REV4A_DATA + '/data/wizard.json';
|
|
14
|
+
|
|
15
|
+
function readWizardFile(): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs.readFileSync(WIZARD_FILE, 'utf-8');
|
|
18
|
+
const data = JSON.parse(raw);
|
|
19
|
+
return data.done === true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function GET(): NextResponse {
|
|
26
|
+
const done = readWizardFile();
|
|
27
|
+
return NextResponse.json({ done });
|
|
28
|
+
}
|