449 lines
19 KiB
Django/Jinja
449 lines
19 KiB
Django/Jinja
<!doctype html>
|
|||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<title>AVWatch — {{ generated_local }}</title>
|
||
|
|
<style>
|
||
|
|
:root{
|
||
|
|
--bg:#f7f7f5; --panel:#ffffff; --ink:#1a1a18; --muted:#6b6b66;
|
||
|
|
--line:#e3e3de; --line-soft:#efefea;
|
||
|
|
--go:#1f7a3d; --go-bg:#e8f5ec;
|
||
|
|
--wait:#8a6100; --wait-bg:#fdf3dd;
|
||
|
|
--avoid:#a3231d; --avoid-bg:#fbe9e7;
|
||
|
|
--manual:#4a5878; --manual-bg:#eceff5;
|
||
|
|
--accent:#2b5c8a;
|
||
|
|
}
|
||
|
|
@media (prefers-color-scheme: dark){
|
||
|
|
:root:not([data-theme="light"]){
|
||
|
|
--bg:#16181a; --panel:#1e2124; --ink:#e8e8e4; --muted:#9a9a94;
|
||
|
|
--line:#31353a; --line-soft:#272b2f;
|
||
|
|
--go:#6ede93; --go-bg:#16301f;
|
||
|
|
--wait:#f0c46a; --wait-bg:#33280f;
|
||
|
|
--avoid:#ff8b80; --avoid-bg:#3a1a17;
|
||
|
|
--manual:#a8b8d8; --manual-bg:#1e2532;
|
||
|
|
--accent:#7fb2e0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
*{box-sizing:border-box}
|
||
|
|
body{margin:0;background:var(--bg);color:var(--ink);
|
||
|
|
font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
|
||
|
|
.wrap{max-width:1080px;margin:0 auto;padding:28px 20px 80px}
|
||
|
|
header{border-bottom:2px solid var(--ink);padding-bottom:16px;margin-bottom:24px}
|
||
|
|
h1{margin:0 0 4px;font-size:26px;letter-spacing:-.02em}
|
||
|
|
.sub{color:var(--muted);font-size:13px}
|
||
|
|
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));
|
||
|
|
gap:10px;margin:20px 0 28px}
|
||
|
|
.tile{background:var(--panel);border:1px solid var(--line);border-radius:8px;
|
||
|
|
padding:12px 14px}
|
||
|
|
.tile .n{font-size:26px;font-weight:650;letter-spacing:-.02em;line-height:1.1}
|
||
|
|
.tile .l{font-size:11px;text-transform:uppercase;letter-spacing:.07em;
|
||
|
|
color:var(--muted);margin-top:3px}
|
||
|
|
h2{font-size:13px;text-transform:uppercase;letter-spacing:.09em;
|
||
|
|
color:var(--muted);margin:34px 0 12px;padding-bottom:6px;
|
||
|
|
border-bottom:1px solid var(--line)}
|
||
|
|
h2 .count{color:var(--ink);font-weight:600}
|
||
|
|
|
||
|
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:9px;
|
||
|
|
padding:15px 17px;margin-bottom:11px}
|
||
|
|
.card.avoid{border-left:4px solid var(--avoid)}
|
||
|
|
.card.wait{border-left:4px solid var(--wait)}
|
||
|
|
.card.go{border-left:4px solid var(--go)}
|
||
|
|
.card.manual-check{border-left:4px solid var(--manual)}
|
||
|
|
.chead{display:flex;flex-wrap:wrap;gap:10px;align-items:baseline}
|
||
|
|
.name{font-weight:640;font-size:16px;flex:1 1 260px}
|
||
|
|
.vendor{color:var(--muted);font-weight:400;font-size:13px}
|
||
|
|
.badge{font-size:11px;font-weight:700;text-transform:uppercase;
|
||
|
|
letter-spacing:.06em;padding:3px 9px;border-radius:99px;white-space:nowrap}
|
||
|
|
.badge.go{background:var(--go-bg);color:var(--go)}
|
||
|
|
.badge.wait{background:var(--wait-bg);color:var(--wait)}
|
||
|
|
.badge.avoid{background:var(--avoid-bg);color:var(--avoid)}
|
||
|
|
.badge.manual-check{background:var(--manual-bg);color:var(--manual)}
|
||
|
|
.badge.unknown{background:var(--manual-bg);color:var(--manual)}
|
||
|
|
.tag{font-size:10.5px;color:var(--muted);border:1px solid var(--line);
|
||
|
|
padding:2px 7px;border-radius:99px;text-transform:uppercase;letter-spacing:.05em}
|
||
|
|
.tag.new{border-color:var(--accent);color:var(--accent);font-weight:650}
|
||
|
|
.tag.crit{border-color:var(--avoid);color:var(--avoid);font-weight:650}
|
||
|
|
|
||
|
|
.vers{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||
|
|
font-size:13px;margin:9px 0 4px}
|
||
|
|
.vers .from{color:var(--muted)} .vers .arrow{color:var(--muted);margin:0 5px}
|
||
|
|
.vers .to{font-weight:650}
|
||
|
|
.headline{margin:8px 0 2px;font-size:14.5px;font-weight:560}
|
||
|
|
ul.reasons{margin:7px 0 0;padding-left:19px}
|
||
|
|
ul.reasons li{margin:3px 0;font-size:13.5px}
|
||
|
|
|
||
|
|
.scores{display:flex;gap:18px;flex-wrap:wrap;margin:11px 0 0;
|
||
|
|
font-size:11.5px;color:var(--muted)}
|
||
|
|
.meter{display:flex;align-items:center;gap:7px}
|
||
|
|
.bar{width:88px;height:5px;background:var(--line-soft);border-radius:99px;
|
||
|
|
overflow:hidden}
|
||
|
|
.bar i{display:block;height:100%;border-radius:99px}
|
||
|
|
.disagree{color:var(--wait);font-weight:650}
|
||
|
|
|
||
|
|
details.ev{margin-top:10px;border-top:1px solid var(--line-soft);padding-top:8px}
|
||
|
|
details.ev summary{cursor:pointer;font-size:12px;color:var(--muted);
|
||
|
|
text-transform:uppercase;letter-spacing:.06em}
|
||
|
|
.evrow{padding:7px 0;border-bottom:1px solid var(--line-soft);font-size:13px}
|
||
|
|
.evrow:last-child{border-bottom:0}
|
||
|
|
.evrow a{color:var(--accent);text-decoration:none}
|
||
|
|
.evrow a:hover{text-decoration:underline}
|
||
|
|
.evmeta{font-size:11px;color:var(--muted);margin-top:2px}
|
||
|
|
.sev{font-weight:700;text-transform:uppercase;font-size:10px;
|
||
|
|
letter-spacing:.05em;padding:1px 5px;border-radius:3px;margin-right:5px}
|
||
|
|
.sev.brick{background:var(--avoid-bg);color:var(--avoid)}
|
||
|
|
.sev.crash{background:var(--avoid-bg);color:var(--avoid)}
|
||
|
|
.sev.regression{background:var(--wait-bg);color:var(--wait)}
|
||
|
|
.sev.annoyance,.sev.info{background:var(--line-soft);color:var(--muted)}
|
||
|
|
.snip{color:var(--muted);font-size:12.5px;margin-top:3px}
|
||
|
|
|
||
|
|
table{width:100%;border-collapse:collapse;font-size:13.5px;
|
||
|
|
background:var(--panel);border:1px solid var(--line);border-radius:8px;
|
||
|
|
overflow:hidden}
|
||
|
|
th{text-align:left;font-size:11px;text-transform:uppercase;
|
||
|
|
letter-spacing:.06em;color:var(--muted);padding:9px 12px;
|
||
|
|
border-bottom:1px solid var(--line);font-weight:600}
|
||
|
|
td{padding:8px 12px;border-bottom:1px solid var(--line-soft)}
|
||
|
|
tr:last-child td{border-bottom:0}
|
||
|
|
td.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||
|
|
font-size:12.5px}
|
||
|
|
.scroll{overflow-x:auto}
|
||
|
|
a.dl{color:var(--accent);text-decoration:none;font-size:12.5px}
|
||
|
|
a.dl:hover{text-decoration:underline}
|
||
|
|
.empty{color:var(--muted);font-style:italic;padding:10px 0;font-size:13.5px}
|
||
|
|
footer{margin-top:48px;padding-top:14px;border-top:1px solid var(--line);
|
||
|
|
color:var(--muted);font-size:11.5px;line-height:1.7}
|
||
|
|
code{background:var(--line-soft);padding:1px 5px;border-radius:3px;font-size:12px}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="wrap">
|
||
|
|
|
||
|
|
<header>
|
||
|
|
<h1>AVWatch</h1>
|
||
|
|
<div class="sub">
|
||
|
|
Update & risk briefing for the production fleet ·
|
||
|
|
generated {{ generated_local }} ·
|
||
|
|
run <code>{{ run.run_id }}</code> ·
|
||
|
|
verdicts by {{ engine_label }}
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="tiles">
|
||
|
|
<div class="tile"><div class="n">{{ run.items_checked }}</div><div class="l">Tracked</div></div>
|
||
|
|
<div class="tile"><div class="n" style="color:var(--wait)">{{ run.updates_found }}</div><div class="l">Behind</div></div>
|
||
|
|
<div class="tile"><div class="n" style="color:var(--avoid)">{{ groups.avoid|length }}</div><div class="l">Do not install</div></div>
|
||
|
|
<div class="tile"><div class="n" style="color:var(--go)">{{ groups.go_behind|length }}</div><div class="l">Cleared to go</div></div>
|
||
|
|
<div class="tile"><div class="n" style="color:var(--accent)">{{ run.new_since_last }}</div><div class="l">New this run</div></div>
|
||
|
|
<div class="tile"><div class="n">{{ groups.no_baseline|length }}</div><div class="l">No baseline</div></div>
|
||
|
|
{% if groups.needs_setup %}
|
||
|
|
<div class="tile"><div class="n" style="color:var(--accent)">{{ groups.needs_setup|length }}</div><div class="l">Not checked</div></div>
|
||
|
|
{% endif %}
|
||
|
|
<div class="tile"><div class="n">{{ groups.manual|length }}</div><div class="l">No firmware</div></div>
|
||
|
|
<div class="tile"><div class="n" style="color:var(--avoid)">{{ run.errors }}</div><div class="l">Source errors</div></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{% macro card(f) %}
|
||
|
|
{% set v = f.verdict %}
|
||
|
|
<div class="card {{ v.recommendation if v else 'unknown' }}">
|
||
|
|
<div class="chead">
|
||
|
|
<div class="name">{{ f.item.name }}
|
||
|
|
<span class="vendor">— {{ f.item.vendor }}</span></div>
|
||
|
|
{% if f.is_new_since_last_run %}<span class="tag new">new</span>{% endif %}
|
||
|
|
{% if f.item.is_showstopper %}<span class="tag crit">{{ f.item.criticality }}</span>{% endif %}
|
||
|
|
<span class="tag">{{ f.item.category }}</span>
|
||
|
|
<span class="badge {{ v.recommendation if v else 'unknown' }}">
|
||
|
|
{{ (v.recommendation if v else 'unknown')|replace('-',' ') }}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{% if f.check.latest %}
|
||
|
|
<div class="vers">
|
||
|
|
<span class="from">{{ f.item.installed or 'unknown' }}</span>
|
||
|
|
<span class="arrow">→</span>
|
||
|
|
<span class="to">{{ f.check.latest }}</span>
|
||
|
|
{% if f.bump not in ('none','unknown') %}
|
||
|
|
<span class="from">({{ f.bump }}{% if f.check.age_days is not none %},
|
||
|
|
released {{ f.check.age_days|round(0)|int }}d ago{% endif %})</span>
|
||
|
|
{% elif f.check.age_days is not none %}
|
||
|
|
<span class="from">(released {{ f.check.age_days|round(0)|int }}d ago)</span>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if v %}
|
||
|
|
{% if v.headline %}<div class="headline">{{ v.headline }}</div>{% endif %}
|
||
|
|
{% if v.reasons %}
|
||
|
|
<ul class="reasons">
|
||
|
|
{% for r in v.reasons %}<li>{{ r }}</li>{% endfor %}
|
||
|
|
{% if v.suggested_wait_days %}
|
||
|
|
<li><strong>Suggested: revisit in about {{ v.suggested_wait_days }} days.</strong></li>
|
||
|
|
{% endif %}
|
||
|
|
</ul>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<div class="scores">
|
||
|
|
<span class="meter">Risk
|
||
|
|
<span class="bar"><i style="width:{{ v.risk_score }}%;background:{{
|
||
|
|
'var(--avoid)' if v.risk_score > 55 else ('var(--wait)' if v.risk_score >= 25 else 'var(--go)')
|
||
|
|
}}"></i></span>
|
||
|
|
{{ v.risk_score|round(0)|int }}/100</span>
|
||
|
|
<span class="meter">Rule-based
|
||
|
|
<span class="bar"><i style="width:{{ v.heuristic_score }}%;background:var(--muted)"></i></span>
|
||
|
|
{{ v.heuristic_score|round(0)|int }}/100
|
||
|
|
→ {{ v.heuristic_recommendation|replace('-',' ') }}</span>
|
||
|
|
<span>Confidence {{ v.confidence }}</span>
|
||
|
|
{% if v.heuristic_recommendation != v.recommendation
|
||
|
|
and v.heuristic_recommendation not in ('unknown',) %}
|
||
|
|
<span class="disagree">⚠ engines disagree — read the evidence</span>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if f.check.detail and f.check.status != 'ok' %}
|
||
|
|
<div class="snip" style="margin-top:8px">{{ f.check.detail }}</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if f.coupling %}
|
||
|
|
<div style="margin-top:10px;padding:9px 12px;border-radius:5px;
|
||
|
|
background:{{ 'var(--avoid-bg)' if f.has_blocking_coupling else 'var(--line-soft)' }};
|
||
|
|
border-left:3px solid {{ 'var(--avoid)' if f.has_blocking_coupling else 'var(--muted)' }}">
|
||
|
|
<div style="font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;
|
||
|
|
color:var(--muted);margin-bottom:5px">Version dependencies
|
||
|
|
{% if f.item.update_group %} · group “{{ f.item.update_group }}”{% endif %}</div>
|
||
|
|
{% for c in f.coupling %}
|
||
|
|
<div style="font-size:13px;margin:3px 0">
|
||
|
|
<span class="sev {{ 'brick' if c.severity == 'blocking' else 'annoyance' }}"
|
||
|
|
>{{ c.severity }}</span>{{ c.message }}
|
||
|
|
</div>
|
||
|
|
{% endfor %}
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if f.evidence %}
|
||
|
|
<details class="ev"{% if v and v.recommendation == 'avoid' %} open{% endif %}>
|
||
|
|
<summary>{{ f.evidence|length }} community post(s) found — the raw evidence</summary>
|
||
|
|
{% for e in f.evidence %}
|
||
|
|
<div class="evrow">
|
||
|
|
<span class="sev {{ e.severity }}">{{ e.severity }}</span>
|
||
|
|
{% if e.url %}<a href="{{ e.url }}" target="_blank" rel="noopener">{{ e.title }}</a>
|
||
|
|
{% else %}{{ e.title }}{% endif %}
|
||
|
|
<div class="evmeta">{{ e.source }}{% if e.published %} ·
|
||
|
|
{{ e.published.strftime('%d %b %Y') }}{% endif %}{% if e.score is not none %} ·
|
||
|
|
score {{ e.score }}{% endif %} · keyword read: {{ e.sentiment }}</div>
|
||
|
|
{% if e.snippet %}<div class="snip">{{ e.snippet[:300] }}</div>{% endif %}
|
||
|
|
</div>
|
||
|
|
{% endfor %}
|
||
|
|
</details>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if f.item.check_url or f.check.release_url %}
|
||
|
|
<div style="margin-top:9px">
|
||
|
|
<a class="dl" href="{{ f.check.release_url or f.item.check_url }}"
|
||
|
|
target="_blank" rel="noopener">Vendor download / release notes →</a>
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
{% endmacro %}
|
||
|
|
|
||
|
|
{% if groups.mismatched %}
|
||
|
|
<h2 style="color:var(--avoid);border-bottom-color:var(--avoid)">
|
||
|
|
Version mismatch — broken now
|
||
|
|
<span class="count">({{ groups.mismatched|length }})</span></h2>
|
||
|
|
<p class="sub" style="margin:-4px 0 12px">
|
||
|
|
These are running versions that do not match gear they are <em>required</em>
|
||
|
|
to match. This is a present fault, not a future risk: the pair will not
|
||
|
|
link, the panel will not connect, or the wall will not light. Fix these
|
||
|
|
before considering any other update on this page.
|
||
|
|
</p>
|
||
|
|
{% for f in groups.mismatched %}{{ card(f) }}{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.avoid %}
|
||
|
|
<h2>Do not install <span class="count">({{ groups.avoid|length }})</span></h2>
|
||
|
|
{% for f in groups.avoid %}{{ card(f) }}{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.wait %}
|
||
|
|
<h2>Hold off for now <span class="count">({{ groups.wait|length }})</span></h2>
|
||
|
|
{% for f in groups.wait %}{{ card(f) }}{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.go_behind %}
|
||
|
|
<h2>Cleared to update <span class="count">({{ groups.go_behind|length }})</span></h2>
|
||
|
|
{% for f in groups.go_behind %}{{ card(f) }}{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.needs_setup %}
|
||
|
|
<h2>Could not be checked this run <span class="count">({{ groups.needs_setup|length }})</span></h2>
|
||
|
|
<div class="card manual-check" style="border-left-color:var(--accent)">
|
||
|
|
<div class="headline">These {{ groups.needs_setup|length }} items are tracked
|
||
|
|
— AVWatch just could not read their versions.</div>
|
||
|
|
<p style="margin:8px 0 0;font-size:13.5px">
|
||
|
|
This is a configuration gap, not a gap in the inventory. Every item below
|
||
|
|
is in <code>inventory.yaml</code> with a vendor, a search query and a
|
||
|
|
criticality already set. Fix the reason and they start reporting versions
|
||
|
|
on the next run.
|
||
|
|
</p>
|
||
|
|
{% set by_reason = {} %}
|
||
|
|
{% for f in groups.needs_setup %}
|
||
|
|
{% set r = f.check.detail or 'unknown reason' %}
|
||
|
|
{% if by_reason.update({r: (by_reason.get(r, []) + [f])}) %}{% endif %}
|
||
|
|
{% endfor %}
|
||
|
|
<ul class="reasons" style="margin-top:10px">
|
||
|
|
{% for reason, fs in by_reason.items() %}
|
||
|
|
<li><strong>{{ reason }}</strong><br>
|
||
|
|
<span style="color:var(--muted);font-size:12.5px">affects
|
||
|
|
{{ fs|length }} item(s)</span></li>
|
||
|
|
{% endfor %}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Vendor</th><th>Category</th>
|
||
|
|
<th>Criticality</th><th>Why not checked</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.needs_setup %}
|
||
|
|
<tr>
|
||
|
|
<td>{{ f.item.name }}</td>
|
||
|
|
<td>{{ f.item.vendor }}</td>
|
||
|
|
<td>{{ f.item.category }}</td>
|
||
|
|
<td>{{ f.item.criticality }}</td>
|
||
|
|
<td style="color:var(--muted)">{{ f.check.detail }}</td>
|
||
|
|
</tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.no_baseline %}
|
||
|
|
<h2>No installed version on record <span class="count">({{ groups.no_baseline|length }})</span></h2>
|
||
|
|
<p class="sub" style="margin:-4px 0 12px">
|
||
|
|
AVWatch found the current version for these, but has no record of what you
|
||
|
|
actually run — so it cannot tell you whether you are behind. Record each one
|
||
|
|
and it never has to ask again. This is the single highest-value thing to
|
||
|
|
fix in this report.
|
||
|
|
</p>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Latest available</th><th>Released</th>
|
||
|
|
<th>Record what you run</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.no_baseline %}
|
||
|
|
<tr>
|
||
|
|
<td>{{ f.item.name }}<br>
|
||
|
|
<span class="vendor">{{ f.item.vendor }}</span></td>
|
||
|
|
<td class="mono"><strong>{{ f.check.latest }}</strong></td>
|
||
|
|
<td>{% if f.check.released %}{{ f.check.released.strftime('%d %b %Y') }}
|
||
|
|
{% else %}—{% endif %}</td>
|
||
|
|
<td><code>avwatch set-installed {{ f.item.id }} <version></code></td>
|
||
|
|
</tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.manual %}
|
||
|
|
<h2>Nothing to track <span class="count">({{ groups.manual|length }})</span></h2>
|
||
|
|
<p class="sub" style="margin:-4px 0 12px">
|
||
|
|
These have no field-updatable firmware or software at all — passive or
|
||
|
|
analogue gear. Listed for completeness so the inventory matches the room;
|
||
|
|
they will never produce an update.
|
||
|
|
</p>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Vendor</th><th>We think we run</th>
|
||
|
|
<th>Criticality</th><th>Where to look</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.manual %}
|
||
|
|
<tr>
|
||
|
|
<td>{{ f.item.name }}</td>
|
||
|
|
<td>{{ f.item.vendor }}</td>
|
||
|
|
<td class="mono">{{ f.item.installed or '—' }}</td>
|
||
|
|
<td>{{ f.item.criticality }}</td>
|
||
|
|
<td>{% if f.item.check_url %}<a class="dl" href="{{ f.item.check_url }}"
|
||
|
|
target="_blank" rel="noopener">vendor page →</a>{% else %}—{% endif %}</td>
|
||
|
|
</tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.errors %}
|
||
|
|
<h2>Source errors <span class="count">({{ groups.errors|length }})</span></h2>
|
||
|
|
<p class="sub" style="margin:-4px 0 12px">
|
||
|
|
A check that used to work and now fails almost always means the vendor
|
||
|
|
changed their page. Fix the <code>pattern</code> for that item in
|
||
|
|
<code>inventory.yaml</code>.
|
||
|
|
</p>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Source</th><th>What went wrong</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.errors %}
|
||
|
|
<tr><td>{{ f.item.name }}</td><td class="mono">{{ f.item.source_kind }}</td>
|
||
|
|
<td>{{ f.check.detail }}</td></tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.frozen %}
|
||
|
|
<h2>Frozen by policy <span class="count">({{ groups.frozen|length }})</span></h2>
|
||
|
|
<p class="sub" style="margin:-4px 0 12px">
|
||
|
|
Deliberately held at their current version via <code>freeze: true</code>.
|
||
|
|
Listed so the freeze stays visible, not because anything needs doing.
|
||
|
|
</p>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Held at</th><th>Available</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.frozen %}
|
||
|
|
<tr><td>{{ f.item.name }}</td>
|
||
|
|
<td class="mono">{{ f.item.installed or '—' }}</td>
|
||
|
|
<td class="mono">{{ f.check.latest or '—' }}</td></tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.current %}
|
||
|
|
<h2>Up to date <span class="count">({{ groups.current|length }})</span></h2>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Vendor</th><th>Version</th><th>Category</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.current %}
|
||
|
|
<tr><td>{{ f.item.name }}</td><td>{{ f.item.vendor }}</td>
|
||
|
|
<td class="mono">{{ f.check.latest }}</td><td>{{ f.item.category }}</td></tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if groups.ahead %}
|
||
|
|
<h2>Installed version is ahead of the public release <span class="count">({{ groups.ahead|length }})</span></h2>
|
||
|
|
<div class="scroll"><table>
|
||
|
|
<thead><tr><th>Item</th><th>Inventory says</th><th>Newest public</th></tr></thead>
|
||
|
|
<tbody>
|
||
|
|
{% for f in groups.ahead %}
|
||
|
|
<tr><td>{{ f.item.name }}</td><td class="mono">{{ f.item.installed }}</td>
|
||
|
|
<td class="mono">{{ f.check.latest }}</td></tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table></div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<footer>
|
||
|
|
<strong>How to read this.</strong> Every verdict shows two independent
|
||
|
|
scores: the reasoning verdict and a crude rule-based one. When they
|
||
|
|
disagree, the evidence links are the tiebreaker — that is what they are
|
||
|
|
there for.<br>
|
||
|
|
Community posts are unverified writing by strangers. AVWatch ranks and
|
||
|
|
summarises them; it does not vouch for them.<br>
|
||
|
|
Nothing here is a substitute for a rollback plan and a rehearsal before a
|
||
|
|
service.<br>
|
||
|
|
Sources this run: {{ run.http_requests }} HTTP requests, {{ run.cache_hits }}
|
||
|
|
served from cache.
|
||
|
|
Record what you actually did with
|
||
|
|
<code>avwatch mark <item-id> installed</code> so future briefings stay
|
||
|
|
accurate.
|
||
|
|
</footer>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|