mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 15:33:11 +08:00
Add operator-subset knowledge base wiring for Netx Ops P1.
Locate and validate MANIFEST under kbRoot, show anti-mixup status in settings, and inject KB_* env plus kb-context so sessions degrade to pure netx when unset. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e6f9d062ed
commit
de7c6a0c4b
23 changed files with 2694 additions and 68 deletions
|
|
@ -15,6 +15,12 @@ import {
|
|||
type AlarmPushRpcCall,
|
||||
type AlarmPushStatus,
|
||||
} from './alarm-push-status-view.ts'
|
||||
import {
|
||||
fetchKbStatus,
|
||||
resolveKbPath,
|
||||
type KbSnapshot,
|
||||
} from './kb-status-view.ts'
|
||||
import { unconfiguredKbSnapshot } from '../netx/kb-manifest.ts'
|
||||
import {
|
||||
EMPTY_IM_DELIVERY_CATALOG,
|
||||
fetchImDeliveryCatalog,
|
||||
|
|
@ -49,6 +55,8 @@ export interface NetxopsSettings {
|
|||
groupTopologyInPreset?: boolean
|
||||
groupTopologyPublic?: boolean
|
||||
nmsProvider?: string
|
||||
/** Operator-subset knowledge package root (Host-local path). */
|
||||
kbRoot?: string
|
||||
}
|
||||
|
||||
interface CredentialState {
|
||||
|
|
@ -66,6 +74,7 @@ export interface NetxopsCardState extends CardShell {
|
|||
groupTopologyInPreset: CardFieldState
|
||||
groupTopologyPublic: CardFieldState
|
||||
nmsProvider: CardFieldState
|
||||
kbRoot: CardFieldState
|
||||
alarmPushEnabled: CardFieldState
|
||||
alarmDeliverDsh: CardFieldState
|
||||
alarmDeliverIm: CardFieldState
|
||||
|
|
@ -78,6 +87,10 @@ export interface NetxopsCardState extends CardShell {
|
|||
apiTokenRemoteReady: boolean
|
||||
/** Host WSS status when Connection RPC is available; otherwise null. */
|
||||
alarmPushStatus: AlarmPushStatus | null
|
||||
/** Latest KB snapshot (saved root, or live preview while editing). */
|
||||
kbStatus: KbSnapshot | null
|
||||
/** Soft-injected `remote.directoryPicker` is available. */
|
||||
kbDirectoryPickerReady: boolean
|
||||
/** Saved IM delivery targets for the picker (soft-depends on dsh-im-ops). */
|
||||
imDeliveryCatalog: ImDeliveryCatalog
|
||||
/** Bulk session-export readiness from Host RPC; null when RPC is absent. */
|
||||
|
|
@ -101,6 +114,12 @@ export interface NetxopsCardFace extends CardActions {
|
|||
}
|
||||
/** Download every durable session on this Host as one ZIP. */
|
||||
exportAllSessions: () => void
|
||||
/** Open the Host directory picker and write the chosen path into kbRoot. */
|
||||
browseKbRoot: () => void
|
||||
}
|
||||
|
||||
type DirectoryPickerRemote = {
|
||||
pick: (signal?: AbortSignal) => Promise<string | null>
|
||||
}
|
||||
|
||||
type CredentialsRemote = {
|
||||
|
|
@ -119,6 +138,11 @@ export class NetxopsCardController {
|
|||
}
|
||||
private rpcCall: AlarmPushRpcCall | undefined
|
||||
private alarmPushStatus: AlarmPushStatus | null = null
|
||||
private kbStatus: KbSnapshot | null = null
|
||||
private kbPreview: KbSnapshot | null = null
|
||||
private kbDirectoryPicker: DirectoryPickerRemote | undefined
|
||||
private kbBrowseInFlight = false
|
||||
private kbStatusInFlight = false
|
||||
private imDeliveryCatalog: ImDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }
|
||||
private sessionsExportStatus: SessionsExportStatus | null = null
|
||||
private sessionsExportBusy = false
|
||||
|
|
@ -140,6 +164,7 @@ export class NetxopsCardController {
|
|||
textField('apiUrl'),
|
||||
textField('lang'),
|
||||
textField('nmsProvider'),
|
||||
textField('kbRoot'),
|
||||
booleanFieldPersistFalse('groupOpsInPreset'),
|
||||
booleanField('groupOpsPublic'),
|
||||
booleanField('groupTopologyInPreset'),
|
||||
|
|
@ -154,7 +179,10 @@ export class NetxopsCardController {
|
|||
[{ field: API_TOKEN_FIELD, write: text => this.writeToken(text) }],
|
||||
)
|
||||
this.store = this.form.bind(() => this.projection())
|
||||
scope.subscribe(() => { void this.readCredential() })
|
||||
scope.subscribe(() => {
|
||||
void this.readCredential()
|
||||
void this.refreshKbPreview()
|
||||
})
|
||||
void this.readCredential()
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +199,7 @@ export class NetxopsCardController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Soft-inject Host Connection RPC used to read alarm-push WSS status.
|
||||
* Soft-inject Host Connection RPC used to read alarm-push WSS status + KB status.
|
||||
* @param call - `ctx.connection.rpc.call`, or undefined when connection leaves.
|
||||
*/
|
||||
setAlarmPushRpc(call: AlarmPushRpcCall | undefined): void {
|
||||
|
|
@ -183,6 +211,11 @@ export class NetxopsCardController {
|
|||
this.alarmPushStatus = null
|
||||
changed = true
|
||||
}
|
||||
if (this.kbStatus !== null || this.kbPreview !== null) {
|
||||
this.kbStatus = null
|
||||
this.kbPreview = null
|
||||
changed = true
|
||||
}
|
||||
if (this.imDeliveryCatalog.options.length > 0 || this.imDeliveryCatalog.available !== true) {
|
||||
this.imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }
|
||||
changed = true
|
||||
|
|
@ -196,14 +229,29 @@ export class NetxopsCardController {
|
|||
}
|
||||
this.startStatusPoll()
|
||||
void this.refreshAlarmPushStatus()
|
||||
void this.refreshKbStatus()
|
||||
void this.refreshKbPreview()
|
||||
void this.refreshImDeliveryCatalog()
|
||||
void this.refreshSessionsExportStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-inject `remote.directoryPicker` for the knowledge-base browse button.
|
||||
*/
|
||||
setDirectoryPicker(picker: DirectoryPickerRemote | undefined): void {
|
||||
const ready = picker !== undefined
|
||||
if (this.kbDirectoryPicker === picker && (this.kbDirectoryPicker !== undefined) === ready) {
|
||||
return
|
||||
}
|
||||
this.kbDirectoryPicker = picker
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
|
||||
private startStatusPoll(): void {
|
||||
if (this.pollTimer !== undefined) return
|
||||
this.pollTimer = setInterval(() => {
|
||||
void this.refreshAlarmPushStatus()
|
||||
void this.refreshKbStatus()
|
||||
void this.refreshImDeliveryCatalog()
|
||||
void this.refreshSessionsExportStatus()
|
||||
}, STATUS_POLL_MS)
|
||||
|
|
@ -293,6 +341,86 @@ export class NetxopsCardController {
|
|||
}
|
||||
}
|
||||
|
||||
private async refreshKbStatus(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined || this.kbStatusInFlight) return
|
||||
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())
|
||||
} catch {
|
||||
// Keep last good snapshot; next poll retries.
|
||||
} finally {
|
||||
this.kbStatusInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshKbPreview(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined) return
|
||||
const draft = this.form.field('kbRoot').text.trim()
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
if (draft === saved) {
|
||||
if (this.kbPreview !== null) {
|
||||
this.kbPreview = null
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const next = draft === ''
|
||||
? unconfiguredKbSnapshot()
|
||||
: await resolveKbPath(call, draft)
|
||||
const prev = this.kbPreview
|
||||
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.kbPreview = next
|
||||
this.store.set(this.projection())
|
||||
} catch {
|
||||
// Keep last preview; edits retry via scope subscribe.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Host OS directory chooser and write the path into kbRoot.
|
||||
*/
|
||||
browseKbRoot(): void {
|
||||
const picker = this.kbDirectoryPicker
|
||||
if (picker === undefined || this.kbBrowseInFlight) return
|
||||
this.kbBrowseInFlight = true
|
||||
void picker.pick()
|
||||
.then((path) => {
|
||||
if (typeof path === 'string' && path.trim() !== '') {
|
||||
this.form.actions().edit('kbRoot', path)
|
||||
void this.refreshKbPreview()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Picker cancel / host refusal — leave draft unchanged.
|
||||
})
|
||||
.finally(() => {
|
||||
this.kbBrowseInFlight = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Download every durable session as one ZIP through the browser download manager.
|
||||
*/
|
||||
|
|
@ -337,6 +465,7 @@ export class NetxopsCardController {
|
|||
apiUrl: this.form.field('apiUrl'),
|
||||
lang: this.form.field('lang'),
|
||||
nmsProvider: this.form.field('nmsProvider'),
|
||||
kbRoot: this.form.field('kbRoot'),
|
||||
groupOpsInPreset: this.form.field('groupOpsInPreset'),
|
||||
groupOpsPublic: this.form.field('groupOpsPublic'),
|
||||
groupTopologyInPreset: this.form.field('groupTopologyInPreset'),
|
||||
|
|
@ -352,6 +481,8 @@ export class NetxopsCardController {
|
|||
apiTokenWritable: this.credential.remoteReady && this.credential.writable,
|
||||
apiTokenRemoteReady: this.credential.remoteReady,
|
||||
alarmPushStatus: this.alarmPushStatus,
|
||||
kbStatus: this.kbPreview ?? this.kbStatus,
|
||||
kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined,
|
||||
imDeliveryCatalog: this.imDeliveryCatalog,
|
||||
sessionsExportStatus: this.sessionsExportStatus,
|
||||
sessionsExportBusy: this.sessionsExportBusy,
|
||||
|
|
@ -407,10 +538,24 @@ export class NetxopsCardController {
|
|||
}
|
||||
|
||||
inject(): NetxopsCardFace {
|
||||
const actions = this.form.actions()
|
||||
return {
|
||||
hooks: { netxopsCard: this.store },
|
||||
...this.form.actions(),
|
||||
...actions,
|
||||
edit: (field, text) => {
|
||||
actions.edit(field, text)
|
||||
if (field === 'kbRoot') void this.refreshKbPreview()
|
||||
},
|
||||
discard: () => {
|
||||
actions.discard()
|
||||
void this.refreshKbPreview()
|
||||
},
|
||||
resetField: (field) => {
|
||||
actions.resetField(field)
|
||||
if (field === 'kbRoot') void this.refreshKbPreview()
|
||||
},
|
||||
exportAllSessions: () => { this.exportAllSessions() },
|
||||
browseKbRoot: () => { this.browseKbRoot() },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue