Files
adam-acs/inertia/layouts/MainLayout.vue
T
Kevin L'HereecandCopilot 847da1cdf5 feat: per-device TR-069 SOAP trace logging
- 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>
2026-06-01 12:07:07 +02:00

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 &amp; 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>