clawport-ui 0.6.7 → 0.8.0
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/.env.example +4 -0
- package/README.md +2 -2
- package/app/activity/page.tsx +2 -16
- package/app/api/chat/[id]/route.ts +2 -1
- package/app/api/kanban/chat/[id]/route.ts +2 -1
- package/app/api/pipelines/route.ts +53 -0
- package/app/api/transcribe/route.ts +2 -1
- package/app/api/tts/route.ts +2 -1
- package/app/api/usage/stream/route.ts +75 -0
- package/app/crons/page.tsx +41 -41
- package/app/memory/page.tsx +1 -11
- package/app/providers.tsx +1 -7
- package/bin/clawport.mjs +9 -4
- package/components/LiveStreamWidget.tsx +186 -112
- package/components/MobileSidebar.tsx +2 -1
- package/components/NavLinks.tsx +52 -48
- package/components/OnboardingWizard.tsx +1 -1
- package/components/Sidebar.tsx +2 -1
- package/components/costs/ClaudeUsageRow.tsx +131 -0
- package/components/costs/CostsPage.tsx +497 -419
- package/components/costs/DailyCostChart.tsx +117 -0
- package/components/costs/OptimizationPanel.tsx +220 -0
- package/components/costs/RunDetailTable.tsx +108 -0
- package/components/costs/SummaryCard.tsx +18 -0
- package/components/costs/TokenDonut.tsx +87 -0
- package/components/costs/TopCrons.tsx +60 -0
- package/components/costs/formatters.ts +20 -0
- package/components/crons/PipelineDetailPanel.tsx +683 -0
- package/components/crons/PipelineGraph.tsx +689 -138
- package/components/crons/PipelineWizard.tsx +771 -0
- package/components/docs/ArchitectureSection.tsx +3 -3
- package/components/docs/GettingStartedSection.tsx +3 -2
- package/components/docs/TroubleshootingSection.tsx +4 -2
- package/components/sidebar/SidebarUsageWidget.tsx +150 -0
- package/lib/claude-usage.test.ts +190 -0
- package/lib/claude-usage.ts +99 -0
- package/lib/costs.test.ts +205 -6
- package/lib/costs.ts +320 -6
- package/lib/cron-utils.test.ts +127 -2
- package/lib/cron-utils.ts +45 -0
- package/lib/env.ts +10 -0
- package/lib/pipeline-utils.test.ts +563 -0
- package/lib/pipeline-utils.ts +296 -0
- package/lib/sanitize.ts +30 -6
- package/lib/setup-detection.ts +17 -0
- package/lib/themes.ts +1 -2
- package/lib/types.ts +26 -0
- package/package.json +1 -1
- package/scripts/setup.mjs +55 -10
package/.env.example
CHANGED
|
@@ -29,6 +29,10 @@ OPENCLAW_GATEWAY_TOKEN=your-gateway-token-here
|
|
|
29
29
|
# Optional
|
|
30
30
|
# ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
+
# OpenClaw gateway port (default: 18789).
|
|
33
|
+
# Change this if you configured a custom port in openclaw.json (gateway.http.port).
|
|
34
|
+
# OPENCLAW_GATEWAY_PORT=18789
|
|
35
|
+
|
|
32
36
|
# ElevenLabs API key — enables voice indicators on agent profiles.
|
|
33
37
|
# Get one at: https://elevenlabs.io
|
|
34
38
|
# Leave blank or remove this line if you don't need voice features.
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ After onboarding, verify the gateway is running:
|
|
|
42
42
|
openclaw gateway status
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
You should see your gateway URL (`localhost:18789`) and auth token. See the [OpenClaw docs](https://docs.openclaw.ai/getting-started) for more detail.
|
|
45
|
+
You should see your gateway URL (default `localhost:18789`) and auth token. If you use a custom port, `clawport setup` will detect it automatically. See the [OpenClaw docs](https://docs.openclaw.ai/getting-started) for more detail.
|
|
46
46
|
|
|
47
47
|
### 2. Install ClawPort
|
|
48
48
|
|
|
@@ -100,7 +100,7 @@ npm run dev
|
|
|
100
100
|
ClawPort reads your OpenClaw workspace to discover agents, then connects to the gateway for all AI operations:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
|
-
Browser --> ClawPort (Next.js) --> OpenClaw Gateway (localhost:18789) --> Claude
|
|
103
|
+
Browser --> ClawPort (Next.js) --> OpenClaw Gateway (localhost:18789 default) --> Claude
|
|
104
104
|
| |
|
|
105
105
|
| Text: /v1/chat/completions (streaming SSE)
|
|
106
106
|
| Vision: openclaw gateway call chat.send (CLI)
|
package/app/activity/page.tsx
CHANGED
|
@@ -2,26 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import type { LogEntry, LogFilter, LogSummary } from '@/lib/types'
|
|
5
|
+
import { timeAgo } from '@/lib/cron-utils'
|
|
5
6
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
6
7
|
import { RefreshCw, Radio } from 'lucide-react'
|
|
7
8
|
import { ErrorState } from '@/components/ErrorState'
|
|
8
9
|
import { LogBrowser } from '@/components/activity/LogBrowser'
|
|
9
10
|
|
|
10
|
-
/* ── Time helpers ──────────────────────────────────────────────── */
|
|
11
|
-
|
|
12
|
-
function timeAgo(dateStr: string): string {
|
|
13
|
-
const d = new Date(dateStr)
|
|
14
|
-
if (isNaN(d.getTime())) return '--'
|
|
15
|
-
const diff = Date.now() - d.getTime()
|
|
16
|
-
const mins = Math.floor(diff / 60000)
|
|
17
|
-
const hrs = Math.floor(diff / 3600000)
|
|
18
|
-
const days = Math.floor(diff / 86400000)
|
|
19
|
-
if (mins < 1) return 'just now'
|
|
20
|
-
if (mins < 60) return `${mins}m ago`
|
|
21
|
-
if (hrs < 24) return `${hrs}h ago`
|
|
22
|
-
return `${days}d ago`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
11
|
/* ── Summary Cards ─────────────────────────────────────────────── */
|
|
26
12
|
|
|
27
13
|
function TotalCard({ count }: { count: number }) {
|
|
@@ -201,7 +187,7 @@ export default function ActivityPage() {
|
|
|
201
187
|
}}
|
|
202
188
|
>
|
|
203
189
|
<Radio size={14} />
|
|
204
|
-
Open Live
|
|
190
|
+
Open Live Logs
|
|
205
191
|
</button>
|
|
206
192
|
|
|
207
193
|
<span style={{ fontSize: 'var(--text-caption1)', color: 'var(--text-tertiary)' }}>
|
|
@@ -4,10 +4,11 @@ import { getAgent } from '@/lib/agents'
|
|
|
4
4
|
import { validateChatMessages } from '@/lib/validation'
|
|
5
5
|
import { hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw } from '@/lib/anthropic'
|
|
6
6
|
import OpenAI from 'openai'
|
|
7
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
7
8
|
|
|
8
9
|
// Route through the OpenClaw gateway — no separate API key needed
|
|
9
10
|
const openai = new OpenAI({
|
|
10
|
-
baseURL:
|
|
11
|
+
baseURL: gatewayBaseUrl(),
|
|
11
12
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
12
13
|
})
|
|
13
14
|
|
|
@@ -2,9 +2,10 @@ export const runtime = 'nodejs'
|
|
|
2
2
|
|
|
3
3
|
import { getAgent } from '@/lib/agents'
|
|
4
4
|
import OpenAI from 'openai'
|
|
5
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
5
6
|
|
|
6
7
|
const openai = new OpenAI({
|
|
7
|
-
baseURL:
|
|
8
|
+
baseURL: gatewayBaseUrl(),
|
|
8
9
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
9
10
|
})
|
|
10
11
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { loadPipelines } from "@/lib/cron-pipelines.server"
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json(loadPipelines())
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
const workspacePath = process.env.WORKSPACE_PATH
|
|
12
|
+
if (!workspacePath) {
|
|
13
|
+
return NextResponse.json({ error: "WORKSPACE_PATH not set" }, { status: 500 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const body = await req.json()
|
|
17
|
+
|
|
18
|
+
// Validate: must be array
|
|
19
|
+
if (!Array.isArray(body)) {
|
|
20
|
+
return NextResponse.json({ error: "Must be an array" }, { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate each pipeline
|
|
24
|
+
for (let i = 0; i < body.length; i++) {
|
|
25
|
+
const p = body[i]
|
|
26
|
+
if (typeof p.name !== "string" || !p.name.trim()) {
|
|
27
|
+
return NextResponse.json({ error: `Pipeline ${i}: missing "name" string` }, { status: 400 })
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(p.edges)) {
|
|
30
|
+
return NextResponse.json({ error: `Pipeline "${p.name}": missing "edges" array` }, { status: 400 })
|
|
31
|
+
}
|
|
32
|
+
for (let j = 0; j < p.edges.length; j++) {
|
|
33
|
+
const e = p.edges[j]
|
|
34
|
+
if (typeof e.from !== "string" || typeof e.to !== "string" || typeof e.artifact !== "string") {
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{ error: `Pipeline "${p.name}", edge ${j}: requires "from", "to", and "artifact" strings` },
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Ensure clawport/ dir exists
|
|
44
|
+
const dir = join(workspacePath, "clawport")
|
|
45
|
+
if (!existsSync(dir)) {
|
|
46
|
+
mkdirSync(dir, { recursive: true })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write pipelines.json
|
|
50
|
+
writeFileSync(join(dir, "pipelines.json"), JSON.stringify(body, null, 2) + "\n", "utf-8")
|
|
51
|
+
|
|
52
|
+
return NextResponse.json({ ok: true })
|
|
53
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const runtime = 'nodejs'
|
|
2
2
|
|
|
3
3
|
import OpenAI from 'openai'
|
|
4
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
4
5
|
|
|
5
6
|
const openai = new OpenAI({
|
|
6
|
-
baseURL:
|
|
7
|
+
baseURL: gatewayBaseUrl(),
|
|
7
8
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
8
9
|
})
|
|
9
10
|
|
package/app/api/tts/route.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const runtime = 'nodejs'
|
|
2
2
|
|
|
3
3
|
import OpenAI from 'openai'
|
|
4
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
4
5
|
|
|
5
6
|
const openai = new OpenAI({
|
|
6
|
-
baseURL:
|
|
7
|
+
baseURL: gatewayBaseUrl(),
|
|
7
8
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
8
9
|
})
|
|
9
10
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { fetchClaudeCodeUsage } from '@/lib/claude-usage'
|
|
2
|
+
|
|
3
|
+
const POLL_INTERVAL_MS = 60 * 1000 // 60 seconds
|
|
4
|
+
const HEARTBEAT_INTERVAL_MS = 15 * 1000
|
|
5
|
+
const MAX_LIFETIME_MS = 30 * 60 * 1000 // 30 minutes
|
|
6
|
+
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const encoder = new TextEncoder()
|
|
9
|
+
|
|
10
|
+
const stream = new ReadableStream({
|
|
11
|
+
start(controller) {
|
|
12
|
+
let poll: ReturnType<typeof setInterval> | null = null
|
|
13
|
+
let heartbeat: ReturnType<typeof setInterval> | null = null
|
|
14
|
+
let lifetime: ReturnType<typeof setTimeout> | null = null
|
|
15
|
+
|
|
16
|
+
function send(data: string) {
|
|
17
|
+
try {
|
|
18
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
19
|
+
} catch { /* controller closed */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let lastGood: Awaited<ReturnType<typeof fetchClaudeCodeUsage>> = null
|
|
23
|
+
|
|
24
|
+
async function tick() {
|
|
25
|
+
try {
|
|
26
|
+
const usage = await fetchClaudeCodeUsage()
|
|
27
|
+
if (usage) lastGood = usage
|
|
28
|
+
send(JSON.stringify({ type: 'usage', data: usage ?? lastGood }))
|
|
29
|
+
} catch {
|
|
30
|
+
send(JSON.stringify({ type: 'usage', data: lastGood }))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Immediate first fetch
|
|
35
|
+
tick()
|
|
36
|
+
|
|
37
|
+
// Poll every 60s
|
|
38
|
+
poll = setInterval(tick, POLL_INTERVAL_MS)
|
|
39
|
+
|
|
40
|
+
// Heartbeat to prevent proxy timeouts
|
|
41
|
+
heartbeat = setInterval(() => {
|
|
42
|
+
try {
|
|
43
|
+
controller.enqueue(encoder.encode(`: heartbeat\n\n`))
|
|
44
|
+
} catch { /* closed */ }
|
|
45
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
46
|
+
|
|
47
|
+
// Max lifetime safety valve
|
|
48
|
+
lifetime = setTimeout(() => {
|
|
49
|
+
cleanup()
|
|
50
|
+
try { controller.close() } catch { /* already closed */ }
|
|
51
|
+
}, MAX_LIFETIME_MS)
|
|
52
|
+
|
|
53
|
+
// Cleanup on client disconnect
|
|
54
|
+
request.signal.addEventListener('abort', () => {
|
|
55
|
+
cleanup()
|
|
56
|
+
try { controller.close() } catch { /* already closed */ }
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
function cleanup() {
|
|
60
|
+
if (poll) { clearInterval(poll); poll = null }
|
|
61
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = null }
|
|
62
|
+
if (lifetime) { clearTimeout(lifetime); lifetime = null }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return new Response(stream, {
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'text/event-stream',
|
|
70
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
71
|
+
'Connection': 'keep-alive',
|
|
72
|
+
'X-Accel-Buffering': 'no',
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
package/app/crons/page.tsx
CHANGED
|
@@ -4,51 +4,14 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import type { Agent, CronJob, CronRun } from "@/lib/types";
|
|
6
6
|
import type { Pipeline } from "@/lib/cron-pipelines";
|
|
7
|
-
import { formatDuration } from "@/lib/cron-utils";
|
|
7
|
+
import { formatDuration, timeAgo, nextRunLabel } from "@/lib/cron-utils";
|
|
8
8
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
9
9
|
import { RefreshCw, BarChart3, Calendar, GitBranch, Copy, Check } from "lucide-react";
|
|
10
10
|
import { ErrorState } from "@/components/ErrorState";
|
|
11
11
|
import { WeeklySchedule } from "@/components/crons/WeeklySchedule";
|
|
12
12
|
import { PipelineGraph } from "@/components/crons/PipelineGraph";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function timeAgo(dateStr: string | null): string {
|
|
17
|
-
if (!dateStr) return "never";
|
|
18
|
-
const d = new Date(dateStr);
|
|
19
|
-
if (isNaN(d.getTime())) return "\u2014";
|
|
20
|
-
const diff = Date.now() - d.getTime();
|
|
21
|
-
const mins = Math.floor(diff / 60000);
|
|
22
|
-
const hrs = Math.floor(diff / 3600000);
|
|
23
|
-
const days = Math.floor(diff / 86400000);
|
|
24
|
-
if (diff < 0) {
|
|
25
|
-
const absDiff = Math.abs(diff);
|
|
26
|
-
const m = Math.floor(absDiff / 60000);
|
|
27
|
-
const h = Math.floor(absDiff / 3600000);
|
|
28
|
-
const dy = Math.floor(absDiff / 86400000);
|
|
29
|
-
if (m < 60) return `in ${m}m`;
|
|
30
|
-
if (h < 24) return `in ${h}h`;
|
|
31
|
-
return `in ${dy}d`;
|
|
32
|
-
}
|
|
33
|
-
if (mins < 1) return "just now";
|
|
34
|
-
if (mins < 60) return `${mins}m ago`;
|
|
35
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
36
|
-
return `${days}d ago`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function nextRunLabel(dateStr: string | null): string {
|
|
40
|
-
if (!dateStr) return "not scheduled";
|
|
41
|
-
const d = new Date(dateStr);
|
|
42
|
-
if (isNaN(d.getTime())) return "\u2014";
|
|
43
|
-
const diff = d.getTime() - Date.now();
|
|
44
|
-
if (diff < 0) return "overdue";
|
|
45
|
-
const mins = Math.floor(diff / 60000);
|
|
46
|
-
const hrs = Math.floor(diff / 3600000);
|
|
47
|
-
const days = Math.floor(diff / 86400000);
|
|
48
|
-
if (mins < 60) return `in ${mins}m`;
|
|
49
|
-
if (hrs < 24) return `in ${hrs}h`;
|
|
50
|
-
return `in ${days}d`;
|
|
51
|
-
}
|
|
13
|
+
import { PipelineDetailPanel } from "@/components/crons/PipelineDetailPanel";
|
|
14
|
+
import { PipelineWizard } from "@/components/crons/PipelineWizard";
|
|
52
15
|
|
|
53
16
|
/* ─── Types ─────────────────────────────────────────────────────── */
|
|
54
17
|
|
|
@@ -439,6 +402,8 @@ export default function CronsPage() {
|
|
|
439
402
|
const [error, setError] = useState<string | null>(null);
|
|
440
403
|
const [updatedAgo, setUpdatedAgo] = useState("just now");
|
|
441
404
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
405
|
+
const [wizardOpen, setWizardOpen] = useState(false);
|
|
406
|
+
const [selectedPipelineJob, setSelectedPipelineJob] = useState<string | null>(null);
|
|
442
407
|
|
|
443
408
|
const pillsRef = useRef<HTMLDivElement>(null);
|
|
444
409
|
|
|
@@ -862,11 +827,46 @@ export default function CronsPage() {
|
|
|
862
827
|
{tab === "schedule" && <WeeklySchedule crons={crons} />}
|
|
863
828
|
|
|
864
829
|
{/* ─── PIPELINES TAB ─────────────────────────────── */}
|
|
865
|
-
{tab === "pipelines" &&
|
|
830
|
+
{tab === "pipelines" && (
|
|
831
|
+
<PipelineGraph
|
|
832
|
+
crons={crons}
|
|
833
|
+
agents={agents}
|
|
834
|
+
pipelines={pipelines}
|
|
835
|
+
onSetupClick={() => setWizardOpen(true)}
|
|
836
|
+
onEditClick={() => setWizardOpen(true)}
|
|
837
|
+
onClearClick={() => {
|
|
838
|
+
fetch("/api/pipelines", {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: { "Content-Type": "application/json" },
|
|
841
|
+
body: JSON.stringify([]),
|
|
842
|
+
}).then(() => refresh())
|
|
843
|
+
}}
|
|
844
|
+
onJobSelect={setSelectedPipelineJob}
|
|
845
|
+
selectedJob={selectedPipelineJob}
|
|
846
|
+
/>
|
|
847
|
+
)}
|
|
866
848
|
</>
|
|
867
849
|
)}
|
|
868
850
|
</div>
|
|
869
851
|
|
|
852
|
+
<PipelineWizard
|
|
853
|
+
open={wizardOpen}
|
|
854
|
+
onOpenChange={setWizardOpen}
|
|
855
|
+
agents={agents}
|
|
856
|
+
crons={crons}
|
|
857
|
+
onSaved={refresh}
|
|
858
|
+
/>
|
|
859
|
+
|
|
860
|
+
{selectedPipelineJob && (
|
|
861
|
+
<PipelineDetailPanel
|
|
862
|
+
jobName={selectedPipelineJob}
|
|
863
|
+
crons={crons}
|
|
864
|
+
agents={agents}
|
|
865
|
+
pipelines={pipelines}
|
|
866
|
+
onClose={() => setSelectedPipelineJob(null)}
|
|
867
|
+
/>
|
|
868
|
+
)}
|
|
869
|
+
|
|
870
870
|
<style>{`
|
|
871
871
|
@media (max-width: 640px) {
|
|
872
872
|
.summary-cards-grid {
|
package/app/memory/page.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
BookOpen,
|
|
20
20
|
} from "lucide-react";
|
|
21
21
|
import { renderMarkdown, colorizeJson } from "@/lib/sanitize";
|
|
22
|
+
import { timeAgo } from "@/lib/cron-utils";
|
|
22
23
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
23
24
|
import { ErrorState } from "@/components/ErrorState";
|
|
24
25
|
|
|
@@ -35,17 +36,6 @@ const TABS: { key: Tab; label: string; Icon: typeof BarChart3 }[] = [
|
|
|
35
36
|
|
|
36
37
|
/* ─── Helpers ────────────────────────────────────────────────── */
|
|
37
38
|
|
|
38
|
-
function timeAgo(dateStr: string): string {
|
|
39
|
-
const diff = Date.now() - new Date(dateStr).getTime();
|
|
40
|
-
const mins = Math.floor(diff / 60000);
|
|
41
|
-
const hrs = Math.floor(diff / 3600000);
|
|
42
|
-
const days = Math.floor(diff / 86400000);
|
|
43
|
-
if (mins < 1) return "just now";
|
|
44
|
-
if (mins < 60) return `${mins}m ago`;
|
|
45
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
46
|
-
return `${days}d ago`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
function formatBytes(bytes: number): string {
|
|
50
40
|
if (bytes < 1024) return `${bytes}B`;
|
|
51
41
|
const kb = bytes / 1024;
|
package/app/providers.tsx
CHANGED
|
@@ -18,13 +18,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
18
18
|
setThemeState(t);
|
|
19
19
|
localStorage.setItem('clawport-theme', t);
|
|
20
20
|
const html = document.documentElement;
|
|
21
|
-
html.
|
|
22
|
-
if (t === 'system') {
|
|
23
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
24
|
-
html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
|
25
|
-
} else {
|
|
26
|
-
html.setAttribute('data-theme', t);
|
|
27
|
-
}
|
|
21
|
+
html.setAttribute('data-theme', t);
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
return (
|
package/bin/clawport.mjs
CHANGED
|
@@ -84,9 +84,13 @@ function run(cmd, args = []) {
|
|
|
84
84
|
child.on('close', (code) => process.exit(code ?? 0))
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function getGatewayPort() {
|
|
88
|
+
return parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10)
|
|
89
|
+
}
|
|
90
|
+
|
|
87
91
|
async function checkGateway() {
|
|
88
92
|
try {
|
|
89
|
-
const res = await fetch(
|
|
93
|
+
const res = await fetch(`http://127.0.0.1:${getGatewayPort()}/`, {
|
|
90
94
|
signal: AbortSignal.timeout(3000),
|
|
91
95
|
})
|
|
92
96
|
return res.ok || res.status > 0
|
|
@@ -187,10 +191,11 @@ async function cmdStatus() {
|
|
|
187
191
|
// Check gateway
|
|
188
192
|
const gatewayUp = await checkGateway()
|
|
189
193
|
|
|
194
|
+
const gwPort = getGatewayPort()
|
|
190
195
|
if (gatewayUp) {
|
|
191
|
-
console.log(` ${green('+')} Gateway reachable at ${dim(
|
|
196
|
+
console.log(` ${green('+')} Gateway reachable at ${dim(`localhost:${gwPort}`)}`)
|
|
192
197
|
} else {
|
|
193
|
-
console.log(` ${red('x')} Gateway not responding at ${dim(
|
|
198
|
+
console.log(` ${red('x')} Gateway not responding at ${dim(`localhost:${gwPort}`)}`)
|
|
194
199
|
console.log(` ${dim('Start it with: openclaw gateway run')}`)
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -254,7 +259,7 @@ async function cmdDoctor() {
|
|
|
254
259
|
|
|
255
260
|
// 4. Gateway reachable
|
|
256
261
|
const gatewayUp = await checkGateway()
|
|
257
|
-
check(gatewayUp,
|
|
262
|
+
check(gatewayUp, `Gateway reachable at localhost:${getGatewayPort()}`, 'Start it with: openclaw gateway run')
|
|
258
263
|
|
|
259
264
|
// 5. Configuration -- .env.local with required vars (package root or ~/.config/clawport-ui)
|
|
260
265
|
const envPath = getEnvLocalPath()
|