feat: rebuild warm operations workbench

This commit is contained in:
Codex
2026-07-30 14:49:33 +08:00
parent 25c853dea8
commit 570edf6bb2
88 changed files with 13413 additions and 2250 deletions
+81 -76
View File
@@ -1,4 +1,5 @@
import { useEffect, useState, type FormEvent } from 'react';
import { Button, Card, Title } from 'animal-island-ui';
import {
createInstanceApiDataSource,
@@ -146,7 +147,9 @@ export function InstanceEditor({
return (
<section className="instance-editor">
<h1>{mode === 'create' ? '添加实例' : '实例设置'}</h1>
<h1>
<Title color="app-green">{mode === 'create' ? '添加实例' : '实例设置'}</Title>
</h1>
{error ? (
<p role="alert" className="state-panel state-error">
{error}
@@ -157,82 +160,84 @@ export function InstanceEditor({
{status}
</p>
) : null}
<form onSubmit={submit}>
<label>
名称
<input required value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
源地址
<input
required
type="url"
value={origin}
onChange={(event) => setOrigin(event.target.value)}
/>
</label>
<label>
标签
<input
value={tags}
onChange={(event) => setTags(event.target.value)}
aria-describedby="tags-help"
/>
</label>
<small id="tags-help">使用英文逗号分隔标签</small>
<label>
认证方式
<select
value={authMethod}
onChange={(event) => setAuthMethod(event.target.value as 'none' | 'password')}
>
<option value="none">无</option>
<option value="password">密码</option>
</select>
</label>
{authMethod === 'password' ? (
<>
{mode === 'edit' ? (
<label>
密码操作
<select
value={passwordAction}
onChange={(event) => {
setPasswordAction(event.target.value as PasswordAction);
setPassword('');
}}
>
<option value="preserve">保留已保存的密码</option>
<option value="set">设置新密码</option>
<option value="clear">清除已保存的密码</option>
</select>
</label>
) : null}
{mode === 'create' || passwordAction === 'set' ? (
<label>
密码
<input
required
type="password"
autoComplete="new-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
</label>
) : null}
</>
) : null}
<div className="form-actions">
<button disabled={busy} type="submit">
{mode === 'create' ? '添加实例' : '保存更改'}
</button>
{mode === 'edit' ? (
<button disabled={busy} type="button" onClick={() => void testConnection()}>
测试连接
</button>
<Card pattern="default" className="editor-card">
<form onSubmit={submit}>
<label>
名称
<input required value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
源地址
<input
required
type="url"
value={origin}
onChange={(event) => setOrigin(event.target.value)}
/>
</label>
<label>
标签
<input
value={tags}
onChange={(event) => setTags(event.target.value)}
aria-describedby="tags-help"
/>
</label>
<small id="tags-help">使用英文逗号分隔标签</small>
<label>
认证方式
<select
value={authMethod}
onChange={(event) => setAuthMethod(event.target.value as 'none' | 'password')}
>
<option value="none">无</option>
<option value="password">密码</option>
</select>
</label>
{authMethod === 'password' ? (
<>
{mode === 'edit' ? (
<label>
密码操作
<select
value={passwordAction}
onChange={(event) => {
setPasswordAction(event.target.value as PasswordAction);
setPassword('');
}}
>
<option value="preserve">保留已保存的密码</option>
<option value="set">设置新密码</option>
<option value="clear">清除已保存的密码</option>
</select>
</label>
) : null}
{mode === 'create' || passwordAction === 'set' ? (
<label>
密码
<input
required
type="password"
autoComplete="new-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
</label>
) : null}
</>
) : null}
</div>
</form>
<div className="form-actions">
<Button disabled={busy} loading={busy} htmlType="submit" type="primary">
{mode === 'create' ? '添加实例' : '保存更改'}
</Button>
{mode === 'edit' ? (
<Button disabled={busy} htmlType="button" onClick={() => void testConnection()}>
测试连接
</Button>
) : null}
</div>
</form>
</Card>
{mode === 'edit' ? (
<section className="danger-zone" aria-labelledby="danger-heading">
<h2 id="danger-heading">危险操作区</h2>
+32 -6
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Tag, Title } from 'animal-island-ui';
import type { InstanceContext, InstanceModule } from '../app-shell.js';
import { canonicalHttpOrigin } from '../fleet/fleet-page.js';
@@ -124,7 +125,9 @@ export function InstanceDetail({
if (!ownsRoute) {
return (
<section>
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
<h1>
<Title>{INSTANCE_MODULE_LABELS[module]}</Title>
</h1>
<p>此路由缺少实例上下文。</p>
</section>
);
@@ -142,16 +145,35 @@ export function InstanceDetail({
</a>
<div className="instance-identity">
<div>
<h1>{instance.name}</h1>
<h1>
<Title color="app-teal">{instance.name}</Title>
</h1>
<code>{instance.id}</code>
</div>
<div className="instance-context-badges">
{instance.status !== 'unknown' ? <span>{displayStatus(instance.status)}</span> : null}
{instance.status !== 'unknown' ? (
<Tag
size="small"
color={
instance.status === 'online'
? 'app-teal'
: instance.status === 'offline'
? 'app-red'
: 'app-yellow'
}
>
{displayStatus(instance.status)}
</Tag>
) : null}
{instance.authentication !== 'unknown' ? (
<span>{AUTH_LABELS[instance.authentication]}</span>
<Tag size="small" color="app-yellow">
{AUTH_LABELS[instance.authentication]}
</Tag>
) : null}
{instance.freshness !== 'unknown' ? (
<span>{FRESHNESS_LABELS[instance.freshness]}</span>
<Tag size="small" color="app-blue">
{FRESHNESS_LABELS[instance.freshness]}
</Tag>
) : null}
</div>
</div>
@@ -185,7 +207,11 @@ export function InstanceDetail({
</nav>
</header>
<div className="instance-module-detail">
<h2>{INSTANCE_MODULE_LABELS[module]}</h2>
<h2>
<Title size="small" color="app-green">
{INSTANCE_MODULE_LABELS[module]}
</Title>
</h2>
{loading ? <p role="status">正在加载能力…</p> : null}
{loadError ? <p role="alert">能力不可用:{loadError}</p> : null}
{!loading && canOpen(module, activeCapability) ? (
@@ -16,7 +16,7 @@ export function createMessagesApiDataSource(options: Options = {}): MessagesData
async load(instanceId, signal): Promise<MessagesSnapshot> {
const response = await fetcher(
`/api/v1/instances/${encodeURIComponent(instanceId)}/messages?limit=50&offset=0`,
{ headers: { accept: 'application/json' }, signal },
{ headers: { accept: 'application/json' }, credentials: 'same-origin', signal },
);
if (!response.ok) throw new Error('message load failed');
const root = record(await response.json());
@@ -50,6 +50,7 @@ export function createMessagesApiDataSource(options: Options = {}): MessagesData
{
method: 'POST',
headers: { accept: 'application/json', 'content-type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(input),
},
);
+2 -2
View File
@@ -243,12 +243,12 @@ export function MessagesModule({ instance, dataSource, refreshSignal }: Messages
name="content"
value={content}
onChange={(event) => setContent(event.target.value)}
maxLength={1600}
maxLength={2000}
required
/>
</label>
<div className="composer-footer">
<small>{content.length} / 1600 字符</small>
<small>{content.length} / 2000 字符</small>
<button type="submit" disabled={sending || !content.trim()}>
{sending ? '正在发送…' : '发送短信'}
</button>
+16 -6
View File
@@ -1,10 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
import {
createOperationClient,
type OperationClient,
} from '../operations/operation-client.js';
import { createOperationClient, type OperationClient } from '../operations/operation-client.js';
import { safeUiError } from '../ui/locale.js';
export type OverviewFieldValue = string | number | boolean | null;
@@ -51,11 +48,22 @@ const SECTIONS = [
const FIELD_LABELS: Readonly<Record<string, string>> = {
Model: '型号',
Manufacturer: '制造商',
IMEI: 'IMEI',
Version: '固件版本',
Uptime: '运行时间',
Slots: '卡槽数',
Active: '活跃数',
Operator: '运营商',
Technology: '接入制式',
Signal: '信号强度',
MCC: 'MCC',
MNC: 'MNC',
Registration: '注册状态',
IPv4: 'IPv4 地址',
IPv6: 'IPv6 地址',
Download: '下行速率',
Upload: '上行速率',
'Messages today': '今日消息数',
Calls: '通话数',
Usage: '使用率',
@@ -214,7 +222,8 @@ export function OverviewSystemPage({
return;
setActionState({ busy: true, message: `正在${op.title}…` });
try {
await client.list({ pageSize: 100 });
// Avoid pageSize=100 first-page truncation for late-sorted operation ids.
await client.list({ search: op.operationId, pageSize: 100 });
const prepared = await client.prepare({
operationId: op.operationId,
targets: [{ instanceId: instance.id, revision: targetRevision }],
@@ -235,7 +244,8 @@ export function OverviewSystemPage({
const job = await client.execute(prepared.id);
setActionState({
busy: false,
message: job.status === 'succeeded' ? `${op.title}已提交成功。` : `${op.title}结果未知或失败。`,
message:
job.status === 'succeeded' ? `${op.title}已提交成功。` : `${op.title}结果未知或失败。`,
...(job.status === 'succeeded' ? {} : { error: `${op.title}结果未知或失败。` }),
});
} catch (error) {