Send a bug report to one place, on purpose, after reading it
The page built its own markdown and nothing could send it; local and upstream are different people, so neither falls back to the other.
This commit is contained in:
@@ -124,6 +124,17 @@
|
||||
# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
|
||||
HOSTN_GITEA_API_TOKEN=""
|
||||
|
||||
# ━━━ Bug Reports ━━━
|
||||
# Only used when BUG_REPORT_LOCAL_ENABLED=true in master.conf. Reports then go to this Gitea
|
||||
# instead of GitHub — and stay there, so they do not reach the Varaverk maintainer.
|
||||
#
|
||||
# Reached locally or over Tailscale, so the token never crosses the public proxy and no Authelia
|
||||
# bypass is needed. It is a credential and lives here rather than master.conf for that reason;
|
||||
# it is never shipped, and the settings UI masks it.
|
||||
HOSTN_BUG_REPORT_URL="" # e.g. http://gitea:3000 or the tailnet name
|
||||
HOSTN_BUG_REPORT_REPO="" # owner/repo
|
||||
HOSTN_BUG_REPORT_TOKEN="" # Gitea API token with issue-write on that repo
|
||||
|
||||
# ━━━ Notifications ━━━
|
||||
# Discord webhook — leave blank to disable.
|
||||
HOSTN_DISCORD_WEBHOOK=""
|
||||
|
||||
@@ -1922,6 +1922,22 @@
|
||||
# anything that in practice means "mine". Matched as literal text, not as patterns.
|
||||
AI_CHAT_MY_SYSTEM_PHRASES=""
|
||||
|
||||
# ━━━ Bug Reports ━━━
|
||||
# A bug the assistant files is written here and goes nowhere until the owner sends it. Nothing is
|
||||
# ever transmitted automatically — the report is shown in full, read-only, and sending is a
|
||||
# second, separate press.
|
||||
#
|
||||
# Two destinations, and they are not a fallback chain. Fetching code from several mirrors is
|
||||
# harmless because they all serve the same thing; sending a report is not, because the
|
||||
# destinations are different people. Local ships OFF so an install that has configured nothing
|
||||
# reports upstream rather than silently into a tracker nobody reads.
|
||||
#
|
||||
# LOCAL ON — reports go to your own Gitea (see HOSTN_BUG_REPORT_* in host*.conf) and stay there.
|
||||
# They do NOT reach the Varaverk maintainer. Turn it on if you want your own backlog.
|
||||
# LOCAL OFF — reports open a prefilled GitHub issue you submit under your own account.
|
||||
BUG_REPORT_LOCAL_ENABLED=false
|
||||
BUG_REPORT_GITHUB_REPO="FailedProxy/Varaverk"
|
||||
|
||||
# ━━━ AI Conf Write Access ━━━
|
||||
# Separate switch from AI_ENABLED, off by default, and an explicit key whitelist. Never paths,
|
||||
# never credentials, never a container name. An empty whitelist means no writes regardless of
|
||||
|
||||
@@ -382,6 +382,52 @@ if ($action === 'bug_close') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// The report, rendered server-side. Read-only by design: what the operator reviews is byte for
|
||||
// byte what gets sent, so approving one text and transmitting another is not possible. It is also
|
||||
// the only renderer — the page used to build its own markdown, which is two formats to keep in
|
||||
// step and one of them always losing.
|
||||
if ($action === 'bug_report') {
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
$bug = null;
|
||||
foreach (vv_ai_bugs_list(false) as $b) if (($b['id'] ?? '') === $id) { $bug = $b; break; }
|
||||
if (!$bug) { echo json_encode(['ok' => false, 'error' => 'no such report']); exit; }
|
||||
|
||||
$t = vv_ai_bug_targets();
|
||||
$title = '[' . ($bug['component'] ?? '?') . '] ' . ($bug['summary'] ?? '');
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'title' => $title,
|
||||
'markdown' => vv_ai_bug_report($bug),
|
||||
'targets' => $t,
|
||||
// Built here because the repo name lives here. Length is the caller's problem to notice:
|
||||
// GitHub truncates a very long query rather than refusing it, which would silently send a
|
||||
// half report — so the page checks and falls back to the copy box.
|
||||
'github' => 'https://github.com/' . $t['github_repo'] . '/issues/new?title='
|
||||
. rawurlencode($title) . '&body=' . rawurlencode(vv_ai_bug_report($bug)),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sends to the operator's own Gitea, and only there. Never falls back to GitHub on failure: the
|
||||
// two destinations are different people, and a silent substitution is how a report meant for a
|
||||
// private backlog ends up public.
|
||||
if ($action === 'bug_send_local') {
|
||||
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
$bug = null;
|
||||
foreach (vv_ai_bugs_list(false) as $b) if (($b['id'] ?? '') === $id) { $bug = $b; break; }
|
||||
if (!$bug) { echo json_encode(['ok' => false, 'error' => 'no such report']); exit; }
|
||||
|
||||
// Re-rendered from the store rather than taken from the request. The browser showed this text
|
||||
// read-only; accepting a body from the page would make that guarantee decorative.
|
||||
$r = vv_ai_bug_send_local('[' . ($bug['component'] ?? '?') . '] ' . ($bug['summary'] ?? ''),
|
||||
vv_ai_bug_report($bug));
|
||||
vv_ai_log(sprintf('bug_send_local id=%s %s', $id,
|
||||
$r['ok'] ? 'ok ' . ($r['url'] ?? '') : 'failed: ' . ($r['error'] ?? '?')));
|
||||
echo json_encode($r);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── findings / finding_action ─────────────────────────────────────────────────
|
||||
// What the repair sweep found, and the operator's answer to it. include/ai_repair.php is pulled
|
||||
// in here rather than at the top of the file: it is the largest include in the plugin and poll
|
||||
|
||||
@@ -1372,6 +1372,73 @@ function vv_ai_bug_report(array $b): string {
|
||||
return $out;
|
||||
}
|
||||
|
||||
// Where a report can go from this install, resolved once so the page and the sender agree.
|
||||
//
|
||||
// Local is opt-in and off by default, because a default that files into the operator's own
|
||||
// tracker means every report from every other install lands somewhere the maintainer never
|
||||
// looks — a fallback chain is right for fetching code and wrong for sending a report.
|
||||
function vv_ai_bug_targets(): array {
|
||||
$v = vv_conf_vars();
|
||||
$slot = strtoupper(vv_detect_host());
|
||||
$on = strtolower(trim($v['BUG_REPORT_LOCAL_ENABLED'] ?? 'false')) === 'true';
|
||||
|
||||
$url = trim((string) ($v[$slot . '_BUG_REPORT_URL'] ?? ''));
|
||||
$repo = trim((string) ($v[$slot . '_BUG_REPORT_REPO'] ?? ''));
|
||||
$token = trim((string) ($v[$slot . '_BUG_REPORT_TOKEN'] ?? ''));
|
||||
|
||||
return [
|
||||
// Configured is not the same as enabled: the switch is off but the details are filled in
|
||||
// is a state worth showing, because it is what "why did this go to GitHub" looks like.
|
||||
'local_enabled' => $on,
|
||||
'local_configured' => $url !== '' && $repo !== '' && $token !== '',
|
||||
'local_url' => $url,
|
||||
'local_repo' => $repo,
|
||||
'github_repo' => trim((string) ($v['BUG_REPORT_GITHUB_REPO'] ?? 'FailedProxy/Varaverk')),
|
||||
];
|
||||
}
|
||||
|
||||
// Files the report as an issue on the operator's own Gitea. Only ever reached when the switch is
|
||||
// on and the details are present — never as a fallback from a failed GitHub send, because the two
|
||||
// go to different people and quietly substituting one for the other is the whole failure mode
|
||||
// this design exists to avoid.
|
||||
function vv_ai_bug_send_local(string $title, string $body): array {
|
||||
$t = vv_ai_bug_targets();
|
||||
if (!$t['local_enabled']) return ['ok' => false, 'error' => 'local reporting is switched off'];
|
||||
if (!$t['local_configured']) return ['ok' => false, 'error' => 'local reporting is on but URL, repo or token is blank'];
|
||||
if (!function_exists('curl_init')) return ['ok' => false, 'error' => 'curl is unavailable'];
|
||||
|
||||
$v = vv_conf_vars();
|
||||
$slot = strtoupper(vv_detect_host());
|
||||
$token = trim((string) ($v[$slot . '_BUG_REPORT_TOKEN'] ?? ''));
|
||||
$api = rtrim($t['local_url'], '/') . '/api/v1/repos/' . trim($t['local_repo'], '/') . '/issues';
|
||||
|
||||
$ch = curl_init($api);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json',
|
||||
'Authorization: token ' . $token],
|
||||
CURLOPT_POSTFIELDS => json_encode(['title' => $title, 'body' => $body]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($resp === false) return ['ok' => false, 'error' => 'could not reach Gitea: ' . $err];
|
||||
if ($code === 401 || $code === 403) return ['ok' => false, 'error' => 'Gitea refused the token (HTTP ' . $code . ')'];
|
||||
if ($code === 404) return ['ok' => false, 'error' => 'Gitea has no such repo (HTTP 404) — check the owner/repo'];
|
||||
if ($code < 200 || $code >= 300) return ['ok' => false, 'error' => 'Gitea returned HTTP ' . $code];
|
||||
|
||||
$d = json_decode((string) $resp, true);
|
||||
// The URL back is the whole point of sending server-side rather than opening a form: it is
|
||||
// proof the issue exists, and somewhere to go and look at it.
|
||||
return ['ok' => true, 'url' => $d['html_url'] ?? '', 'number' => $d['number'] ?? null];
|
||||
}
|
||||
|
||||
// Best effort, and blank rather than wrong. /etc/unraid-version is a shell assignment; on
|
||||
// anything that is not Unraid there is simply no file and the report says "?".
|
||||
function vv_ai_bug_unraid_version(): string {
|
||||
|
||||
+98
-39
@@ -319,6 +319,20 @@ if (is_dir('/var/log/varaverk')) {
|
||||
</div>
|
||||
<div id="vv-ai-bugs"></div>
|
||||
<!-- Manual-copy fallback for plain-http WebGUIs, where the clipboard API is unavailable. -->
|
||||
<!-- Review, then send. The box is readonly because the text shown here is the text that
|
||||
goes — the server re-renders from the store rather than accepting a body from the page,
|
||||
so an editable box would only create a gap between what was approved and what was sent.
|
||||
Sending is a separate press from building, and nothing leaves on its own. -->
|
||||
<div id="vv-ai-bug-send" style="display:none;margin-top:8px;">
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<button class="vv-ai-btn" id="vv-ai-bug-local" type="button"
|
||||
style="display:none" onclick="vvAiBugSendLocal(this)">Send to Gitea</button>
|
||||
<button class="vv-ai-btn" id="vv-ai-bug-github" type="button"
|
||||
onclick="vvAiBugGithub()">Open a GitHub issue</button>
|
||||
<button class="vv-ai-btn ghost" type="button" onclick="vvAiBugCopy()">Copy</button>
|
||||
</div>
|
||||
<div class="vv-ai-set-d" id="vv-ai-bug-dest" style="margin-top:6px;"></div>
|
||||
</div>
|
||||
<textarea id="vv-ai-bug-copybox" class="vv-ai-input" rows="8" readonly spellcheck="false"
|
||||
style="display:none;margin-top:8px" onclick="this.select()"></textarea>
|
||||
</div>
|
||||
@@ -616,7 +630,8 @@ vv_ai_chat_markup('vv-ai', [
|
||||
|
||||
function renderBugs(bugs) {
|
||||
const wrap = $('vv-ai-bugs-wrap');
|
||||
vvBugs = bugs;
|
||||
// The list used to be cached here for the page's own markdown builder. The server renders the
|
||||
// report now, fetched by id, so there is nothing left to keep.
|
||||
if (!bugs.length) { wrap.style.display = 'none'; return; }
|
||||
wrap.style.display = '';
|
||||
$('vv-ai-bugs-sum').innerHTML =
|
||||
@@ -631,7 +646,7 @@ vv_ai_chat_markup('vv-ai', [
|
||||
<span class="vv-ai-bug-m">${esc(b.id)}${esc(seen)} · ${esc(when)}${
|
||||
b.context && b.context.verified === null
|
||||
? ' · <span class="vv-ai-warn">evidence unverified</span>' : ''}</span>
|
||||
<button class="vv-ai-btn ghost" onclick="vvAiBugCopy('${esc(b.id)}')">Copy issue</button>
|
||||
<button class="vv-ai-btn" onclick="vvAiBugReport('${esc(b.id)}')">Report…</button>
|
||||
<button class="vv-ai-btn ghost" onclick="vvAiBugClose('${esc(b.id)}')">Dismiss</button>
|
||||
</div>
|
||||
<div class="vv-ai-bug-s">${esc(b.summary)}</div>
|
||||
@@ -642,46 +657,90 @@ vv_ai_chat_markup('vv-ai', [
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Copy out, never transmit. The operator reads the text before it goes anywhere, which is the
|
||||
// whole reason this is a button and not a webhook — evidence is quoted log lines, and Varaverk
|
||||
// logs carry share names, container names and paths. Redaction by inspection beats redaction
|
||||
// by rule, and it cannot be unpublished later.
|
||||
let vvBugs = [];
|
||||
// The report itself is rendered by the server — see api/ai.php?action=bug_report. This page
|
||||
// used to build its own markdown, which meant two formats for the same thing and no way to
|
||||
// tell which one a given issue had been filed with.
|
||||
//
|
||||
// Read-only on purpose. What is reviewed is byte for byte what is sent, so there is no gap
|
||||
// between the text approved and the text transmitted — and the server re-renders from the
|
||||
// store when sending rather than trusting anything the page hands back.
|
||||
window.vvAiBugReport = function (id) {
|
||||
const box = document.getElementById('vv-ai-bug-copybox');
|
||||
const bar = document.getElementById('vv-ai-bug-send');
|
||||
box.value = 'Loading…'; box.style.display = ''; bar.style.display = 'none';
|
||||
|
||||
function vvAiBugMarkdown(b) {
|
||||
const d = t => t ? new Date(t * 1000).toISOString().slice(0, 10) : '—';
|
||||
return `### ${b.component} — ${b.summary}\n\n`
|
||||
+ `| | |\n|---|---|\n`
|
||||
+ `| Component | \`${b.component}\` |\n`
|
||||
+ `| Varaverk | \`${b.commit || 'unknown'}\` |\n`
|
||||
+ `| Host slot | ${b.host || '—'} |\n`
|
||||
+ `| Seen | ${b.seen}× · ${d(b.first)} → ${d(b.last)} |\n\n`
|
||||
+ `**Evidence**\n\n\`\`\`\n${b.evidence}\n\`\`\`\n\n`
|
||||
+ (b.context && b.context.asked ? `**Asked while diagnosing:** ${b.context.asked}\n\n` : '')
|
||||
+ (b.context && b.context.log ? `**Log:** \`${b.context.log}\`\n\n` : '')
|
||||
+ `_Filed automatically by the Varaverk assistant. Report ${b.id}._\n`;
|
||||
}
|
||||
fetch(API + '?action=bug_report&id=' + encodeURIComponent(id))
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.ok) throw new Error(d.error || 'could not build the report');
|
||||
box.value = d.markdown;
|
||||
bar.dataset.id = id;
|
||||
bar.dataset.url = d.github;
|
||||
bar.style.display = '';
|
||||
|
||||
window.vvAiBugCopy = function (id) {
|
||||
const b = vvBugs.find(x => x.id === id);
|
||||
if (!b) return;
|
||||
const text = vvAiBugMarkdown(b);
|
||||
const done = ok => {
|
||||
const btn = event && event.target;
|
||||
if (btn) { btn.textContent = ok ? 'Copied' : 'Select below'; setTimeout(() => btn.textContent = 'Copy issue', 1800); }
|
||||
if (!ok) {
|
||||
// The clipboard API needs a secure context and the WebGUI is often plain http on the
|
||||
// LAN. Falling back to a selectable box means the button always does something useful
|
||||
// rather than failing silently on exactly the setups this is built for.
|
||||
const box = document.getElementById('vv-ai-bug-copybox');
|
||||
box.style.display = ''; box.value = text; box.focus(); box.select();
|
||||
}
|
||||
};
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => done(true)).catch(() => done(false));
|
||||
} else {
|
||||
done(false);
|
||||
const t = d.targets || {};
|
||||
const local = $('vv-ai-bug-local'), gh = $('vv-ai-bug-github');
|
||||
// Only offered where it can actually work. A button that reports "local reporting is
|
||||
// switched off" after being pressed is a button that should not have been drawn.
|
||||
local.style.display = (t.local_enabled && t.local_configured) ? '' : 'none';
|
||||
local.textContent = 'Send to ' + (t.local_repo || 'Gitea');
|
||||
// GitHub is the path when local is off — and stays available when it is on, because
|
||||
// "my own backlog" and "tell the maintainer" are different intentions, not a fallback.
|
||||
gh.style.display = '';
|
||||
$('vv-ai-bug-dest').textContent = t.local_enabled
|
||||
? (t.local_configured
|
||||
? 'Local reporting is on — this stays on your Gitea and does not reach the maintainer.'
|
||||
: 'Local reporting is on but URL, repo or token is blank, so only GitHub is available.')
|
||||
: 'Opens a prefilled issue you submit under your own GitHub account.';
|
||||
box.focus(); box.select();
|
||||
})
|
||||
.catch(e => { box.value = 'Could not build the report: ' + (e.message || e); });
|
||||
};
|
||||
|
||||
window.vvAiBugSendLocal = function (btn) {
|
||||
const bar = document.getElementById('vv-ai-bug-send');
|
||||
const out = $('vv-ai-bug-dest');
|
||||
btn.disabled = true;
|
||||
out.textContent = 'sending…';
|
||||
// URLSearchParams, not FormData — a multipart POST to this endpoint hangs with no status.
|
||||
fetch(API, { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: new URLSearchParams({ action: 'bug_send_local', id: bar.dataset.id }) })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
btn.disabled = false;
|
||||
if (!d.ok) { out.innerHTML = `<span class="vv-ai-bad">${esc(d.error || 'failed')}</span>`; return; }
|
||||
// The issue URL is the receipt. Without it "sent" is just a word the page chose.
|
||||
out.innerHTML = d.url
|
||||
? `<span class="vv-ai-ok">filed:</span> <a href="${esc(d.url)}" target="_blank"
|
||||
rel="noopener noreferrer" style="color:#7d9be8">${esc(d.url)}</a>`
|
||||
: '<span class="vv-ai-ok">filed</span>';
|
||||
})
|
||||
.catch(e => { btn.disabled = false; out.innerHTML = `<span class="vv-ai-bad">${esc(String(e))}</span>`; });
|
||||
};
|
||||
|
||||
window.vvAiBugGithub = function () {
|
||||
const bar = document.getElementById('vv-ai-bug-send');
|
||||
const url = bar.dataset.url || '';
|
||||
// GitHub silently truncates an over-long query rather than refusing it, which would file half
|
||||
// a report and look like it worked. Past that length the copy box is the honest path.
|
||||
if (url.length > 7500) {
|
||||
$('vv-ai-bug-dest').innerHTML =
|
||||
'<span class="vv-ai-warn">Too long for a prefilled URL — copy the text above into a new '
|
||||
+ 'issue instead.</span>';
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
// Kept because the clipboard API needs a secure context and the WebGUI is often plain http on
|
||||
// the LAN — on exactly the setups this is built for, the box is the path that works.
|
||||
window.vvAiBugCopy = function (id) {
|
||||
const box = document.getElementById('vv-ai-bug-copybox');
|
||||
if (!box.value || box.style.display === 'none') { vvAiBugReport(id); return; }
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(box.value).catch(() => { box.focus(); box.select(); });
|
||||
} else { box.focus(); box.select(); }
|
||||
};
|
||||
|
||||
window.vvAiBugClose = function (id) {
|
||||
|
||||
@@ -66,6 +66,18 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|---|---|---|---|
|
||||
| `HOST1_BACKUP_VERIFY_SHARES` | a list, one entry per line | in this section | Leave empty to use HOST1_DAILY_SYNC_SHARES automatically. |
|
||||
|
||||
## Bug Reports
|
||||
|
||||
Route: Settings tab → All settings → *Bug Reports*
|
||||
|
||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
| Setting | Control | Where | What it does |
|
||||
|---|---|---|---|
|
||||
| `HOST1_BUG_REPORT_URL` | a text box | in this section | e.g. http://gitea:3000 or the tailnet name |
|
||||
| `HOST1_BUG_REPORT_REPO` | a text box | in this section | owner/repo |
|
||||
| `HOST1_BUG_REPORT_TOKEN` | a masked box with a **Show** button | in this section | Gitea API token with issue-write on that repo |
|
||||
|
||||
## Certificate Monitor
|
||||
|
||||
Reachable from:
|
||||
@@ -1143,6 +1155,17 @@ Saved into `master.conf`, which does not need to be opened by hand.
|
||||
| `ARR_RECOVERY_STATS` | a text box | in this section | blocklist + re-search stats |
|
||||
| `ARR_RECOVERY_FAILURE_COUNTS` | a text box | in this section | per-item chronic-failure tracking |
|
||||
|
||||
## Bug Reports
|
||||
|
||||
Route: Settings tab → All settings → *Bug Reports*
|
||||
|
||||
Saved into `master.conf`, which does not need to be opened by hand.
|
||||
|
||||
| Setting | Control | Where | What it does |
|
||||
|---|---|---|---|
|
||||
| `BUG_REPORT_LOCAL_ENABLED` | a switch | in this section | LOCAL ON — reports go to your own Gitea (HOST1_BUG_REPORT_* in host1.conf) and stay there. They do NOT reach the Varaverk maintainer. LOCAL OFF — reports open a prefilled GitHub issue you submit under your own account. Nothing is ever transmitted automatically: the report is shown read-only and sending is a separate press. |
|
||||
| `BUG_REPORT_GITHUB_REPO` | a text box | in this section | — |
|
||||
|
||||
## Certificate Monitor
|
||||
|
||||
Reachable from:
|
||||
|
||||
Reference in New Issue
Block a user