feat: estimated next connection + fix build errors

- Store lastPeriodicInformAt on device when PERIODIC Inform received
- Display 'Prochaine connexion' in DeviceDetail identity card
- Fix tsconfig exclude (tests, inertia, coverage) for Docker build
- Fix TS errors in tests (CommandResult type, unused imports)
- Fix distinctCriteriaValues typing in controller
This commit is contained in:
Kevin L'Hereec committed 2026-06-02 17:56:17 +02:00
1 parent e1e8ae5097
commit 31578e368f
7 files changed
+55 -10

No files matched your search

+6
View File
@@ -172,6 +172,12 @@ export default class CwmpController {
logger.info({ events }, 'CWMP Inform events extracted')
// Track last periodic inform timestamp
const isPeriodic = events.some((e) => String(e).toUpperCase().includes('PERIODIC') || String(e) === '2')
if (isPeriodic) {
await DeviceModel.updateOne({ identifier: hashId }, { $set: { lastPeriodicInformAt: new Date() } }).catch(() => {})
}
// Start state machine
await manager.startStateMachine(device, session, { events })
manager.bindRemoteToDevice(remoteKey, device.deviceId)
+1 -1
View File
@@ -8,7 +8,7 @@ export default class DevicesController {
private deviceService = new DeviceService()
async criteriaValues({ response }: HttpContext) {
const values = await DeviceModel.distinctCriteriaValues()
const values = await (DeviceModel as any).distinctCriteriaValues()
return response.json(values)
}
+1
View File
@@ -14,6 +14,7 @@ export interface DeviceRecord {
compliance?: DeviceComplianceInfo
provisioningCode?: string
references?: DeviceReference[]
lastPeriodicInformAt?: Date
updatedAt?: Date
createdAt?: Date
}
+29
View File
@@ -126,6 +126,18 @@ function submitGetModel() {
})
}
// Estimated next connection = lastPeriodicInformAt + PeriodicInformInterval
const estimatedNextConnection = (() => {
const lastPeriodic = device.lastPeriodicInformAt
if (!lastPeriodic) return null
const allParams = [...(device.properties || []), ...(device.parameters || [])]
const intervalParam = allParams.find((p: any) => p.name?.includes('PeriodicInformInterval'))
const intervalSeconds = intervalParam?.value ? parseInt(intervalParam.value, 10) : 0
if (!intervalSeconds || intervalSeconds <= 0) return null
const next = new Date(new Date(lastPeriodic).getTime() + intervalSeconds * 1000)
return next.toLocaleString()
})()
// Check if the device has a ConnectionRequestURL (needed for immediate mode / Refresh)
const hasConnectionRequestUrl = (() => {
const allParams = [...(device.properties || []), ...(device.parameters || [])]
@@ -333,6 +345,23 @@ async function resolveAllProviders() {
</div>
</div>
<!-- Zone 5b : Prochaine connexion estimée -->
<div v-if="estimatedNextConnection" class="col-sm-6 col-lg-4">
<div class="rounded-3 px-3 py-3 d-flex align-items-center gap-3" style="background:#f6f7f9">
<div class="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
style="width:34px;height:34px;background:#e4e6ea">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="text-muted">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0"/>
</svg>
</div>
<div>
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">Prochaine connexion</div>
<div class="font-monospace small fw-medium">{{ estimatedNextConnection }}</div>
</div>
</div>
</div>
</div>
<!-- Zone 6 : Références client -->
+5 -5
View File
@@ -41,15 +41,15 @@ describe('Command', () => {
it('complete transitions SENT → COMPLETED', () => {
const cmd = makeCommand()
cmd.markAsSent()
cmd.complete({ success: true })
cmd.complete({ success: true, timestamp: new Date() })
expect(cmd.status).toBe(CommandStatus.COMPLETED)
expect(cmd.result).toEqual({ success: true })
expect(cmd.result?.success).toBe(true)
expect(cmd.isCompleted()).toBe(true)
})
it('complete throws if PENDING', () => {
const cmd = makeCommand()
expect(() => cmd.complete({})).toThrow('Cannot complete command')
expect(() => cmd.complete({ success: true, timestamp: new Date() } as any)).toThrow('Cannot complete command')
})
it('fail transitions to FAILED', () => {
@@ -64,7 +64,7 @@ describe('Command', () => {
it('fail throws if already COMPLETED', () => {
const cmd = makeCommand()
cmd.markAsSent()
cmd.complete({})
cmd.complete({ success: true, timestamp: new Date() } as any)
expect(() => cmd.fail('oops')).toThrow('Cannot fail a successful command')
})
@@ -78,7 +78,7 @@ describe('Command', () => {
it('cancel throws if COMPLETED', () => {
const cmd = makeCommand()
cmd.markAsSent()
cmd.complete({})
cmd.complete({ success: true, timestamp: new Date() } as any)
expect(() => cmd.cancel()).toThrow('Cannot cancel command')
})
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
// Mock logger + DeviceModel
vi.mock('@adonisjs/core/services/logger', () => ({
+12 -3
View File
@@ -2,7 +2,16 @@
"extends": "@adonisjs/tsconfig/tsconfig.app.json",
"compilerOptions": {
"rootDir": "./",
"jsx": "react",
"outDir": "./build"
}
}
},
"vueCompilerOptions": {
"strictTemplates": true,
"target": 3.3
},
"exclude": [
"node_modules",
"tests",
"coverage",
"inertia"
]
}