diff --git a/.adonisjs/client/registry/index.ts b/.adonisjs/client/registry/index.ts index 0bb6e4b..790086f 100644 --- a/.adonisjs/client/registry/index.ts +++ b/.adonisjs/client/registry/index.ts @@ -90,12 +90,30 @@ const routes = { tokens: [{"old":"/api/v1/tr069-trace","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace","type":0,"val":"tr069-trace","end":""}], types: placeholder as Registry['tr_069_trace.store']['types'], }, + 'tr_069_trace.raw_config': { + methods: ["GET","HEAD"], + pattern: '/api/v1/tr069-trace/config', + tokens: [{"old":"/api/v1/tr069-trace/config","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"config","end":""}], + types: placeholder as Registry['tr_069_trace.raw_config']['types'], + }, + 'tr_069_trace.save_raw_config': { + methods: ["PUT"], + pattern: '/api/v1/tr069-trace/config', + tokens: [{"old":"/api/v1/tr069-trace/config","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"config","end":""}], + types: placeholder as Registry['tr_069_trace.save_raw_config']['types'], + }, 'tr_069_trace.destroy': { methods: ["DELETE"], pattern: '/api/v1/tr069-trace/:id', tokens: [{"old":"/api/v1/tr069-trace/:id","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/:id","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/:id","type":1,"val":"id","end":""}], types: placeholder as Registry['tr_069_trace.destroy']['types'], }, + 'tr_069_trace.show_log': { + methods: ["GET","HEAD"], + pattern: '/api/v1/tr069-trace/:id/log', + tokens: [{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":1,"val":"id","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"log","end":""}], + types: placeholder as Registry['tr_069_trace.show_log']['types'], + }, 'configurations_v_1.index': { methods: ["GET","HEAD"], pattern: '/api/v1/configurations', diff --git a/.adonisjs/client/registry/schema.d.ts b/.adonisjs/client/registry/schema.d.ts index 75de136..4156996 100644 --- a/.adonisjs/client/registry/schema.d.ts +++ b/.adonisjs/client/registry/schema.d.ts @@ -175,6 +175,30 @@ export interface Registry { errorResponse: ExtractErrorResponse>> } } + 'tr_069_trace.raw_config': { + methods: ["GET","HEAD"] + pattern: '/api/v1/tr069-trace/config' + types: { + body: {} + paramsTuple: [] + params: {} + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } + 'tr_069_trace.save_raw_config': { + methods: ["PUT"] + pattern: '/api/v1/tr069-trace/config' + types: { + body: {} + paramsTuple: [] + params: {} + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } 'tr_069_trace.destroy': { methods: ["DELETE"] pattern: '/api/v1/tr069-trace/:id' @@ -187,6 +211,18 @@ export interface Registry { errorResponse: ExtractErrorResponse>> } } + 'tr_069_trace.show_log': { + methods: ["GET","HEAD"] + pattern: '/api/v1/tr069-trace/:id/log' + types: { + body: {} + paramsTuple: [ParamValue] + params: { id: ParamValue } + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } 'configurations_v_1.index': { methods: ["GET","HEAD"] pattern: '/api/v1/configurations' diff --git a/.adonisjs/client/registry/tree.d.ts b/.adonisjs/client/registry/tree.d.ts index af290a0..7303bc5 100644 --- a/.adonisjs/client/registry/tree.d.ts +++ b/.adonisjs/client/registry/tree.d.ts @@ -24,7 +24,10 @@ export interface ApiDefinition { tr069Trace: { index: typeof routes['tr_069_trace.index'] store: typeof routes['tr_069_trace.store'] + rawConfig: typeof routes['tr_069_trace.raw_config'] + saveRawConfig: typeof routes['tr_069_trace.save_raw_config'] destroy: typeof routes['tr_069_trace.destroy'] + showLog: typeof routes['tr_069_trace.show_log'] } configurationsV1: { index: typeof routes['configurations_v_1.index'] diff --git a/.adonisjs/server/routes.d.ts b/.adonisjs/server/routes.d.ts index e7e32bb..213f655 100644 --- a/.adonisjs/server/routes.d.ts +++ b/.adonisjs/server/routes.d.ts @@ -18,7 +18,10 @@ export type ScannedRoutes = { 'commands.store': { paramsTuple?: []; params?: {} } 'tr_069_trace.index': { paramsTuple?: []; params?: {} } 'tr_069_trace.store': { paramsTuple?: []; params?: {} } + 'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} } + 'tr_069_trace.save_raw_config': { paramsTuple?: []; params?: {} } 'tr_069_trace.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } + 'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'configurations_v_1.index': { paramsTuple?: []; params?: {} } 'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} } 'configurations_v_1.store': { paramsTuple?: []; params?: {} } @@ -81,6 +84,8 @@ export type ScannedRoutes = { 'commands.index': { paramsTuple?: []; params?: {} } 'commands.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'tr_069_trace.index': { paramsTuple?: []; params?: {} } + 'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} } + 'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'configurations_v_1.index': { paramsTuple?: []; params?: {} } 'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} } 'configurations_v_1.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } @@ -112,6 +117,8 @@ export type ScannedRoutes = { 'commands.index': { paramsTuple?: []; params?: {} } 'commands.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'tr_069_trace.index': { paramsTuple?: []; params?: {} } + 'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} } + 'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'configurations_v_1.index': { paramsTuple?: []; params?: {} } 'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} } 'configurations_v_1.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } @@ -142,6 +149,7 @@ export type ScannedRoutes = { 'firmware_catalog.delete_route': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } } PUT: { + 'tr_069_trace.save_raw_config': { paramsTuple?: []; params?: {} } 'configurations_v_1.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'firmware_catalog.update_entry': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'firmware_catalog.update_route': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index 54c98e9..5d7a204 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -3,6 +3,7 @@ import { buildSoapEnvelope } from '../protocol/soap.js' import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js' import { StateMachineManager } from '../services/cwmp/state_machine_manager.js' import { Device, detectDataModelFromParams } from '../domain/device/device.js' +import { reclassifyDeviceFromCatalog } from '../services/parameter_writability.js' import { DeviceId } from '../domain/device/device_id.js' import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js' import DeviceModel from '../models/device.js' @@ -120,6 +121,12 @@ export default class CwmpController { if (paramValues.length > 0) { device.updateFromValues(paramValues) } + + // Reclassify properties → parameters using catalog when GPN is unsupported + if (device.gpnStatus === 'unsupported') { + reclassifyDeviceFromCatalog(device) + } + device.setTmpParamValues(paramValues) // Detect TR data model from Inform parameter names (Device.* = TR-181, InternetGatewayDevice.* = TR-098) diff --git a/app/services/cwmp/device_action_executor.ts b/app/services/cwmp/device_action_executor.ts index 1431fbc..c8294f5 100644 --- a/app/services/cwmp/device_action_executor.ts +++ b/app/services/cwmp/device_action_executor.ts @@ -1,18 +1,9 @@ import { Device } from '../../domain/device/device.js' -import { DeviceParameter } from '../../domain/device/device_parameter.js' import { ParameterType } from '../../domain/shared/types.js' import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js' -import { normalizePath } from '../parameter_writability.js' +import { reclassifyDeviceFromCatalog } from '../parameter_writability.js' import DeviceModel from '../../models/device.js' import logger from '@adonisjs/core/services/logger' -import { createRequire } from 'node:module' - -const require = createRequire(import.meta.url) -const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string }> = - require('../../../config/tr181-catalog.json') -const catalogWritableIndex = new Set( - tr181Catalog.filter((e) => e.writable).map((e) => e.path) -) // Parameter types (simplified from legacy provisioning domain) interface SetParameterValue { @@ -93,33 +84,8 @@ export class DeviceActionExecutor { } /** - * Reclassify device properties as parameters using the TR-181 catalog. - * Called when GPN is unsupported — properties that are writable per the catalog - * should be moved to parameters so the configuration engine can write to them. + * Get parameter values from device. */ - private reclassifyFromCatalog(device: Device): void { - const properties = device.getProperties() - let reclassified = 0 - - for (const prop of properties) { - const normalized = normalizePath(prop.name) - if (catalogWritableIndex.has(normalized)) { - // Move from properties to parameters (writable) - const snap = prop.toSnapshot() - device.setParameter( - new DeviceParameter(snap.name, snap.value, snap.type, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) - ) - // Remove from properties map - ;(device as any)._properties.delete(prop.name) - reclassified++ - } - } - - if (reclassified > 0) { - logger.debug({ identifier: device.identifier, reclassified }, '[DeviceAction] Reclassified properties → parameters via catalog') - } - } - async getParameterValues( device: Device, parameterPaths: string[], @@ -208,7 +174,7 @@ export class DeviceActionExecutor { if (code === 9000 || code === 9001) { device.setGpnStatus('unsupported') // Reclassify parameters using TR-181 catalog writability - this.reclassifyFromCatalog(device) + reclassifyDeviceFromCatalog(device) await this.persistDevice(device) } // Autres erreurs (réseau, timeout, 9002+) → gpnStatus inchangé diff --git a/app/services/parameter_writability.ts b/app/services/parameter_writability.ts index cd97b2b..45585a9 100644 --- a/app/services/parameter_writability.ts +++ b/app/services/parameter_writability.ts @@ -1,5 +1,7 @@ import { createRequire } from 'node:module' import type { DeviceSnapshot } from '../domain/device/device.js' +import type { Device } from '../domain/device/device.js' +import { DeviceParameter } from '../domain/device/device_parameter.js' const require = createRequire(import.meta.url) const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string }> = @@ -7,6 +9,9 @@ const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string // Index chargé une fois au démarrage — O(1) lookup const catalogIndex = new Map(tr181Catalog.map((e) => [e.path, e.writable])) +const catalogWritableIndex = new Set( + tr181Catalog.filter((e) => e.writable).map((e) => e.path) +) /** Remplace les segments numériques par {i} pour matcher les entrées du catalogue */ export function normalizePath(path: string): string { @@ -46,3 +51,27 @@ export function resolveWritability(snapshot: DeviceSnapshot, paramPath: string): if (gpnStatus === 'unsupported') return { writable: true, source: 'default' } return { writable: null, source: 'unknown' } } + +/** + * Reclassify device properties as parameters using the TR-181 catalog. + * Properties that are writable per the catalog are moved to parameters. + * Should be called after updateFromValues when gpnStatus is 'unsupported'. + */ +export function reclassifyDeviceFromCatalog(device: Device): number { + const properties = device.getProperties() + let reclassified = 0 + + for (const prop of properties) { + const normalized = normalizePath(prop.name) + if (catalogWritableIndex.has(normalized)) { + const snap = prop.toSnapshot() + device.setParameter( + new DeviceParameter(snap.name, snap.value, snap.type, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) + ) + ;(device as any)._properties.delete(prop.name) + reclassified++ + } + } + + return reclassified +} diff --git a/inertia/pages/DeviceDetail.vue b/inertia/pages/DeviceDetail.vue index 813624a..554e1dc 100644 --- a/inertia/pages/DeviceDetail.vue +++ b/inertia/pages/DeviceDetail.vue @@ -14,6 +14,51 @@ const { device, modelPaths, commands } = defineProps<{ const isLoading = ref(false) const showDeleteConfirm = ref(false) +const isTraced = ref(false) +const tracingAction = ref(false) + +// Check if device is already traced +async function checkTraceStatus() { + try { + const response = await fetch('/api/v1/tr069-trace') + if (response.ok) { + const data = await response.json() + const entries = data.entries || [] + isTraced.value = entries.some((e: any) => e.deviceId === device.identifier) + } + } catch { + // Silently ignore + } +} + +async function toggleTrace() { + tracingAction.value = true + try { + if (isTraced.value) { + const response = await fetch(`/api/v1/tr069-trace/${encodeURIComponent(device.identifier)}`, { + method: 'DELETE', + }) + if (response.ok || response.status === 204) { + isTraced.value = false + } + } else { + const response = await fetch('/api/v1/tr069-trace', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deviceId: device.identifier }), + }) + if (response.ok) { + isTraced.value = true + } + } + } catch { + // Silently ignore + } finally { + tracingAction.value = false + } +} + +checkTraceStatus() function deleteDevice() { router.delete(`/devices/${encodeURIComponent(device.identifier)}`, { @@ -328,6 +373,21 @@ function getPropertyValue(name: string): string { +