Integrate reviewed safe recovery and incident guidance

Preserve both exact Product intelligence commits. Replace the obsolete recovery-token recipe with the existing deployment-specific reset path and client-bound recovery facts, and guide users to bounded sanitized evidence with privacy review rather than dangerous retries.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-30 01:04:48 +01:00
5 changed files with 190 additions and 30 deletions
+40 -15
View File
@@ -309,25 +309,50 @@ check, report the Pulse version and displayed check time/status separately; API
success alone does not confirm the Pulse display has recovered.
### Recovery Mode
If you are completely locked out, you can trigger a recovery token from localhost:
```bash
curl -X POST http://localhost:7655/api/security/recovery \
-d '{"action":"generate_token","duration":30}'
```
Use the returned token in `X-Recovery-Token` when calling `/api/security/recovery` to enable or disable local-only auth bypass (`disable_auth` / `enable_auth`). Token generation is localhost-only.
Example (enable recovery mode):
```bash
curl -X POST http://localhost:7655/api/security/recovery \
-H "X-Recovery-Token: <token>" \
-d '{"action":"disable_auth"}'
```
For a forgotten local password, follow [I forgot my password](#i-forgot-my-password)
above, using the steps for your deployment. Enter the host-only bootstrap token
in that instance's setup screen; do not paste it into a command or a report.
For OIDC, SAML or proxy login, use the identity-provider or administrator path
described there instead.
The advanced `/api/security/recovery` API creates a **browser-bound recovery
session**, not a server-wide authentication bypass. Despite its legacy name,
`disable_auth` does not disable authentication for other clients. Recovery
sessions work only over direct loopback requests; remote and reverse-proxy
requests cannot use them. A successful curl response does not unlock a separate
browser: the session cookie belongs to the client that made the request.
`enable_auth` clears that recovery session; it does not reset a password.
Do not transfer recovery cookies between clients or paste recovery tokens into
command arguments, URLs, screenshots or GitHub threads. The password-reset steps
above avoid that credential-handling detour.
---
## 🆘 Getting Help
If you're still stuck:
1. **Check Logs**: `journalctl -u pulse -n 100` or `docker logs --tail 100 pulse`.
2. **Check Version**: `curl http://localhost:7655/api/version`.
3. **Open Issue**: Report on [GitHub Issues](https://github.com/rcourtman/Pulse/issues) with your logs and version info.
1. **Keep the original evidence**: note what you did, when it happened and the
exact error. Do not repeat an update, outage or notification storm merely to
reproduce it. A failed update banner does not prove the action left the
target unchanged; check its current state before another attempt.
2. **Identify the affected version**: give the running Pulse and relevant agent
versions, not just the version before an upgrade. For Docker, include the
running image tag or digest. If installation never started Pulse, give the
attempted release and public installer/helper source, or say "unknown".
3. **Choose relevant, safe evidence**: if Pulse is running and collection is
safe, use **Settings → Diagnostics → Export for GitHub (sanitized)** for
connection or data failures. For a visual problem, a screenshot or the exact
error may be enough. If logs are needed, inspect a bounded local excerpt
(`journalctl -u pulse -n 100 --no-pager` or `docker logs --tail 100 pulse`),
not a full configuration or data-directory upload.
4. **Review before posting**: even a sanitized export or screenshot can contain
identifying details. Remove credentials, session cookies, webhook URLs and
private host, network or personal information. Never post bootstrap/recovery
tokens, `.env` files, private keys or an unsanitized export.
5. **Use the appropriate thread**: [GitHub Issues](https://github.com/rcourtman/Pulse/issues)
for a bug, or [Discussions](https://github.com/rcourtman/Pulse/discussions) for
a setup question. Add new evidence to an existing matching report rather
than opening a duplicate. Do not refile information you have already supplied.
@@ -20,6 +20,7 @@ const targets: Record<string, string> = {
reload: '/docs/API#resource-maintenance-and-operator-state',
malformed: '/docs/API#%invalid',
missing: '/docs/API#does-not-exist',
troubleshooting: '/docs/TROUBLESHOOTING#recovery-mode',
};
window.history.replaceState({}, '', targets[scenario] ?? targets.plain);
+40 -15
View File
@@ -309,25 +309,50 @@ check, report the Pulse version and displayed check time/status separately; API
success alone does not confirm the Pulse display has recovered.
### Recovery Mode
If you are completely locked out, you can trigger a recovery token from localhost:
```bash
curl -X POST http://localhost:7655/api/security/recovery \
-d '{"action":"generate_token","duration":30}'
```
Use the returned token in `X-Recovery-Token` when calling `/api/security/recovery` to enable or disable local-only auth bypass (`disable_auth` / `enable_auth`). Token generation is localhost-only.
Example (enable recovery mode):
```bash
curl -X POST http://localhost:7655/api/security/recovery \
-H "X-Recovery-Token: <token>" \
-d '{"action":"disable_auth"}'
```
For a forgotten local password, follow [I forgot my password](#i-forgot-my-password)
above, using the steps for your deployment. Enter the host-only bootstrap token
in that instance's setup screen; do not paste it into a command or a report.
For OIDC, SAML or proxy login, use the identity-provider or administrator path
described there instead.
The advanced `/api/security/recovery` API creates a **browser-bound recovery
session**, not a server-wide authentication bypass. Despite its legacy name,
`disable_auth` does not disable authentication for other clients. Recovery
sessions work only over direct loopback requests; remote and reverse-proxy
requests cannot use them. A successful curl response does not unlock a separate
browser: the session cookie belongs to the client that made the request.
`enable_auth` clears that recovery session; it does not reset a password.
Do not transfer recovery cookies between clients or paste recovery tokens into
command arguments, URLs, screenshots or GitHub threads. The password-reset steps
above avoid that credential-handling detour.
---
## 🆘 Getting Help
If you're still stuck:
1. **Check Logs**: `journalctl -u pulse -n 100` or `docker logs --tail 100 pulse`.
2. **Check Version**: `curl http://localhost:7655/api/version`.
3. **Open Issue**: Report on [GitHub Issues](https://github.com/rcourtman/Pulse/issues) with your logs and version info.
1. **Keep the original evidence**: note what you did, when it happened and the
exact error. Do not repeat an update, outage or notification storm merely to
reproduce it. A failed update banner does not prove the action left the
target unchanged; check its current state before another attempt.
2. **Identify the affected version**: give the running Pulse and relevant agent
versions, not just the version before an upgrade. For Docker, include the
running image tag or digest. If installation never started Pulse, give the
attempted release and public installer/helper source, or say "unknown".
3. **Choose relevant, safe evidence**: if Pulse is running and collection is
safe, use **Settings → Diagnostics → Export for GitHub (sanitized)** for
connection or data failures. For a visual problem, a screenshot or the exact
error may be enough. If logs are needed, inspect a bounded local excerpt
(`journalctl -u pulse -n 100 --no-pager` or `docker logs --tail 100 pulse`),
not a full configuration or data-directory upload.
4. **Review before posting**: even a sanitized export or screenshot can contain
identifying details. Remove credentials, session cookies, webhook URLs and
private host, network or personal information. Never post bootstrap/recovery
tokens, `.env` files, private keys or an unsanitized export.
5. **Use the appropriate thread**: [GitHub Issues](https://github.com/rcourtman/Pulse/issues)
for a bug, or [Discussions](https://github.com/rcourtman/Pulse/discussions) for
a setup question. Add new evidence to an existing matching report rather
than opening a duplicate. Do not refile information you have already supplied.
@@ -131,6 +131,27 @@ describe('docsLinks', () => {
}
});
it('keeps lockout and incident reporting guidance safe for the affected client', () => {
const troubleshooting = readFileSync(
path.join(repoRoot, 'docs', 'TROUBLESHOOTING.md'),
'utf8',
);
const recovery = troubleshooting.split('### Recovery Mode')[1].split('\n---')[0];
expect(recovery).toContain('[I forgot my password](#i-forgot-my-password)');
expect(recovery).toContain('browser-bound recovery');
expect(recovery).toContain('A successful curl response does not unlock a separate');
expect(recovery).toContain('remote and reverse-proxy');
expect(recovery).not.toContain('generate_token');
expect(recovery).not.toMatch(/```(?:bash|sh)[\s\S]*?X-Recovery-Token/);
const help = troubleshooting.split('## 🆘 Getting Help')[1];
expect(help).toContain('Do not repeat an update, outage or notification storm');
expect(help).toContain('Export for GitHub (sanitized)');
expect(help).toContain('Review before posting');
expect(help).toContain('session cookies, webhook URLs');
expect(help).toContain('If installation never started Pulse');
});
it('ships a bounded maintenance example separately from incident snoozing', () => {
const apiReference = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8');
const maintenance = apiReference
@@ -0,0 +1,88 @@
// Render the real shipped troubleshooting guide with the production Docs page.
// Run from the assigned workspace root:
// pulse-worker-browser scripts/check-community-troubleshooting.cjs
const fs = require('node:fs');
const path = require('node:path');
const { chromium } = require('playwright');
const root = process.cwd();
const frontend = path.join(root, 'frontend-modern');
const output = path.join(root, 'tmp', 'community-troubleshooting');
const port = 5198;
const observations = [];
const errors = [];
function check(condition, message) {
observations.push({ passed: condition, message });
if (!condition) throw new Error(message);
}
(async () => {
fs.mkdirSync(output, { recursive: true });
// Tailwind resolves its config/content globs from the frontend working dir.
process.chdir(frontend);
const { createServer } = await import(
path.join(frontend, 'node_modules', 'vite', 'dist', 'node', 'index.js')
);
const server = await createServer({
root: frontend,
configFile: path.join(frontend, 'vite.config.ts'),
server: { host: '127.0.0.1', port, strictPort: true },
});
let browser;
let result;
try {
await server.listen();
browser = await chromium.launch({
headless: true, channel: 'chromium', args: ['--no-sandbox'],
});
const page = await browser.newPage();
page.on('pageerror', (error) => errors.push(error.message));
page.on('response', (response) => {
if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`);
});
// Only app-runtime auth probes are mocked; the Markdown asset is real.
await page.route(new RegExp(`^http://127\\.0\\.0\\.1:${port}/api/`), (route) =>
route.fulfill({
status: 200, contentType: 'application/json',
body: JSON.stringify({ hasAuthentication: false }),
}),
);
for (const [size, width, height] of [['desktop', 1440, 900], ['phone', 390, 844]]) {
await page.setViewportSize({ width, height });
await page.goto(`http://127.0.0.1:${port}/browser-tests/docs-fragment-navigation.html?scenario=troubleshooting`);
await page.waitForFunction(() => document.activeElement?.id === 'recovery-mode');
const article = page.locator('article');
const text = await article.innerText();
check(text.includes('browser-bound recovery session'), `${size}: browser-bound recovery is explained`);
check(text.includes('A successful curl response does not unlock a separate browser'), `${size}: curl is not presented as a browser unlock`);
check(!(await article.locator('pre').allTextContents()).some((code) => code.includes('X-Recovery-Token') || code.includes('generate_token')), `${size}: no recovery credential command is offered`);
check(text.includes('Export for GitHub (sanitized)') && text.includes('Review before posting'), `${size}: safe reporting evidence is visible`);
check(text.includes('Do not repeat an update, outage or notification storm'), `${size}: unsafe reproduction is not required`);
check(!(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)), `${size}: no horizontal page overflow`);
await page.screenshot({ path: path.join(output, `${size}-recovery.png`) });
// The supported reset link must work with the real router and heading ID.
await article.getByRole('link', { name: 'I forgot my password', exact: true }).click();
await page.waitForFunction(() => document.activeElement?.id === 'i-forgot-my-password');
check((await page.locator('#i-forgot-my-password').innerText()) === 'I forgot my password', `${size}: reset link reaches the deployment-specific steps`);
await article.getByText(size === 'phone' ? 'Review before posting' : 'Keep the original evidence', { exact: true }).scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(output, `${size}-help.png`) });
}
check(errors.length === 0, 'no page errors or failed HTTP responses');
result = {
result: 'passed', observations, errors, chromium: browser.version(),
playwright: require('playwright/package.json').version,
sourceAsset: 'frontend-modern/public/docs/TROUBLESHOOTING.md',
};
} catch (error) {
result = { result: 'failed', observations, errors, error: error.message };
} finally {
if (browser) await browser.close();
await server.close();
}
fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.result === 'passed' ? 0 : 1;
})();