Commit Graph
156 Commits
Author SHA1 Message Date
DrSmoothl bfd00a14db fix(webui): restore debug heading hierarchy
Keep the top bar as the page's single primary heading and rename the content section to Developer Tools at the secondary level.

This removes duplicate page titles for screen readers while preserving the existing debug layout and feature grouping.
2026-07-16 15:36:09 +08:00
DrSmoothl 5a64acd807 feat(webui): improve overflowing tab navigation
Add edge fades and automatic active-tab reveal to shared horizontal tab lists, including revalidation when asynchronous labels or badges change width.

Give configuration tabs complete tab and panel semantics, keyboard navigation, focus treatment, and 40px desktop or 44px mobile targets without changing their visual hierarchy.
2026-07-16 15:35:59 +08:00
DrSmoothl 897f55c88b fix(webui): keep modal controls accessible
Increase shared dialog and sheet close targets to 40px on desktop and 44px on mobile while preserving the compact 16px icon.

Constrain dialogs to the viewport and scroll their body inside a fixed shell so long forms never move the close control off-screen.
2026-07-16 15:35:50 +08:00
DrSmoothl 9e34ebc0d4 fix(webui): harden responsive top bar layout
Rebalance the mobile top bar so the page title can shrink independently while navigation and account actions keep stable touch targets.

Align status and description visibility with the actual desktop breakpoint, hide only decorative content on narrow screens, and retain kiosk access on common phone widths without horizontal overflow.

Add explicit title context for truncated labels and preserve the existing desktop density.
2026-07-16 14:57:00 +08:00
DrSmoothl e842f6128b feat(webui): refine mobile theme controls
Give every theme option a reliable 44-pixel touch target on mobile while retaining the denser desktop presentation.

Adjust the active indicator to preserve visual balance at both sizes and add tactile press feedback that follows the application's reduced-motion settings.
2026-07-16 14:56:40 +08:00
DrSmoothl a4bf838bfd feat(webui): improve shared control ergonomics
Raise shared button and form-control defaults to provide clearer, more consistent interaction targets across the interface.

Guarantee 44-pixel touch targets and 16-pixel form text on narrow screens, preserve explicit desktop size overrides, and prevent mobile browser focus zoom.

Replace broad transitions with targeted press feedback, respect reduced-motion preferences, and allow callers to opt out without overriding their persistent transforms.
2026-07-16 14:56:30 +08:00
DrSmoothl ebd02c3f3d chore(release): v1.12.3 2026-07-15 21:31:59 +08:00
DrSmoothl bd9236e3fa feat(webui): expose transactional backup restore
Replace route-local restore writes with the core transaction and map preflight, rollback, and post-commit cleanup failures to precise HTTP responses. Treat only ENOENT as a missing export file and return transaction, restored, skipped, migrated, and restart facts.

Update the settings panel to describe the full sensitive configuration group, avoid restart guidance for empty plans, and reload settings after both successful and failed imports so the UI reflects committed disk state. Add HTTP regression coverage for export and restore error semantics.
2026-07-15 16:12:51 +08:00
DrSmoothl a3467a8d89 chore(release): v1.12.2 2026-07-14 07:57:08 +08:00
DrSmoothl c04864dd21 chore(release): v1.12.1 2026-07-10 14:00:40 +08:00
DrSmoothl a67c95407e feat(webui): show saved and applied config state
Surface whether a OneBot configuration was persisted, applied live, or left degraded. Keep failed adapters and apply errors visible in the config and overview pages instead of reporting a false success.
2026-07-10 13:49:49 +08:00
DrSmoothl 90f65654c7 feat(webui): animate log rows — entrance + scroll edge fade (#185)
Give the virtualized log viewer motion without hurting readability or choking at
high rate. A genuinely new tail line fades and rises in (200ms, ease-out-expo);
detection lives in the SSE flush (real arrivals only), is bounded by a 260ms
timer, and never replays when an old row scrolls back into view or when a filter
toggles. A firehose batch (more than 4 new lines in one flush) suppresses the
per-row entrance so a 100 line/s stream never strobes — the scroll flow and edge
fade carry it instead.

Scrolling gets a soft, scroll-position-aware edge fade: a boundary dissolves only
when there is actually hidden content past it (a ResizeObserver + scroll listener
keep it in sync), so a fully-scrolled top/bottom row is never dimmed. The entrance
animation lives on an inner element so it never collides with the virtual row's
own translateY positioning or perturbs measureElement. All of it is gated on the
motion switches: the entrance on reduce/disable-motion, the static edge fade on
the hard 关闭全部动效 only.
2026-07-07 23:27:46 +08:00
DrSmoothl b6fb5bd530 chore(release): v1.12.0 2026-07-07 22:38:02 +08:00
DrSmoothl cd04271b69 fix(webui): confirm before loading from the process-probe dialog
The 加载 button added in #189 injected directly, unlike the process-list
load which routes through the confirm dialog. Fire the same
setConfirm({kind:'load'}) flow so both entry points share one
confirmation step.
2026-07-07 22:36:19 +08:00
Airline233 9914db69e6 feat(webui): add load button to process probe dialog (#189) 2026-07-07 22:34:55 +08:00
DrSmoothl fc424ab740 perf(webui): stop login-waves rAF under reduced-motion; gate deliveries poll on visibility (#185)
login-waves ran its requestAnimationFrame loop unconditionally, ignoring
reduced-motion. Honour it (OS `prefers-reduced-motion` or the app's
`data-reduce-motion`): when reduced, paint a single static frame and never start
the loop; otherwise animate. React live to OS preference changes, and repaint the
static frame on resize while reduced so the field doesn't blank out. The loop is
torn down on unmount as before.

deliveries-widget polled /api/notifications/recent every 15s regardless of tab
visibility. Skip the poll while `document.hidden`, refresh immediately on
becoming visible, and add an in-flight guard so a slow request can't stack
overlapping polls.
2026-07-07 22:34:07 +08:00
DrSmoothl d984833914 perf(webui): share one ref-counted /api/logs/stream across subscribers (#185)
The log viewer and each dashboard "recent alerts" widget opened their own
EventSource to /api/logs/stream, so N alert tiles + the viewer meant N+1
identical connections (plus a short-lived-connection churn on navigation). Make
openLogStream ref-counted: the first subscriber opens a single shared stream,
every frame and status change fans out to all subscribers (each isolated with
try/catch and re-checked against the live Set so a mid-frame unsubscribe is
skipped), and the last to leave closes it. Late subscribers replay the current
status.

Hardening: a no-token open no longer caches a dead disposer (it would have
blocked every later subscriber from ever connecting); the shared URL bakes in the
token, so setToken() tears down and reopens on any token change (re-login would
otherwise retry a stale-token URL forever); the connecting window seeds
'reconnecting' so a late joiner's badge isn't wrongly 'closed'. Consumers are
unchanged — the dedup is transparent behind api.logs.stream.
2026-07-07 22:29:10 +08:00
DrSmoothl c49a5a0aa8 perf(webui): make debug feed pause truly close the SSE (#185)
The live-activity feed kept its /api/debug/stream EventSource open while paused
and merely dropped frames in the handler, so a paused feed still held a backend
connection slot and processed every event for nothing. Make `paused` a
dependency of the subscription effect: pausing now closes the stream and
resuming reopens it. Events during a pause were already discarded, and the row
buffer is preserved across toggles, so the only change is that a paused feed
stops consuming.

Deliberately NOT gated on tab visibility: debug-page keeps this component mounted
across tabs on purpose (the comment there notes actions fired from other tabs
must still land in the feed), so closing on tab switch would regress that. Only
an explicit pause severs the connection. StatusPill gained a `paused` state so a
deliberate pause reads as 已暂停 rather than 未连接.
2026-07-07 22:17:16 +08:00
DrSmoothl 4802dfe7aa perf(webui): virtualize the log viewer row list (#185)
The logs page rendered every filtered row, so a full 2000-line view mounted
~10k DOM nodes and cost ~66ms of layout per commit. Switch to a virtualized
list (@tanstack/react-virtual) that mounts only the visible window plus a small
overscan — a few dozen nodes regardless of how many lines the view holds.

Rows are dynamically measured (measureElement) so wrap mode's variable-height
lines need no fixed row-height assumption; the size cache is keyed by row id so
the maxLines front-drop doesn't mis-attribute heights across the index shift.
The column header moved just outside the scroll viewport so the virtual list is
the sole scroll content (no sticky-offset math). Auto-scroll now follows the
tail via scrollToIndex with a next-frame re-pin (freshly-appended rows carry the
size estimate until measured, which otherwise left wrap-mode tails short), and
re-runs on filter/level/wrap/maxLines changes, not just new logs. ScrollArea
gained an optional viewportRef so the virtualizer can drive off the real Radix
scroll element. The per-row entrance fade is dropped (rows mount/unmount on
scroll); divide-y became a per-row border.
2026-07-07 22:07:02 +08:00
DrSmoothl fd57eb4b6f fix(webui): scope message-segment drag to the grip handle
The whole segment row in the debug message builder was `draggable`, so an
HTML5 drag started on any pointer gesture inside it — including selecting
part of a pasted URL in a segment input, which dragged the card instead of
selecting text.

Move `draggable` (and onDragStart/onDragEnd) off the row onto the grip
handle only; the row keeps onDragOver/onDrop so it stays the drop target.
The body is now fully selectable/editable. The grip also moves up beside
the segment label, freeing the card's left edge. Nested forward-node
builders keep their stopPropagation drag boundary.
2026-07-07 15:48:52 +08:00
DrSmoothl d35e276730 chore(release): v1.11.4 2026-07-06 15:04:55 +08:00
DrSmoothl 0e6e2ec51d chore(release): v1.11.3 2026-07-05 14:26:10 +08:00
DrSmoothl efac547d6a feat(webui): redesign CPU & memory overview cards
CPU 使用率: replace the flat per-core bar row with an adaptive heatmap
grid — auto-fill square cells whose color maps to per-core load — so it
reads cleanly from a few cores up to many-core (64/128) hosts instead of
squashing into an unreadable strip.

内存使用: replace the single usage bar with a segmented
本进程 / 其他已用 / 空闲 breakdown bar plus a legend, surfacing the
SnowLuma process's own RSS footprint (systemInfo.runtime.rss) that the
card never exposed before.

Removed the now-unused bestGrid() helper. All motion uses transform
(scale) rather than width/height to avoid layout thrash.
2026-07-04 22:10:38 +08:00
DrSmoothl 6cb7f2c6d0 chore(release): v1.11.2 2026-07-04 13:39:00 +08:00
DrSmoothl f127e3dc59 fix(webui): meet WCAG AA for log-level text on light theme
The log-level accent colors (info/success/warn/error) are tuned as fills;
as small text on the light canvas they fell below WCAG AA (info 2.6:1,
success 2.4:1, warn 2.0:1, error 4.3:1, trace 2.5:1). Add dedicated
--log-* text tokens that are darkened in light mode (>=4.5:1) while
keeping the bright, already-passing values in dark mode, so the app-wide
accent is untouched. trace moves from /60 to /90 to clear AA yet stay the
faintest level. Applied to both the logs page and overview RecentAlerts.
2026-07-04 00:08:30 +08:00
DrSmoothl 7b93eb2716 fix(webui): treat ?token= URL param as the login password (#194)
abef797 misread the request and seeded the value as a bearer session token
(which the operator would have to dig out of localStorage). The intent is
simpler: `?token=<你的WebUI登录密码>` — the value is the plain WebUI login
password, so the link is trivially constructable from something the operator
already knows.

- Revert the client-constructor bearer seeding from abef797.
- AuthBoundary: when no stored token already authed us and the URL carries
  ?token=, run it through client.login() (the normal password path). A correct
  password lands on the home page; a wrong one falls through to the login page
  (server-verified, no bypass). The password is carried into the forced
  change-password gate, matching the interactive login flow.
- The param is consumed once and stripped from the URL (replaceState) before
  the login call — a password in the address bar leaks via history / logs /
  Referer.

typecheck 12 包干净。
2026-07-03 19:41:25 +08:00
DrSmoothl abef7973d9 feat(webui): auto-login via ?token= URL query param (#194)
登录页每次手输密码很繁琐。支持在 URL 带 `?token=<accessToken>` 直接免密进入:
API client 构造时读取该参数作为 bearer token,交由既有 /api/status 门校验——
有效则直达主页,无效/过期则照常落登录页(无安全降级,服务端仍校验)。

安全处理:token 读取后**立即从 URL 剥离**(history.replaceState)——凭据留在地址栏
会经浏览器历史 / 服务端访问日志 / Referer 头泄露。同时持久化到 tokenStore,
使随后刷新(URL 已无参数)仍保持登录。URL token 优先于已存 token。

只做 token(不做 password)——token 就是实际 bearer 凭据,无需额外登录往返、
也比明文密码更适合放链接。
2026-07-03 19:30:53 +08:00
DrSmoothl 3e99d3bb5a fix(webui): guard async polling against re-entrant overlap
三个 setInterval 轮询(列表 reconcile、systemInfo 快刷、6h 更新检查)调度的都是 async
tick,但 setInterval 不感知 Promise 完成——当某次请求慢于轮询间隔时,下一次 tick 会在
上一次未完成时又发起,导致同类请求并发堆叠、越积越慢。

每个 tick 加在途 running 互斥:上一次刷新还在飞就跳过本次,完成(try/finally,含出错)后
恢复调度。保留已有的 document.hidden 门控与变可见补刷。慢 tick 只是被跳过、完成后自愈,
不会永久停摆。

(引 non-overlapping-poll 工具 + 给 webui 新增测试基建对 3 个轮询点属过度工程,故就地加
互斥。)
2026-07-02 21:04:35 +08:00
DrSmoothl c85c510c0b perf(webui): trim debug-view rendering cost (json-tree, activity, tester)
- json-tree:折叠节点不再枚举子项(改用 count),整棵树的 JSON.stringify 从每次渲染
  改为点击复制时才算。
- live-activity:搜索命中串在入队时预生成,过滤不再对每行 JSON.stringify(300 行 ×
  每次击键)。
- action-tester:流式帧封顶 2000,长/失控流不再无限增长数组与 DOM。

均行为保持(或纯改善),对抗性 Agent 逐条核验无回归。
2026-07-02 15:02:13 +08:00
DrSmoothl 99547e847e perf(webui): gate list/system polling on tab visibility
两个兜底轮询(列表 reconcile、systemInfo 快轮询)的 tick 在 document.hidden 时跳过,
并监听 visibilitychange 在切回时立即补一次。后台标签页不再空转 fetch;实时状态本就
走 SSE(未门控),切回时立即刷新无空态闪烁。
2026-07-02 15:02:13 +08:00
DrSmoothl d17381a221 perf(webui): batch log SSE into ~80ms flushes instead of per-line setState
日志页每收到一条日志就 setLogs(prev => [...prev.filter(id≠), entry].slice(-cap)),
高日志速率下是每条一次 O(n) 过滤+复制 + 一次 React commit。改为入队到 pending、
每 ~80ms 批量 flush:一次 O(n) 合并(Map 去重、last wins)+ 一次 commit。去重(重连
重放同 id)与 maxLines 上限语义保持不变,卸载时 dispose + 清 timer。
2026-07-02 15:02:13 +08:00
DrSmoothl 72aedbc612 chore(release): v1.11.1 2026-07-01 22:15:01 +08:00
DrSmoothl c822c5dab6 refactor(webui): extract useFlashMessage hook for settings panels
三个 settings 面板各手写同一套 flash-message 状态机(msg state + setTimeout 自动
消失 + 卸载清 timer)。global-config / notifications 是完整版(逐字相同);
system-panel 是退化版(无 timer ref、无 cleanup、rapid flash 累积 timer、卸载后可能
stale setMsg)。抽 useFlashMessage(durationMs=2400) → { msg, flash, setMsg }。

- global-config / notifications:迁移后行为不变(2400ms;flash 瞬时 + setMsg 持久
  错误保留)。
- system-panel:保留 4000ms(useFlashMessage(4000)),顺带补上 prior-timer 取消 +
  卸载 cleanup(修 timer 泄漏,现代 React 里无害的小改善)。

webui typecheck(tsc -b) + vite build 干净;对抗性 Agent 逐行核对确认 hook 等价、
两面板行为不变、无依赖/import 回归。
2026-07-01 20:03:58 +08:00
DrSmoothl 298d174640 refactor(webui): extract openSseChannel for the three EventSource openers
stateStream / openLogStream / openDebugStream 三个 EventSource opener 近乎逐字重复
(token 守卫 → EventSource → onopen/onerror → onmessage try-parse-catch → close
disposer),token/reconnect/畸形帧-跳过 语义各写一遍、易漂。

- 新增私有 openSseChannel<T>(path, onMessage, onStatus?) 收敛骨架;各 opener 塌成
  一行调用,consumer 逻辑作为 onMessage 传入(保持「回调在 try 内、抛错当畸形帧
  吞掉」语义)。logs 的控制帧过滤(`'type' in parsed`)留在其 onMessage 回调里。
- StreamStatus 提到顶部 import,删掉 openDebugStream 的 inline import。

行为严格不变、对外签名不变:webui typecheck(tsc -b) + vite build 干净;对抗性 Agent
逐字对比 HEAD 确认三 opener 等价、onMessage-在-try-内 语义保留、无漏并入。
2026-07-01 18:58:22 +08:00
囧囧JOJO f424454422 fix(webui): align debug account picker styling (#181) 2026-07-01 14:43:29 +08:00
DrSmoothl 48e2777bc3 chore(release): v1.11.0 2026-06-30 20:38:37 +08:00
DrSmoothl 8fc5917b3b fix(webui): align message composer/builder (Apple-HIG)
Two alignment fixes in the 消息构建 tab:
- MessageBuilder is now a bounded "compose canvas" (always bordered) so the
  empty hint reads as an intentional empty state inside a defined input
  region, instead of a stray centered line floating in the left-aligned
  form.
- The 账号 / 目标 label rows are pinned to a fixed h-8 (== the 群聊/私聊
  segmented control's height) so both labels and the selectors beneath them
  share a baseline — min-h-7 wasn't enough since the segmented is ~32px.
2026-06-30 18:57:54 +08:00
DrSmoothl d5fbd6fa06 feat(webui): multi-tab debug console + embedded API browser
Phase 6 of the debug-console expansion — the four pieces become one
console: 测试台 / 消息构建 / 实时活动 / API 浏览器, behind an Apple-HIG
sliding-indicator tab bar.

- ApiBrowser: a searchable, category-grouped reference for every
  declarative action (~520), straight from /api/debug/actions — params /
  returns / invariants / role + readOnly + stream badges. Each row's 试一下
  prefills the tester and switches to it (ActionTester gains a
  presetAction prop keyed by nonce).
- Tester / compose / activity stay MOUNTED and toggle via CSS, so the
  live feed's SSE never drops — it keeps capturing events fired while you
  invoke from the tester tab (the invoke-then-observe loop) — and
  in-progress params/messages survive a peek at another tab. The API
  browser, heavy and stateless, mounts on demand.

Adversarially reviewed: no crash / leak / mis-trigger; the review caught
that conditional-mounting silently dropped cross-tab live events and
discarded in-progress work — fixed by keeping the stateful panels mounted.
2026-06-30 17:40:04 +08:00
DrSmoothl 383ddb0a5d feat(webui): visual message builder + composer (nested forward)
Phase 5 of the debug-console expansion. A drag-reorderable segment
builder so you never hand-write a OneBot message array: common segments
first-class (text / @ / image / face / reply / record / video), advanced
folded (file / mface / poke / json / xml / markdown / forward-node), with
`node` nesting a full builder (merged forward, depth-capped). @ uses the
group member picker via the sibling group context; media via FileSource.
Each segment carries a stable `_id` key so editing a field never remounts
the row.

- MessageBuilder + toOneBot() serialise to the exact OneBot v11 shapes
  SnowLuma's parser reads (verified field-by-field).
- MessageComposer (the 消息构建 tab): pick target, build, preview, send —
  routing through send_*_forward_msg when the message has nodes, else
  send_*_msg. Refuses a mixed node/non-node forward (the backend would
  silently drop the stray segments) and validates incomplete segments
  before sending.
- ParamField renders the builder for message-type params in the tester
  (raw-text escape hatch kept), emitting the serialized array as the
  string value.

Adversarially reviewed: serialization contract and React key/focus both
verified clean (no _id leakage); fixed the mixed-forward silent-drop,
added pre-send segment validation, and made the drag boundary explicit.
2026-06-30 17:31:51 +08:00
DrSmoothl 11866caa2f feat(webui): role-driven action tester with streaming + history
Phase 4 of the debug-console expansion — the tester no longer makes you
hand-write JSON. Each param renders the right smart control from its
semantic role: group/friend/member pickers (member linked to the sibling
group_id), FileSource for media, FaceGrid, datetime for timestamps,
switch for bool, select for enum — with the raw-JSON escape hatch kept.

- Read-only actions badge green, side-effecting amber; a name-pattern set
  of destructive actions (kick/ban/recall/delete/…) confirms first.
- Stream API actions invoke over /api/debug/invoke-stream and render
  frames live, tracked in the app-level task registry with progress; a
  clean success requires the terminal response frame, and a failed frame
  shows its reason (not just the {type:'error'} marker).
- Every call lands in capped localStorage history with one-click replay;
  responses render in a collapsible JsonTree.
- Extracted LiveActivity (filter / search / export) and the Segmented
  control into their own components; debug-page is now a thin host.

Adversarially reviewed: stream terminal-frame judging, member/group
linkage, and contract shapes verified; fixed the failed-stream-frame
reason being hidden, a latent required-enum controlled-select trap, stale
member value on group change, and several low UX edges.
2026-06-30 17:17:15 +08:00
DrSmoothl c5611ed622 feat(webui): debug-console shared primitives, data layer, task manager
Phase 3 of the debug-console expansion — the Apple-HIG foundation the
new tabs build on (reusing the existing ui kit; new controls match its
idiom, no browser-native widgets).

API client + types:
- debug.invokeStream() reads /api/debug/invoke-stream via fetch + a body
  reader, parsing data:<json> SSE frames (header auth, cancellable).
- debug.upload() streams a file via XHR for real upload progress.
- DebugActionParam.role, DebugActionDoc.stream/invariants, FieldRole,
  DebugStreamFrame, DebugUploadResult.

Primitives:
- Tabs (sliding indicator + roving tabindex), Picker (searchable,
  windowed combobox with a raw-value escape hatch), JsonTree (collapsible
  response viewer, image-url preview, copy w/ http fallback, node cap),
  FileSource (URL / server-path / browser-upload tri-state), FaceGrid.

Infra:
- DebugTaskProvider + TaskBadge: app-level registry of in-flight upload/
  stream tasks so they survive in-app tab switches, with a global
  progress chip and a beforeunload guard; eviction never drops a running
  task. Wired into App.
- use-debug-contacts: lazy friend/group/member data layer, shared module
  cache + localStorage (version-stamped, avatars stripped to dodge quota)
  + TTL + request-id race guard, degrading to raw input on error.

Adversarially reviewed (no automated component tests): virtualization
math and SSE framing verified correct; fixed the stale-contacts-on-switch
member-selection hazard, the quota-kills-persistence issue, and several
low UX edges (active-row clamp, filter scroll reset, copy fallback).
2026-06-30 16:53:43 +08:00
DrSmoothl 5dc3ec88d4 fix(webui): smooth sidebar collapse — no reflow, centered icons, pin button
The hover-rail flipped its content to a collapsed layout instantly while the
width animated, so on every expand/collapse icons jumped toward the top-left and
labels flashed in, wrapped super-tall, then settled — and the collapsed rail
leaked the first glyph of every label plus hard-clipped highlight bars.

Lay the rail content out at a fixed width and let the aside clip it: nothing
reflows during the width animation. Icons live in a fixed centered column (never
move); labels stay mounted with nowrap and just fade out when collapsed (no
wrap, no leak); the active pill and update banner render their background as an
absolute layer that becomes a centered squircle on the rail instead of a
hard-clipped bar.

Adds the promised pin toggle to the sidebar footer (desktop) — mirrors the
「钉住侧栏展开」 appearance setting: pinned keeps the rail open, unpinned returns
to hover-to-peek.
2026-06-28 23:29:25 +08:00
DrSmoothl 7ec74f2928 chore(release): v1.10.4 2026-06-28 22:48:00 +08:00
DrSmoothl 2b15bc6710 feat(webui): group Settings sidebar into 控制台 / SnowLuma / 平台
The flat tab strip mixed three domains — the dashboard itself, the all-accounts
SnowLuma protocol runtime, and the host/platform. Regroup into 3 labelled
sections (macOS System-Settings idiom): section headers show on the desktop
rail; the mobile strip stays a flat wrap via display:contents. Each group is a
role=group + aria-labelledby; the nav gains an aria-label.

This gives '全局配置' (and future global protocol knobs) a principled home and
ends the mixing. Phase 4 (final) of the global-settings split.
2026-06-28 22:25:39 +08:00
DrSmoothl 196f98e97c feat(webui): global-config settings tab + /api/global-config
Adds a '全局配置' Settings tab that edits config/snowluma.json — the rkey
fallback-server list and the deployment-wide musicSignUrl — backed by a new
bearer-gated GET/POST /api/global-config (load/saveGlobalSettings, then
oneBotManager.reloadGlobalSettings to hot-apply to live instances).

The panel reuses the established settings-panel idiom (Input/Button/Label,
debounced auto-save, flash); rkey servers are an inline add/remove list with
client-side http(s) validation hints. It deliberately does not reconcile inputs
from the server's normalized response (would erase a half-typed URL); a
load-failure shows an error+retry, and the save toast says '无效项已忽略' when
the server would drop an entry.

Removes the now-dead per-UIN musicSignUrl form (moved to global in Phase 2).
Phase 3 of the global-settings split (nav grouping is Phase 4).
2026-06-28 22:16:07 +08:00
DrSmoothl 659970077a refactor(onebot): move rkey fallback from per-UIN to a global config store
The rkey fallback endpoint is deployment infra (one self-hosted server serves
all accounts; fallback rkeys are fungible), so it doesn't belong in per-UIN
onebot_<uin>.json. Introduce config/snowluma.json — a generic home for small
all-accounts SnowLuma settings — and move rkey.fallbackServers there.

- New onebot/global-config.ts: load/save/normalize with section-merge + atomic
  write; http(s)+host URL validation; default [] (off). Lives in onebot (not
  core) because the onebot manager constructs instances and must not depend on
  core.
- OneBotInstance takes globalSettings; manager loads it on session start and
  exposes reloadGlobalSettings() to hot-push edits to every live instance via
  applyGlobalSettings → RKeyCache.setFallbackServers.
- Remove rkey from per-UIN OneBotConfig + the WebUI per-UIN round-trip (never
  released, so no migration). Register snowluma.json in the backup allowlist.

Reworks the per-UIN rkey from 12b0190. Phase 1 of the global-settings split.
2026-06-28 20:52:07 +08:00
DrSmoothl 12b01907bc feat(onebot): opt-in remote rkey fallback server (#156)
On accounts where the native OIDB 0x9067_202 rkey fetch persistently returns
nothing, every image was served as a bare multimedia.nt.qq.com.cn URL the CDN
rejects with invalid rkey — SnowLuma had no fallback, unlike NapCat.

Add config.rkey.fallbackServers (default []: OFF; no third-party server is
contacted unless the operator configures one). When the native fetch leaves the
cache without a usable image rkey, each configured HTTP endpoint is tried in
turn for { group_rkey, private_rkey, expired_time } (NapCat rkey-server format;
an OneBot { retcode, data } wrapper is also accepted), populating the group/
private image rkey types. Values keep any leading &rkey=/?rkey= since
resolveImageUrl strips before re-appending.

WebUI has no form yet but round-trips rkey.fallbackServers verbatim so a hand-
edited value survives an unrelated config save. Part B of #156 (cooldown is A).
2026-06-28 01:37:58 +08:00
DrSmoothl 5eaac0870c feat(webui): scale notification-channel opt-in to large lists
The per-account 上下线通知渠道 picker rendered every global channel as a
flat toggle row, so 40+ channels produced an unbounded, messy wall inside
通用设置. Rework it into a collapsible control:

- collapsed: one-line summary — selected count badge + selected channels
  as removable chips (cap 8, then "+N 更多")
- expanded: description + name/url search filter + 全选/清空 bulk actions
  + a height-bounded ScrollArea list (max-h-64) so it never blows up
- globally-disabled channels stay locked off; data flow unchanged

Reuses Badge / ScrollArea / ToggleSwitch / Input.
2026-06-28 01:23:12 +08:00
DrSmoothl 04591517fa feat(webui): redesign adapter edit dialog (Apple-HIG) + custom dropdown
Rework the OneBot adapter create/edit modal into an iOS grouped-list
layout matching the debug-page language: captioned soft-card sections,
a per-kind icon chip, settings rows, and staggered motion entrance.

- reportSelfMessage: segmented control -> shared ToggleSwitch
- messageFormat (+ WS role): segmented/native select -> new custom
  DropdownSelect (accessible listbox, keyboard nav, spring popover)
- enable toggle promoted to a prominent top card
- new components/ui/dropdown-select.tsx (zero new deps, hand-rolled)

Presentation-only; draft/validation/data flow unchanged.
2026-06-28 01:14:37 +08:00
DrSmoothl 351db573d8 chore(release): v1.10.3 2026-06-26 19:49:41 +08:00