- Tr069TraceService: in-memory Map O(1) lookup, Promise-queue per device for sequential file writes, 10MB rotation (5 files kept) - config/tr069-trace.json: persisted list of traced devices (atomic write) - CwmpLoggerMiddleware: buffers request+response, writes to device log file after next() once ctx.cwmpDeviceId is set by controller - CwmpController: sets ctx.cwmpDeviceId on all code paths (Inform + others) - StateMachineManager: getHashIdForRemote() for non-Inform messages - REST API: GET/POST/DELETE /api/v1/tr069-trace - UI: Tr069Trace Vue page with device search, add/remove tracing - 8 new unit tests (192 total, all passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
253 lines
6.8 KiB
Vue
253 lines
6.8 KiB
Vue
<script setup lang="ts">
|
|
import { Link, Head, usePage } from '@inertiajs/vue3'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { ref } from 'vue'
|
|
|
|
const { t, locale } = useI18n()
|
|
const currentLocale = ref(locale.value)
|
|
const page = usePage()
|
|
const langMenuOpen = ref(false)
|
|
|
|
function switchLocale(lang: string) {
|
|
locale.value = lang
|
|
currentLocale.value = lang
|
|
langMenuOpen.value = false
|
|
}
|
|
|
|
function toggleLangMenu() {
|
|
langMenuOpen.value = !langMenuOpen.value
|
|
}
|
|
|
|
function isActive(paths: string | string[]): boolean {
|
|
const url = page.url
|
|
if (Array.isArray(paths)) {
|
|
return paths.some((p) => url === p || (p !== '/' && url.startsWith(p)))
|
|
}
|
|
return url === paths || (paths !== '/' && url.startsWith(paths))
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Head :title="page.component" />
|
|
|
|
<div class="d-flex flex-column min-vh-100">
|
|
<!-- Navbar -->
|
|
<header>
|
|
<div class="bg-dark">
|
|
<div class="container-xxl">
|
|
<nav id="main-menu" class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<!-- Brand / Logo -->
|
|
<h1 class="navbar-brand mb-0 me-3">
|
|
<Link href="/">
|
|
<span class="visually-hidden">{{ t('header.title') }}</span>
|
|
<svg
|
|
width="30"
|
|
height="30"
|
|
viewBox="0 0 100 100"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden="true"
|
|
>
|
|
<rect width="100" height="100" fill="#ff7900" />
|
|
<rect x="14.29" y="71.43" width="71.43" height="14.29" fill="#fff" />
|
|
</svg>
|
|
</Link>
|
|
</h1>
|
|
|
|
<!-- Mobile toggle -->
|
|
<button
|
|
class="navbar-toggler"
|
|
type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#navbarContent"
|
|
aria-controls="navbarContent"
|
|
aria-expanded="false"
|
|
aria-label="Toggle navigation"
|
|
>
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<!-- Nav links -->
|
|
<ul class="navbar-nav_container navbar-nav">
|
|
<li class="nav-item">
|
|
<Link
|
|
href="/"
|
|
class="nav-link"
|
|
:class="{
|
|
active:
|
|
isActive('/') &&
|
|
!page.url.startsWith('/configurations') &&
|
|
!page.url.startsWith('/commands'),
|
|
}"
|
|
>
|
|
{{ t('header.devices') }}
|
|
</Link>
|
|
</li>
|
|
<li class="nav-item">
|
|
<Link
|
|
href="/configurations"
|
|
class="nav-link"
|
|
:class="{ active: isActive('/configurations') }"
|
|
>
|
|
{{ t('header.configurations') }}
|
|
</Link>
|
|
</li>
|
|
<li class="nav-item">
|
|
<Link
|
|
href="/firmware-catalog"
|
|
class="nav-link"
|
|
:class="{ active: isActive('/firmware-catalog') }"
|
|
>
|
|
{{ t('header.firmware') }}
|
|
</Link>
|
|
</li>
|
|
<li class="nav-item">
|
|
<Link href="/commands" class="nav-link" :class="{ active: isActive('/commands') }">
|
|
{{ t('header.commands') }}
|
|
</Link>
|
|
</li>
|
|
<li class="nav-item">
|
|
<Link
|
|
href="/tr069-trace"
|
|
class="nav-link"
|
|
:class="{ active: isActive('/tr069-trace') }"
|
|
>
|
|
{{ t('header.tr069Trace') }}
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Right: language dropdown -->
|
|
<div class="navbar-right">
|
|
<ul class="navbar-nav">
|
|
<li class="nav-item dropdown" :class="{ show: langMenuOpen }">
|
|
<a
|
|
class="nav-link dropdown-toggle"
|
|
href="javascript:void(0);"
|
|
role="button"
|
|
@click="toggleLangMenu"
|
|
:aria-expanded="langMenuOpen"
|
|
>
|
|
<span>{{ currentLocale.toUpperCase() }}</span>
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end" :class="{ show: langMenuOpen }">
|
|
<li>
|
|
<a
|
|
class="dropdown-item"
|
|
:class="{ active: currentLocale === 'fr' }"
|
|
href="javascript:void(0);"
|
|
@click="switchLocale('fr')"
|
|
>
|
|
FR — Français
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a
|
|
class="dropdown-item"
|
|
:class="{ active: currentLocale === 'en' }"
|
|
href="javascript:void(0);"
|
|
@click="switchLocale('en')"
|
|
>
|
|
EN — English
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-grow-1" style="background-color: #f6f6f6">
|
|
<slot />
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer navbar fixed-bottom bg-dark" data-bs-theme="dark">
|
|
<h2 class="visually-hidden">Sitemap & information</h2>
|
|
<div class="container-xxl footer-terms">
|
|
<ul class="navbar-nav flex-row gap-md-3">
|
|
<li class="fw-bold text-white">© Orange 2025</li>
|
|
<li>
|
|
<a class="nav-link" href="#">{{ t('footer.legal') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="nav-link" href="#">{{ t('footer.privacy') }}</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
main {
|
|
padding-bottom: 80px;
|
|
}
|
|
|
|
#main-menu {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
#main-menu .navbar-brand {
|
|
margin: 0;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
#main-menu .navbar-brand a {
|
|
display: inline-block;
|
|
padding-bottom: 0.25rem;
|
|
}
|
|
|
|
#main-menu .navbar-brand svg {
|
|
display: block;
|
|
}
|
|
|
|
#main-menu .nav-link.active {
|
|
padding-bottom: 0.75rem;
|
|
}
|
|
|
|
.navbar-nav_container {
|
|
display: flex;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
gap: 0.25rem;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.navbar-right {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.navbar-right .navbar-nav {
|
|
display: flex;
|
|
flex-direction: row;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.navbar-right .nav-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-item.dropdown .dropdown-menu {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
left: auto;
|
|
}
|
|
|
|
.dropdown-item.active {
|
|
background-color: #ff7900;
|
|
color: #fff;
|
|
}
|
|
</style>
|