mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 12:10:46 +08:00
Fix KB settings badge stuck on unconfigured after bind (0.1.38).
Reload Host kbRoot after browse/save so the status badge tracks the live package instead of a stale snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0f31a6648a
commit
1b4c402723
9 changed files with 261 additions and 50 deletions
|
|
@ -218,7 +218,9 @@ export class CardForm<T> {
|
|||
|
||||
private async store(field: string, value: unknown): Promise<boolean> {
|
||||
await this.scope.set(field, value)
|
||||
return this.userLayer()?.[field] === value
|
||||
// `settings.mutate` can settle without throwing on a refused write; confirm
|
||||
// via the resolved section value (user layer alone can lag or omit keys).
|
||||
return this.sectionValue(field) === value || this.userLayer()?.[field] === value
|
||||
}
|
||||
|
||||
private stage(field: string, edit: StagedEdit): void {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from './alarm-push-status-view.ts'
|
||||
import {
|
||||
fetchKbStatus,
|
||||
reloadKbStatus,
|
||||
resolveKbPath,
|
||||
type KbSnapshot,
|
||||
} from './kb-status-view.ts'
|
||||
|
|
@ -155,7 +156,9 @@ export class NetxopsCardController {
|
|||
private kbDirectoryPicker: DirectoryPickerRemote | undefined
|
||||
private kbBrowseInFlight = false
|
||||
private kbStatusInFlight = false
|
||||
private kbReloadInFlight = false
|
||||
private kbUiError: string | null = null
|
||||
private lastSyncedKbRoot: string | undefined = undefined
|
||||
private imDeliveryCatalog: ImDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }
|
||||
private sessionsExportStatus: SessionsExportStatus | null = null
|
||||
private sessionsExportBusy = false
|
||||
|
|
@ -199,6 +202,7 @@ export class NetxopsCardController {
|
|||
scope.subscribe(() => {
|
||||
void this.readCredential()
|
||||
void this.refreshKbPreview()
|
||||
void this.syncKbRootFromSettings()
|
||||
})
|
||||
void this.readCredential()
|
||||
}
|
||||
|
|
@ -246,7 +250,7 @@ export class NetxopsCardController {
|
|||
}
|
||||
this.startStatusPoll()
|
||||
void this.refreshAlarmPushStatus()
|
||||
void this.refreshKbStatus()
|
||||
void this.reloadKbFromHost()
|
||||
void this.refreshKbPreview()
|
||||
void this.refreshImDeliveryCatalog()
|
||||
void this.refreshSessionsExportStatus()
|
||||
|
|
@ -364,18 +368,7 @@ export class NetxopsCardController {
|
|||
this.kbStatusInFlight = true
|
||||
try {
|
||||
const next = await fetchKbStatus(call)
|
||||
const prev = this.kbStatus
|
||||
if (
|
||||
prev
|
||||
&& prev.status === next.status
|
||||
&& prev.realRoot === next.realRoot
|
||||
&& prev.operatorName === next.operatorName
|
||||
&& prev.country === next.country
|
||||
&& prev.version === next.version
|
||||
&& prev.errorMessage === next.errorMessage
|
||||
) return
|
||||
this.kbStatus = next
|
||||
this.store.set(this.projection())
|
||||
this.applyKbSnapshot(next)
|
||||
} catch {
|
||||
// Keep last good snapshot; next poll retries.
|
||||
} finally {
|
||||
|
|
@ -383,6 +376,50 @@ export class NetxopsCardController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask Host to re-resolve kbRoot from live settings and republish.
|
||||
* Prefer this after browse/save so the badge cannot stick on a stale snapshot.
|
||||
*/
|
||||
private async reloadKbFromHost(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined || this.kbReloadInFlight) return
|
||||
this.kbReloadInFlight = true
|
||||
try {
|
||||
const next = await reloadKbStatus(call)
|
||||
this.applyKbSnapshot(next)
|
||||
this.lastSyncedKbRoot = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
} catch {
|
||||
// Fall back to published status poll.
|
||||
void this.refreshKbStatus()
|
||||
} finally {
|
||||
this.kbReloadInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
private applyKbSnapshot(next: KbSnapshot): void {
|
||||
const prev = this.kbStatus
|
||||
if (
|
||||
prev
|
||||
&& prev.status === next.status
|
||||
&& prev.realRoot === next.realRoot
|
||||
&& prev.operatorName === next.operatorName
|
||||
&& prev.country === next.country
|
||||
&& prev.version === next.version
|
||||
&& prev.errorMessage === next.errorMessage
|
||||
) return
|
||||
this.kbStatus = next
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
|
||||
/** When settings document kbRoot changes, force Host reload (covers footer Save / Reset). */
|
||||
private async syncKbRootFromSettings(): Promise<void> {
|
||||
if (this.rpcCall === undefined) return
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
if (saved === this.lastSyncedKbRoot) return
|
||||
this.lastSyncedKbRoot = saved
|
||||
await this.reloadKbFromHost()
|
||||
}
|
||||
|
||||
private async refreshKbPreview(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined) return
|
||||
|
|
@ -416,6 +453,38 @@ export class NetxopsCardController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist kbRoot then force Host reload so the badge matches the bound package.
|
||||
*/
|
||||
private async commitKbRoot(path: string): Promise<void> {
|
||||
const trimmed = path.trim()
|
||||
this.kbUiError = null
|
||||
this.store.set(this.projection())
|
||||
await this.form.save()
|
||||
const shell = this.form.shell()
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
if (shell.failed || saved !== trimmed) {
|
||||
this.kbUiError = saved === trimmed
|
||||
? 'kbRoot save failed'
|
||||
: `kbRoot save did not land (saved="${saved || '(empty)'}")`
|
||||
this.store.set(this.projection())
|
||||
const call = this.rpcCall
|
||||
if (call !== undefined && trimmed !== '') {
|
||||
try {
|
||||
this.kbPreview = await resolveKbPath(call, trimmed)
|
||||
this.store.set(this.projection())
|
||||
} catch {
|
||||
// keep error banner
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
this.kbPreview = null
|
||||
this.lastSyncedKbRoot = undefined
|
||||
await this.reloadKbFromHost()
|
||||
void this.refreshKbPreview()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Host OS directory chooser (or /api directoryPicker/pick fallback)
|
||||
* and write the path into kbRoot.
|
||||
|
|
@ -428,12 +497,7 @@ export class NetxopsCardController {
|
|||
|
||||
const applyPath = (path: string): void => {
|
||||
this.form.actions().edit('kbRoot', path)
|
||||
void this.refreshKbPreview()
|
||||
// Selecting a folder should land immediately — don't require a separate Save.
|
||||
void this.form.save().then(() => {
|
||||
void this.refreshKbStatus()
|
||||
void this.refreshKbPreview()
|
||||
})
|
||||
void this.commitKbRoot(path)
|
||||
}
|
||||
|
||||
const fail = (error: unknown): void => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { AlarmPushRpcCall } from './alarm-push-status-view.ts'
|
|||
export const NETXOPS_RPC_CHANNEL = '/netxops'
|
||||
|
||||
export const KB_STATUS_ENDPOINT = 'kb.status'
|
||||
export const KB_RELOAD_ENDPOINT = 'kb.reload'
|
||||
export const KB_RESOLVE_ENDPOINT = 'kb.resolve'
|
||||
|
||||
export type { KbSnapshot, KbStatus }
|
||||
|
|
@ -50,6 +51,10 @@ export function asKbSnapshot(value: unknown): KbSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
function asRpcOk(result: unknown): result is { ok: true; value?: unknown } {
|
||||
return result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the published KB snapshot from the Host.
|
||||
*/
|
||||
|
|
@ -58,10 +63,37 @@ export async function fetchKbStatus(
|
|||
signal?: AbortSignal,
|
||||
): Promise<KbSnapshot> {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL, KB_STATUS_ENDPOINT, {}, signal)
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true) {
|
||||
return asKbSnapshot((result as { value?: unknown }).value)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
return unconfiguredKbSnapshot()
|
||||
// Do not pretend "unconfigured" on transport/shape miss — keep empty identity
|
||||
// but mark error so the badge is not stuck on the soft "pure netx" copy.
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === false) {
|
||||
const message = String(
|
||||
(result as { error?: { message?: string } }).error?.message ?? 'kb.status rpc failed',
|
||||
)
|
||||
return { ...unconfiguredKbSnapshot(), status: 'error', errorMessage: message }
|
||||
}
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: 'error',
|
||||
errorMessage: 'kb.status unavailable',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force Host to re-resolve `kbRoot` from live settings and republish.
|
||||
*/
|
||||
export async function reloadKbStatus(
|
||||
call: AlarmPushRpcCall,
|
||||
signal?: AbortSignal,
|
||||
): Promise<KbSnapshot> {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL, KB_RELOAD_ENDPOINT, {}, signal)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
// Older hosts without kb.reload — fall back to status / resolve.
|
||||
return fetchKbStatus(call, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,13 +106,17 @@ export async function resolveKbPath(
|
|||
): Promise<KbSnapshot> {
|
||||
// Prefer flat `{ path }` (netxops channel); also try `{ args: { path } }`.
|
||||
let result = await call(NETXOPS_RPC_CHANNEL, KB_RESOLVE_ENDPOINT, { path }, signal)
|
||||
if (!(result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true)) {
|
||||
if (!asRpcOk(result)) {
|
||||
result = await call(NETXOPS_RPC_CHANNEL, KB_RESOLVE_ENDPOINT, { args: { path } }, signal)
|
||||
}
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true) {
|
||||
return asKbSnapshot((result as { value?: unknown }).value)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: 'error',
|
||||
errorMessage: 'kb.resolve unavailable',
|
||||
}
|
||||
return unconfiguredKbSnapshot()
|
||||
}
|
||||
|
||||
/** Badge tone for the settings card. */
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
kbBrowse: 'Browse…',
|
||||
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
||||
kbBrowseFailed: 'Browse failed: {detail}',
|
||||
kbSaveHint: 'Browse writes and saves automatically. Manual paste still needs Save.',
|
||||
kbSaveHint: 'Browse writes and saves automatically, then reloads Host KB status. Manual paste still needs Save.',
|
||||
kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}',
|
||||
kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)',
|
||||
kbStatusError: 'Knowledge base: error — {detail}',
|
||||
|
|
@ -184,7 +184,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
kbBrowse: '浏览…',
|
||||
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
||||
kbBrowseFailed: '浏览失败:{detail}',
|
||||
kbSaveHint: '点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。',
|
||||
kbSaveHint: '点「浏览」选目录后会自动写入、保存,并刷新宿主知识库状态;若是手粘路径仍需点保存。',
|
||||
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
|
||||
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
|
||||
kbStatusError: '知识库: 错误 — {detail}',
|
||||
|
|
|
|||
22
src/index.ts
22
src/index.ts
|
|
@ -624,6 +624,28 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
if (endpoint === 'kb.status') {
|
||||
return { ok: true, value: getKbContext() }
|
||||
}
|
||||
if (endpoint === 'kb.reload') {
|
||||
// Re-read live settings and publish — used by the settings card after
|
||||
// browse/save so the badge does not stick on a stale unconfigured snapshot.
|
||||
const current = source()
|
||||
const kb = resolveKbRoot(current.kbRoot ?? '')
|
||||
publishKbContext(kb)
|
||||
applyKbEnv(kb)
|
||||
if (kb.status === 'configured') {
|
||||
connCtx.logger.info(
|
||||
'netxops: kb.reload → %s (%s / %s v%s)',
|
||||
kb.realRoot,
|
||||
kb.operatorName,
|
||||
kb.country,
|
||||
kb.version,
|
||||
)
|
||||
} else if (kb.status === 'error') {
|
||||
connCtx.logger.warn('netxops: kb.reload error — %s', kb.errorMessage)
|
||||
} else {
|
||||
connCtx.logger.info('netxops: kb.reload → unconfigured (kbRoot empty)')
|
||||
}
|
||||
return { ok: true, value: kb }
|
||||
}
|
||||
if (endpoint === 'kb.resolve') {
|
||||
const path = extractKbResolvePath(payload)
|
||||
return { ok: true, value: resolveKbRoot(path) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue