- 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)
211 lines
7.1 KiB
Vue
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>
|