refactor: migrate the frontend from AngularJS to Vue 3

This commit is contained in:
tdurieux
2026-09-09 13:19:53 +02:00
parent dc0ef022fb
commit 4a18f94631
65 changed files with 6164 additions and 8425 deletions
+88 -96
View File
@@ -14,37 +14,37 @@
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div ng-if="loading" class="admin-empty">Loading overview&hellip;</div>
<div ng-if="error" class="alert alert-danger" style="margin:12px 0"><i class="fas fa-exclamation-triangle"></i> {{error}}</div>
<div class="admin-empty" v-if="loading">Loading overview&hellip;</div>
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><i class="fas fa-exclamation-triangle"></i> {{ error }}</div>
<div ng-if="data">
<div v-if="data">
<!-- ── Top KPI row ──────────────────────────────────────────── -->
<section class="ov-kpi-row">
<div class="ov-kpi-card">
<div class="ov-kpi-label">Repositories <span class="ov-dot ov-dot-ok"></span></div>
<div class="ov-kpi-value">{{humanNum(data.repos.total)}}</div>
<div class="ov-kpi-sub">+{{data.repos.newRepos24h}} &middot; last 24h</div>
<div class="ov-kpi-value">{{ humanNum(data?.repos?.total) }}</div>
<div class="ov-kpi-sub">+{{ data?.repos?.newRepos24h }} &middot; last 24h</div>
</div>
<div class="ov-kpi-card">
<div class="ov-kpi-label">CPU</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.cpuPercent > 80}">{{data.system.cpuPercent}}%</div>
<div class="ov-kpi-sub">{{data.system.cpuCount}} cores &middot; load {{data.system.loadAvg[0].toFixed(1)}}</div>
<div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.cpuPercent &gt; 80}">{{ data?.system?.cpuPercent }}%</div>
<div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores &middot; load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
</div>
<div class="ov-kpi-card">
<div class="ov-kpi-label">Memory</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.memPercent > 85}">{{data.system.memPercent}}%</div>
<div class="ov-kpi-sub">{{humanBytes(data.system.memUsed)}} / {{humanBytes(data.system.memTotal)}}</div>
<div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.memPercent &gt; 85}">{{ data?.system?.memPercent }}%</div>
<div class="ov-kpi-sub">{{ humanBytes(data?.system?.memUsed) }} / {{ humanBytes(data?.system?.memTotal) }}</div>
</div>
<div class="ov-kpi-card">
<div class="ov-kpi-label">Disk</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.diskPercent > 85}">{{data.system.diskPercent}}%</div>
<div class="ov-kpi-sub">{{humanBytes(data.system.diskUsed)}} / {{humanBytes(data.system.diskTotal)}} &middot; {{data.system.diskMount}}</div>
<div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.diskPercent &gt; 85}">{{ data?.system?.diskPercent }}%</div>
<div class="ov-kpi-sub">{{ humanBytes(data?.system?.diskUsed) }} / {{ humanBytes(data?.system?.diskTotal) }} &middot; {{ data?.system?.diskMount }}</div>
</div>
<div class="ov-kpi-card">
<div class="ov-kpi-label">Uptime</div>
<div class="ov-kpi-value">{{humanDuration(data.system.uptime)}}</div>
<div class="ov-kpi-sub">{{data.system.nodeVersion}} &middot; {{data.system.platform}}</div>
<div class="ov-kpi-value">{{ humanDuration(data?.system?.uptime) }}</div>
<div class="ov-kpi-sub">{{ data?.system?.nodeVersion }} &middot; {{ data?.system?.platform }}</div>
</div>
</section>
@@ -52,17 +52,17 @@
<section class="ov-daily-row">
<div class="ov-daily-card">
<div class="ov-daily-label">New repos today</div>
<div class="ov-daily-value">+{{data.daily.today.repositories | number}}</div>
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.repositories) }}</div>
<div class="ov-daily-sub">since yesterday</div>
</div>
<div class="ov-daily-card">
<div class="ov-daily-label">New users today</div>
<div class="ov-daily-value">+{{data.daily.today.users | number}}</div>
<div class="ov-daily-sub">{{data.users.total | number}} total users</div>
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.users) }}</div>
<div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total users</div>
</div>
<div class="ov-daily-card">
<div class="ov-daily-label">Page views today</div>
<div class="ov-daily-value">+{{data.daily.today.pageViews | number}}</div>
<div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.pageViews) }}</div>
<div class="ov-daily-sub">since yesterday</div>
</div>
</section>
@@ -74,17 +74,15 @@
<span class="ov-chart-title">Daily page views &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views"
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views">
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
<div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;">
<span class="ov-spark-fill" :style="{height: historyBarH(d, &#x27;dailyPageViews&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
<span>{{historyLabel(data.history[0])}}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
<div class="ov-spark-x" v-if="data?.history?.length">
<span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div>
</div>
<div class="ov-chart-card">
@@ -92,17 +90,15 @@
<span class="ov-chart-title">New repos &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos"
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos">
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
<div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;">
<span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, &#x27;dailyRepositories&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
<span>{{historyLabel(data.history[0])}}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
<div class="ov-spark-x" v-if="data?.history?.length">
<span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div>
</div>
<div class="ov-chart-card">
@@ -110,17 +106,15 @@
<span class="ov-chart-title">Users &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
ng-attr-data-tip="{{historyLabel(d)}}: {{d.nbUsers | number}} users"
ng-attr-aria-label="{{historyLabel(d)}}: {{d.nbUsers | number}} users">
<span class="ov-spark-fill ov-spark-fill-user" ng-style="{height: historyBarH(d, 'nbUsers') + 'px'}"></span>
<div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;">
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyBarH(d, &#x27;nbUsers&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
<span>{{historyLabel(data.history[0])}}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
<div class="ov-spark-x" v-if="data?.history?.length">
<span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div>
</div>
<div class="ov-chart-card">
@@ -128,17 +122,15 @@
<span class="ov-chart-title">New users &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index"
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users"
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users">
<span class="ov-spark-fill ov-spark-fill-new-user" ng-style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
<div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;">
<span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, &#x27;dailyUsers&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
<span>{{historyLabel(data.history[0])}}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
<div class="ov-spark-x" v-if="data?.history?.length">
<span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div>
</div>
</section>
@@ -150,21 +142,21 @@
<div class="ov-panel-card">
<div class="ov-panel-head">
<span class="ov-panel-title">Repo status</span>
<span class="ov-panel-meta">{{data.repos.total | number}} total</span>
<span class="ov-panel-meta">{{ fmt?.number(data?.repos?.total) }} total</span>
</div>
<div class="ov-stacked-bar">
<span class="ov-bar-seg ov-bar-ready" ng-style="{width: barPct('ready') + '%'}" title="Ready"></span>
<span class="ov-bar-seg ov-bar-preparing" ng-style="{width: barPct('preparing') + '%'}" title="Preparing"></span>
<span class="ov-bar-seg ov-bar-error" ng-style="{width: barPct('error') + '%'}" title="Error"></span>
<span class="ov-bar-seg ov-bar-expired" ng-style="{width: barPct('expired') + '%'}" title="Expired"></span>
<span class="ov-bar-seg ov-bar-removed" ng-style="{width: barPct('removed') + '%'}" title="Removed"></span>
<span class="ov-bar-seg ov-bar-ready" title="Ready" :style="{width: barPct(&#x27;ready&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct(&#x27;preparing&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct(&#x27;error&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct(&#x27;expired&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct(&#x27;removed&#x27;) + &#x27;%&#x27;}"></span>
</div>
<div class="ov-bar-legend">
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{statusCount('ready') | number}}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{statusCount('preparing') + statusCount('download') | number}}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{statusCount('error') | number}}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{statusCount('expired') + statusCount('expiring') | number}}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{statusCount('removed') + statusCount('removing') | number}}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
</div>
</div>
@@ -172,27 +164,27 @@
<div class="ov-panel-card">
<div class="ov-panel-head">
<span class="ov-panel-title">Errors &middot; 24h</span>
<span class="ov-panel-meta">{{data.errors.last24h}} total</span>
<span class="ov-panel-meta">{{ data?.errors?.last24h }} total</span>
</div>
<div class="ov-error-bars">
<div class="ov-ebar-row">
<span class="ov-ebar-label">5xx</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" ng-style="{width: errPct('error') + '%'}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.error}}</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" :style="{width: errPct(&#x27;error&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{ data?.errors?.severity?.error }}</span>
</div>
<div class="ov-ebar-row">
<span class="ov-ebar-label">4xx</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" ng-style="{width: errPct('warn') + '%'}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.warn}}</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" :style="{width: errPct(&#x27;warn&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{ data?.errors?.severity?.warn }}</span>
</div>
<div class="ov-ebar-row">
<span class="ov-ebar-label">Info</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" ng-style="{width: errPct('info') + '%'}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.info}}</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" :style="{width: errPct(&#x27;info&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{ data?.errors?.severity?.info }}</span>
</div>
</div>
<div class="ov-panel-foot" ng-if="data.repos.recentErrors24h">
<a href="/admin/errors">{{data.repos.recentErrors24h}} repos in error state &rarr;</a>
<div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
<a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state &rarr;</a>
</div>
</div>
@@ -211,21 +203,21 @@
</div>
<a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">download</span>
<span class="ov-route-n">{{data.queues.download.active}}</span>
<span class="ov-route-n">{{data.queues.download.waiting}}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.download.failed > 0}">{{data.queues.download.failed}}</span>
<span class="ov-route-n">{{ data?.queues?.download?.active }}</span>
<span class="ov-route-n">{{ data?.queues?.download?.waiting }}</span>
<span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.download?.failed &gt; 0}">{{ data?.queues?.download?.failed }}</span>
</a>
<a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">remove</span>
<span class="ov-route-n">{{data.queues.remove.active}}</span>
<span class="ov-route-n">{{data.queues.remove.waiting}}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.remove.failed > 0}">{{data.queues.remove.failed}}</span>
<span class="ov-route-n">{{ data?.queues?.remove?.active }}</span>
<span class="ov-route-n">{{ data?.queues?.remove?.waiting }}</span>
<span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.remove?.failed &gt; 0}">{{ data?.queues?.remove?.failed }}</span>
</a>
<a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">cache</span>
<span class="ov-route-n">{{data.queues.cache.active}}</span>
<span class="ov-route-n">{{data.queues.cache.waiting}}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.cache.failed > 0}">{{data.queues.cache.failed}}</span>
<span class="ov-route-n">{{ data?.queues?.cache?.active }}</span>
<span class="ov-route-n">{{ data?.queues?.cache?.waiting }}</span>
<span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.cache?.failed &gt; 0}">{{ data?.queues?.cache?.failed }}</span>
</a>
</div>
</div>
@@ -236,46 +228,46 @@
<section class="ov-services-card">
<div class="ov-services-head">
<span class="ov-panel-title">Services</span>
<span class="ov-panel-meta">{{data.users.total | number}} users &middot; {{data.conferences.total}} conferences</span>
<span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users &middot; {{ data?.conferences?.total }} conferences</span>
</div>
<div class="ov-services-grid">
<div class="ov-svc">
<span class="ov-svc-dot ov-dot-ok"></span>
<div class="ov-svc-info">
<span class="ov-svc-name">web</span>
<span class="ov-svc-meta">{{data.system.nodeVersion}}</span>
<span class="ov-svc-meta">{{ data?.system?.nodeVersion }}</span>
</div>
<span class="ov-svc-detail">uptime {{humanDuration(data.system.uptime)}}</span>
<span class="ov-svc-detail">uptime {{ humanDuration(data?.system?.uptime) }}</span>
</div>
<div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.download) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info">
<span class="ov-svc-name">download</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.download)">{{queueTotal(data.queues.download)}} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.download)">idle</span>
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} jobs</span>
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.download)">idle</span>
</div>
</div>
<div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.cache) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info">
<span class="ov-svc-name">cache</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.cache)">{{queueTotal(data.queues.cache)}} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.cache)">idle</span>
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} jobs</span>
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.cache)">idle</span>
</div>
</div>
<div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.remove) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info">
<span class="ov-svc-name">remove</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.remove)">{{queueTotal(data.queues.remove)}} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.remove)">idle</span>
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} jobs</span>
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.remove)">idle</span>
</div>
</div>
<div class="ov-svc">
<span class="ov-svc-dot" ng-class="data.repos.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span>
<span class="ov-svc-dot" :class="data?.repos?.recentErrors24h &gt; 10 ? &#x27;ov-dot-warn&#x27; : &#x27;ov-dot-ok&#x27;"></span>
<div class="ov-svc-info">
<span class="ov-svc-name">errors</span>
<span class="ov-svc-meta">{{data.errors.last24h}} / 24h</span>
<span class="ov-svc-meta">{{ data?.errors?.last24h }} / 24h</span>
</div>
</div>
</div>