Files
adam-acs/inertia/pages/Home.vue
T
Kevin L'Hereec 3bb85d9031
Build & Deploy / build (push) Successful in 30s
Build & Deploy / docker (push) Successful in 1m4s
Build & Deploy / deploy (push) Successful in 35s
fix: support alias-based IPv6 interface names and add engine reset utility
- Home.vue: extend RE_IPV4/RE_IPV6 to match alias-based interface names
  like Device.IP.Interface.[BR_LAN].IPv6Address.[GUA_LAN].IPAddress
  (used by Sagemcom Livebox 4 with AliasBasedAddressing=1)
- reference_engine.ts: add resetReferenceEngine() to allow config reload
  without restart (wassup provider disabled via config/references.json)
2026-07-17 14:21:02 +02:00

211 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { router } from '@inertiajs/vue3'
import { useI18n } from 'vue-i18n'
import TitleBar from '../components/TitleBar.vue'
import { fetchWithCsrf } from '../helpers/fetch_with_csrf'
const { t } = useI18n()
const { devices } = defineProps<{
message: string
devices: any[]
}>()
const selected = ref<boolean[]>([])
onMounted(() => {
const saved = localStorage.getItem('selectedDevices')
if (saved) {
try {
const savedSelectedIds: string[] = JSON.parse(saved)
selected.value = devices.map((device) => savedSelectedIds.includes(device.identifier))
return
} catch { /* ignore */ }
}
selected.value = devices.map(() => false)
})
watch(selected, (newSelected) => {
const selectedIds = devices
.filter((_, index) => newSelected[index])
.map((device) => device.identifier)
localStorage.setItem('selectedDevices', JSON.stringify(selectedIds))
}, { deep: true })
const allSelected = computed({
get() {
return selected.value.length > 0 && selected.value.every(Boolean)
},
set(value: boolean) {
selected.value = selected.value.map(() => value)
},
})
function toggleSelectAll() {
allSelected.value = !allSelected.value
}
function getParameterValue(params: any[], name: string): string {
const param = params?.find((p: any) => p.name === name)
return param?.value ?? ''
}
function getPropertyValue(props: any[], name: string): string {
const prop = props?.find((p: any) => p.name === name)
return prop?.value ?? ''
}
// Hoisted regex constants — compiled once, not on every call
// Matches both numeric indexes (e.g. Interface.1) and alias-based names (e.g. Interface.[BR_LAN])
const IF_SEG = /(\d+|\[[^\]]+\])/
const RE_IPV4 = new RegExp(`^Device\\.IP\\.Interface\\.${IF_SEG.source}\\.IPv4Address\\.${IF_SEG.source}\\.IPAddress$`)
const RE_IPV6 = new RegExp(`^Device\\.IP\\.Interface\\.${IF_SEG.source}\\.IPv6Address\\.${IF_SEG.source}\\.IPAddress$`)
function getIpAddress(parameters: any[], properties: any[]): string {
// Priority 1 : TR-098 ExternalIPAddress (reuse existing helpers to avoid duplication)
const tr098Paths = [
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress',
'Device.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress',
]
for (const path of tr098Paths) {
const v = getParameterValue(parameters, path) || getPropertyValue(properties, path)
if (v) return v
}
// Priority 2 : TR-181 IPv4 — prefer index 1, then any index
const ipv4Exact =
getParameterValue(parameters, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') ||
getPropertyValue(properties, 'Device.IP.Interface.1.IPv4Address.1.IPAddress')
if (ipv4Exact) return ipv4Exact
const ipv4Any =
parameters?.find((p: any) => RE_IPV4.test(p.name) && p.value) ||
properties?.find((p: any) => RE_IPV4.test(p.name) && p.value)
if (ipv4Any) return ipv4Any.value
// Priority 3 : TR-181 IPv6 — prefer index 1, then any index (displayed with brackets)
const ipv6Exact =
getParameterValue(parameters, 'Device.IP.Interface.1.IPv6Address.1.IPAddress') ||
getPropertyValue(properties, 'Device.IP.Interface.1.IPv6Address.1.IPAddress')
if (ipv6Exact) return `[${ipv6Exact}]`
const ipv6Any =
parameters?.find((p: any) => RE_IPV6.test(p.name) && p.value) ||
properties?.find((p: any) => RE_IPV6.test(p.name) && p.value)
if (ipv6Any) return `[${ipv6Any.value}]`
return '—'
}
function goToDevice(identifier: string) {
router.visit(`/devices/${encodeURIComponent(identifier)}`)
}
const selectedIdentifiers = computed(() => {
return devices
.filter((_, index) => selected.value[index])
.map((device) => device.identifier)
})
function goToFleetManagement() {
if (selectedIdentifiers.value.length === 0) return
router.post('/commands/fleet', { devices: selectedIdentifiers.value })
}
const isDeleting = ref(false)
async function deleteSelected() {
if (selectedIdentifiers.value.length === 0 || isDeleting.value) return
isDeleting.value = true
try {
await Promise.all(
selectedIdentifiers.value.map((id) =>
fetchWithCsrf(`/internal/devices/${encodeURIComponent(id)}`, { method: 'DELETE' })
)
)
router.reload()
} finally {
isDeleting.value = false
}
}
</script>
<template>
<TitleBar section="devices" />
<div class="container-xxl py-4">
<div class="table-responsive">
<table class="table table-sm table-hover has-checkbox">
<thead>
<tr>
<th scope="col">
<div class="form-check mb-0">
<input
class="form-check-input"
type="checkbox"
id="selectAll"
v-model="allSelected"
/>
<label class="form-check-label" for="selectAll">
<span class="visually-hidden">{{ t('checkbox.selectAll') }}</span>
</label>
</div>
</th>
<th scope="col">{{ t('labels.manufacturer') }}</th>
<th scope="col">{{ t('labels.productClass') }}</th>
<th scope="col">{{ t('labels.serialNumber') }}</th>
<th scope="col">{{ t('labels.externalIP') }}</th>
<th scope="col">{{ t('labels.softwareVersion') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="(device, index) in devices"
:key="device.identifier"
:class="{ 'table-active': selected[index] }"
class="clickable-row"
@click="goToDevice(device.identifier)"
>
<td @click.stop>
<div class="form-check mb-0">
<input
class="form-check-input"
type="checkbox"
:id="'select' + index"
v-model="selected[index]"
/>
<label class="form-check-label" :for="'select' + index">
<span class="visually-hidden">{{ t('checkbox.selectRow', { index: index + 1 }) }}</span>
</label>
</div>
</td>
<td>{{ device.deviceId?.manufacturer }}</td>
<td>{{ device.deviceId?.productClass }}</td>
<td>{{ device.deviceId?.serialNumber }}</td>
<td>{{ getIpAddress(device.parameters, device.properties) }}</td>
<td>{{ getPropertyValue(device.properties, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion') || getPropertyValue(device.properties, 'Device.DeviceInfo.SoftwareVersion') || '—' }}</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3 d-flex gap-2">
<button
class="btn btn-primary"
@click="goToFleetManagement"
:disabled="selectedIdentifiers.length === 0"
>
{{ t('button.fleetManagement', { count: selectedIdentifiers.length }) }}
</button>
<button
class="btn btn-outline-danger"
@click="deleteSelected"
:disabled="selectedIdentifiers.length === 0 || isDeleting"
>
{{ isDeleting ? 'Suppression…' : `Supprimer (${selectedIdentifiers.length})` }}
</button>
</div>
</div>
</template>