@nocville/npc 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -5
- package/dist/cjs/cli.js +55 -34
- package/dist/cjs/core/base-npc.js +8 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/process-watcher/index.js +61 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +45 -19
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +56 -35
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +8 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +62 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
- package/dist/npcs/process-watcher/process-scanner.js +61 -71
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +12 -2
- package/dist/npcs/receptionist/index.js +46 -20
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -2,6 +2,10 @@ export const DEFAULTS = {
|
|
|
2
2
|
pollInterval: 60000, // 60s
|
|
3
3
|
minPollInterval: 15000, // floor
|
|
4
4
|
navTimeoutMs: 15000,
|
|
5
|
+
/** Extra time for screenshot + encode before a capture counts as hung. */
|
|
6
|
+
captureGraceMs: 15000,
|
|
7
|
+
/** Wait before relaunching Chrome after it failed to start. */
|
|
8
|
+
relaunchBackoffMs: 10 * 60 * 1000,
|
|
5
9
|
maxColors: 32,
|
|
6
10
|
/** Polls to keep the 'changed' sub-status active after a page change. */
|
|
7
11
|
changedPolls: 2,
|
|
@@ -14,6 +18,21 @@ export const DEFAULTS = {
|
|
|
14
18
|
scaleDivisor: 8,
|
|
15
19
|
waitUntil: 'networkidle2',
|
|
16
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Size limits. The pixel payload is base64(W×H) and the API caps a screen value
|
|
23
|
+
* at 220 KB, so W×H must stay under ~165k; 320×240 leaves headroom. The
|
|
24
|
+
* viewport bound keeps Chrome's raster (and the PNG decode) to sane memory.
|
|
25
|
+
*/
|
|
26
|
+
export const LIMITS = {
|
|
27
|
+
screenMin: 16,
|
|
28
|
+
screenMaxSide: 320,
|
|
29
|
+
screenMaxPixels: 320 * 240,
|
|
30
|
+
viewportMin: 64,
|
|
31
|
+
viewportMax: 4096,
|
|
32
|
+
};
|
|
33
|
+
export const WATCH_MODES = ['auto', 'screenshot', 'basic'];
|
|
34
|
+
/** puppeteer `waitUntil` values. */
|
|
35
|
+
export const WAIT_UNTIL = ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'];
|
|
17
36
|
/** Parse a "WxH" string into a {width, height} pair, or null if malformed. */
|
|
18
37
|
export function parseDimensions(s) {
|
|
19
38
|
const m = s
|
|
@@ -22,6 +41,27 @@ export function parseDimensions(s) {
|
|
|
22
41
|
.match(/^(\d+)x(\d+)$/);
|
|
23
42
|
if (!m)
|
|
24
43
|
return null;
|
|
25
|
-
|
|
44
|
+
const width = parseInt(m[1], 10);
|
|
45
|
+
const height = parseInt(m[2], 10);
|
|
46
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
47
|
+
}
|
|
48
|
+
/** Why a TV resolution is unusable, or null when it is fine. */
|
|
49
|
+
export function screenSizeProblem(w, h) {
|
|
50
|
+
const { screenMin, screenMaxSide, screenMaxPixels } = LIMITS;
|
|
51
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < screenMin || h < screenMin) {
|
|
52
|
+
return `--screen must be at least ${screenMin}x${screenMin}`;
|
|
53
|
+
}
|
|
54
|
+
if (w > screenMaxSide || h > screenMaxSide || w * h > screenMaxPixels) {
|
|
55
|
+
return `--screen must fit within ${screenMaxSide}x${screenMaxSide} and ${screenMaxPixels} pixels (e.g. 320x240)`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Why a browser viewport is unusable, or null when it is fine. */
|
|
60
|
+
export function viewportProblem(w, h) {
|
|
61
|
+
const { viewportMin, viewportMax } = LIMITS;
|
|
62
|
+
const ok = (n) => Number.isInteger(n) && n >= viewportMin && n <= viewportMax;
|
|
63
|
+
return ok(w) && ok(h)
|
|
64
|
+
? null
|
|
65
|
+
: `--viewport sides must be between ${viewportMin} and ${viewportMax} pixels`;
|
|
26
66
|
}
|
|
27
67
|
//# sourceMappingURL=defaults.js.map
|
|
@@ -11,11 +11,18 @@ export interface WebPageWatcherConfig extends PollingNpcConfig {
|
|
|
11
11
|
screenWidth?: number;
|
|
12
12
|
screenHeight?: number;
|
|
13
13
|
waitUntil?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Run Chrome with its sandbox (default true). The sandbox is always off
|
|
16
|
+
* when running as root, where Chrome can't use it.
|
|
17
|
+
*/
|
|
18
|
+
chromeSandbox?: boolean;
|
|
14
19
|
}
|
|
15
20
|
export declare class WebPageWatcherNPC extends PollingNPC {
|
|
16
21
|
private url;
|
|
22
|
+
private host;
|
|
17
23
|
private mode;
|
|
18
24
|
private chromePath?;
|
|
25
|
+
private chromeSandbox;
|
|
19
26
|
private viewportW;
|
|
20
27
|
private viewportH;
|
|
21
28
|
private screenW;
|
|
@@ -23,7 +30,10 @@ export declare class WebPageWatcherNPC extends PollingNPC {
|
|
|
23
30
|
private waitUntil;
|
|
24
31
|
private puppeteer;
|
|
25
32
|
private browser;
|
|
26
|
-
|
|
33
|
+
/** puppeteer-core or Chrome is missing: never retried. */
|
|
34
|
+
private browserMissing;
|
|
35
|
+
/** Chrome failed to launch: retry after this time (epoch ms). */
|
|
36
|
+
private nextLaunchAttempt;
|
|
27
37
|
private chromeHintShown;
|
|
28
38
|
private lastHash;
|
|
29
39
|
private changedPollsLeft;
|
|
@@ -34,9 +44,16 @@ export declare class WebPageWatcherNPC extends PollingNPC {
|
|
|
34
44
|
get companion(): CompanionDecl;
|
|
35
45
|
protected collect(): Promise<CollectedMetrics>;
|
|
36
46
|
protected onStop(): Promise<void>;
|
|
47
|
+
private withDeadline;
|
|
37
48
|
private collectScreenshot;
|
|
38
49
|
private ensureBrowser;
|
|
50
|
+
private closeBrowser;
|
|
39
51
|
private maybeShowChromeHint;
|
|
52
|
+
/**
|
|
53
|
+
* Plain fetch: HTTP status + page title on a ticker. When this is a fallback
|
|
54
|
+
* from screenshot mode (no Chrome), a healthy page is reported as a warning
|
|
55
|
+
* to nudge the Chrome setup; an explicit `--mode=basic` reports it healthy.
|
|
56
|
+
*/
|
|
40
57
|
private collectBasic;
|
|
41
58
|
}
|
|
42
59
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
-
import {
|
|
2
|
+
import { moodForStatus } from '../../core/thresholds.js';
|
|
3
|
+
import { statusColor } from '../../core/screen.js';
|
|
4
|
+
import { NpcConfigError } from '../../core/errors.js';
|
|
5
|
+
import { isHttpUrl } from '../../core/http.js';
|
|
6
|
+
import { DEFAULTS, WATCH_MODES, WAIT_UNTIL, screenSizeProblem, viewportProblem, } from './defaults.js';
|
|
3
7
|
import { pngToPixelImage } from './pixelate.js';
|
|
4
|
-
import { findChrome, loadPuppeteer, browserConnected } from './capture.js';
|
|
8
|
+
import { findChrome, loadPuppeteer, browserConnected, chromeArgs, runningAsRoot, } from './capture.js';
|
|
5
9
|
import { basicCheck, statusFromHttp, hostnameOf } from './basic-mode.js';
|
|
10
|
+
/** Thrown when a capture exceeds its deadline (the browser is then recycled). */
|
|
11
|
+
class CaptureTimeout extends Error {
|
|
12
|
+
}
|
|
6
13
|
export class WebPageWatcherNPC extends PollingNPC {
|
|
7
14
|
url;
|
|
15
|
+
host;
|
|
8
16
|
mode;
|
|
9
17
|
chromePath;
|
|
18
|
+
chromeSandbox;
|
|
10
19
|
viewportW;
|
|
11
20
|
viewportH;
|
|
12
21
|
screenW;
|
|
@@ -14,7 +23,10 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
14
23
|
waitUntil;
|
|
15
24
|
puppeteer = null;
|
|
16
25
|
browser = null;
|
|
17
|
-
|
|
26
|
+
/** puppeteer-core or Chrome is missing: never retried. */
|
|
27
|
+
browserMissing = false;
|
|
28
|
+
/** Chrome failed to launch: retry after this time (epoch ms). */
|
|
29
|
+
nextLaunchAttempt = 0;
|
|
18
30
|
chromeHintShown = false;
|
|
19
31
|
lastHash = 0;
|
|
20
32
|
changedPollsLeft = 0;
|
|
@@ -24,22 +36,41 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
24
36
|
// Floor the poll interval — screenshots are expensive.
|
|
25
37
|
pollInterval: Math.max(config.pollInterval ?? DEFAULTS.pollInterval, DEFAULTS.minPollInterval),
|
|
26
38
|
});
|
|
39
|
+
// The page is rendered and shown to the whole office: http(s) only, never
|
|
40
|
+
// file:, data:, chrome: or javascript: URLs.
|
|
41
|
+
if (!isHttpUrl(config.url)) {
|
|
42
|
+
throw new NpcConfigError('--url must be an http(s) URL');
|
|
43
|
+
}
|
|
27
44
|
this.url = config.url;
|
|
28
|
-
this.
|
|
45
|
+
this.host = hostnameOf(config.url);
|
|
46
|
+
const mode = config.mode ?? 'auto';
|
|
47
|
+
if (!WATCH_MODES.includes(mode)) {
|
|
48
|
+
throw new NpcConfigError(`--mode must be one of ${WATCH_MODES.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
this.mode = mode;
|
|
51
|
+
const waitUntil = config.waitUntil ?? DEFAULTS.waitUntil;
|
|
52
|
+
if (!WAIT_UNTIL.includes(waitUntil)) {
|
|
53
|
+
throw new NpcConfigError(`--wait-until must be one of ${WAIT_UNTIL.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
this.waitUntil = waitUntil;
|
|
29
56
|
this.chromePath = config.chromePath;
|
|
57
|
+
this.chromeSandbox = config.chromeSandbox ?? true;
|
|
30
58
|
this.screenW = config.screenWidth ?? DEFAULTS.screenW;
|
|
31
59
|
this.screenH = config.screenHeight ?? DEFAULTS.screenH;
|
|
32
60
|
// Derive the viewport from the screen size for native downscaling unless the
|
|
33
61
|
// user overrides it (in which case pngToPixelImage box-filters to screen).
|
|
34
62
|
this.viewportW = config.viewportWidth ?? this.screenW * DEFAULTS.scaleDivisor;
|
|
35
63
|
this.viewportH = config.viewportHeight ?? this.screenH * DEFAULTS.scaleDivisor;
|
|
36
|
-
|
|
64
|
+
const sizeProblem = screenSizeProblem(this.screenW, this.screenH) ??
|
|
65
|
+
viewportProblem(this.viewportW, this.viewportH);
|
|
66
|
+
if (sizeProblem)
|
|
67
|
+
throw new NpcConfigError(sizeProblem);
|
|
37
68
|
}
|
|
38
69
|
get name() {
|
|
39
70
|
return 'web-page-watcher';
|
|
40
71
|
}
|
|
41
72
|
get defaultDisplayName() {
|
|
42
|
-
return
|
|
73
|
+
return this.host;
|
|
43
74
|
}
|
|
44
75
|
get archetype() {
|
|
45
76
|
return 'custom';
|
|
@@ -49,43 +80,58 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
49
80
|
screenType: 'web-view',
|
|
50
81
|
size: { w: 4, h: 3 },
|
|
51
82
|
placement: 'adjacent-wall',
|
|
52
|
-
label:
|
|
83
|
+
label: this.host,
|
|
53
84
|
};
|
|
54
85
|
}
|
|
55
86
|
async collect() {
|
|
56
87
|
if (this.mode === 'basic')
|
|
57
|
-
return this.collectBasic();
|
|
88
|
+
return this.collectBasic(false);
|
|
58
89
|
const browser = await this.ensureBrowser();
|
|
59
90
|
if (!browser) {
|
|
60
|
-
// 'auto' silently degrades; explicit 'screenshot' warns once.
|
|
61
91
|
this.maybeShowChromeHint();
|
|
62
|
-
return this.collectBasic();
|
|
92
|
+
return this.collectBasic(true);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return await this.withDeadline(this.collectScreenshot(browser));
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (this.state !== 'running')
|
|
99
|
+
throw err; // shutting down: nothing to report
|
|
100
|
+
// A crashed or wedged browser: recycle it and report this cycle from a plain fetch.
|
|
101
|
+
this.logger.warn('Screenshot failed; using a basic check this cycle', {
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
await this.closeBrowser();
|
|
105
|
+
return this.collectBasic(true);
|
|
63
106
|
}
|
|
64
|
-
return this.collectScreenshot(browser);
|
|
65
107
|
}
|
|
66
108
|
async onStop() {
|
|
109
|
+
// Close Chrome first: it aborts an in-flight capture, so the base class
|
|
110
|
+
// doesn't wait out a slow page load before pushing offline.
|
|
111
|
+
await this.closeBrowser();
|
|
67
112
|
await super.onStop();
|
|
68
|
-
if (this.browser) {
|
|
69
|
-
try {
|
|
70
|
-
await this.browser.close();
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// best effort
|
|
74
|
-
}
|
|
75
|
-
this.browser = null;
|
|
76
|
-
}
|
|
77
113
|
}
|
|
78
114
|
// --- Screenshot mode ---
|
|
115
|
+
withDeadline(work) {
|
|
116
|
+
const ms = DEFAULTS.navTimeoutMs + DEFAULTS.captureGraceMs;
|
|
117
|
+
let timer;
|
|
118
|
+
const deadline = new Promise((_, reject) => {
|
|
119
|
+
timer = setTimeout(() => reject(new CaptureTimeout(`capture exceeded ${ms}ms`)), ms);
|
|
120
|
+
});
|
|
121
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
122
|
+
}
|
|
79
123
|
async collectScreenshot(browser) {
|
|
80
124
|
const page = await browser.newPage();
|
|
81
125
|
try {
|
|
82
|
-
//
|
|
83
|
-
|
|
126
|
+
// A page that alert()s would block navigation and the screenshot.
|
|
127
|
+
page.on?.('dialog', (dialog) => void dialog.dismiss().catch(() => undefined));
|
|
128
|
+
// deviceScaleFactor 1/8 makes Chrome rasterize the viewport down to the
|
|
129
|
+
// screen size natively (e.g. 1280×960 → 160×120); pngToPixelImage
|
|
84
130
|
// box-filters as a fallback if it doesn't.
|
|
85
131
|
await page.setViewport({
|
|
86
132
|
width: this.viewportW,
|
|
87
133
|
height: this.viewportH,
|
|
88
|
-
deviceScaleFactor:
|
|
134
|
+
deviceScaleFactor: 1 / DEFAULTS.scaleDivisor,
|
|
89
135
|
});
|
|
90
136
|
let httpStatus = 0;
|
|
91
137
|
let timedOut = false;
|
|
@@ -113,15 +159,14 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
113
159
|
else if (this.changedPollsLeft > 0)
|
|
114
160
|
this.changedPollsLeft--;
|
|
115
161
|
const status = statusFromHttp(httpStatus, timedOut);
|
|
116
|
-
const host = hostnameOf(this.url);
|
|
117
162
|
const message = changed
|
|
118
163
|
? 'Page updated!'
|
|
119
164
|
: httpStatus
|
|
120
|
-
? `${host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
|
|
121
|
-
: `${host} unreachable`;
|
|
165
|
+
? `${this.host} ${httpStatus < 400 ? 'OK' : 'ERR'} (${httpStatus}, ${loadMs}ms)`
|
|
166
|
+
: `${this.host} ${timedOut ? 'timed out' : 'unreachable'}`;
|
|
122
167
|
return {
|
|
123
168
|
status,
|
|
124
|
-
mood:
|
|
169
|
+
mood: moodForStatus(status),
|
|
125
170
|
message,
|
|
126
171
|
subStatus: this.changedPollsLeft > 0 ? 'changed' : undefined,
|
|
127
172
|
screenData,
|
|
@@ -138,74 +183,92 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
138
183
|
}
|
|
139
184
|
}
|
|
140
185
|
async ensureBrowser() {
|
|
141
|
-
if (this.
|
|
186
|
+
if (this.browserMissing || Date.now() < this.nextLaunchAttempt)
|
|
142
187
|
return null;
|
|
143
188
|
if (this.browser && browserConnected(this.browser))
|
|
144
189
|
return this.browser;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!this.puppeteer) {
|
|
148
|
-
this.puppeteer = await loadPuppeteer();
|
|
149
|
-
}
|
|
190
|
+
this.browser = null; // absent or disconnected — (re)launch
|
|
191
|
+
this.puppeteer ??= await loadPuppeteer();
|
|
150
192
|
const chromePath = findChrome(this.chromePath);
|
|
151
193
|
if (!this.puppeteer || !chromePath) {
|
|
152
|
-
this.
|
|
194
|
+
this.browserMissing = true;
|
|
153
195
|
return null;
|
|
154
196
|
}
|
|
197
|
+
const { args, sandboxed } = chromeArgs({
|
|
198
|
+
sandbox: this.chromeSandbox,
|
|
199
|
+
isRoot: runningAsRoot(),
|
|
200
|
+
});
|
|
201
|
+
if (!sandboxed) {
|
|
202
|
+
this.logger.warn('Chrome sandbox disabled' +
|
|
203
|
+
(this.chromeSandbox ? ' (running as root)' : '') +
|
|
204
|
+
': only watch pages you trust');
|
|
205
|
+
}
|
|
155
206
|
try {
|
|
156
|
-
|
|
207
|
+
const browser = await this.puppeteer.launch({
|
|
157
208
|
executablePath: chromePath,
|
|
158
209
|
headless: true,
|
|
159
|
-
args
|
|
210
|
+
args,
|
|
160
211
|
});
|
|
161
|
-
|
|
162
|
-
this.browser
|
|
212
|
+
browser.on('disconnected', () => {
|
|
213
|
+
if (this.browser === browser)
|
|
214
|
+
this.browser = null;
|
|
163
215
|
});
|
|
164
|
-
|
|
216
|
+
// Stopped while Chrome was starting: don't leave it running.
|
|
217
|
+
if (this.state !== 'running') {
|
|
218
|
+
await browser.close().catch(() => undefined);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
this.browser = browser;
|
|
222
|
+
return browser;
|
|
165
223
|
}
|
|
166
224
|
catch (err) {
|
|
167
|
-
this.logger.warn('Failed to launch Chrome', {
|
|
225
|
+
this.logger.warn('Failed to launch Chrome; retrying in 10 minutes', {
|
|
168
226
|
error: err instanceof Error ? err.message : String(err),
|
|
169
227
|
});
|
|
170
|
-
this.
|
|
228
|
+
this.nextLaunchAttempt = Date.now() + DEFAULTS.relaunchBackoffMs;
|
|
171
229
|
return null;
|
|
172
230
|
}
|
|
173
231
|
}
|
|
232
|
+
async closeBrowser() {
|
|
233
|
+
const browser = this.browser;
|
|
234
|
+
this.browser = null;
|
|
235
|
+
if (browser)
|
|
236
|
+
await browser.close().catch(() => undefined);
|
|
237
|
+
}
|
|
174
238
|
maybeShowChromeHint() {
|
|
175
|
-
if (this.chromeHintShown)
|
|
239
|
+
if (this.chromeHintShown || !this.browserMissing)
|
|
176
240
|
return;
|
|
177
241
|
this.chromeHintShown = true;
|
|
178
242
|
this.logger.warn('Chrome/puppeteer-core unavailable — falling back to basic mode. ' +
|
|
179
243
|
'Install `puppeteer-core` and Google Chrome (or set --chrome-path/CHROME_PATH) for the pixel-art TV.');
|
|
180
244
|
}
|
|
181
245
|
// --- Basic mode ---
|
|
182
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Plain fetch: HTTP status + page title on a ticker. When this is a fallback
|
|
248
|
+
* from screenshot mode (no Chrome), a healthy page is reported as a warning
|
|
249
|
+
* to nudge the Chrome setup; an explicit `--mode=basic` reports it healthy.
|
|
250
|
+
*/
|
|
251
|
+
async collectBasic(fallback) {
|
|
183
252
|
const loadStart = Date.now();
|
|
184
|
-
const check = await basicCheck(this.url);
|
|
253
|
+
const check = await basicCheck(this.url, DEFAULTS.navTimeoutMs);
|
|
185
254
|
const loadMs = Date.now() - loadStart;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
status
|
|
191
|
-
const host = hostnameOf(this.url);
|
|
192
|
-
const message = check.title ? `${host}: ${check.title}` : `${host} (${check.status || 'ERR'})`;
|
|
255
|
+
const httpStatus = statusFromHttp(check.status, check.timedOut);
|
|
256
|
+
const status = fallback && httpStatus === 'healthy' ? 'warning' : httpStatus;
|
|
257
|
+
const message = check.title
|
|
258
|
+
? `${this.host}: ${check.title}`
|
|
259
|
+
: `${this.host} (${check.status || (check.timedOut ? 'timed out' : 'ERR')})`;
|
|
193
260
|
return {
|
|
194
261
|
status,
|
|
195
|
-
mood:
|
|
262
|
+
mood: moodForStatus(status),
|
|
196
263
|
message,
|
|
197
264
|
screenData: {
|
|
198
265
|
layout: 'ticker',
|
|
199
266
|
values: [
|
|
200
|
-
{ label: 'URL', value: host },
|
|
267
|
+
{ label: 'URL', value: this.host },
|
|
201
268
|
{
|
|
202
269
|
label: 'HTTP',
|
|
203
270
|
value: check.status ? String(check.status) : 'ERR',
|
|
204
|
-
color:
|
|
205
|
-
? 'green'
|
|
206
|
-
: check.status >= 500 || check.status === 0
|
|
207
|
-
? 'red'
|
|
208
|
-
: 'amber',
|
|
271
|
+
color: statusColor(httpStatus),
|
|
209
272
|
},
|
|
210
273
|
{ label: 'Title', value: check.title || '(no title)' },
|
|
211
274
|
{ label: 'Mode', value: 'basic' },
|
|
@@ -215,16 +278,4 @@ export class WebPageWatcherNPC extends PollingNPC {
|
|
|
215
278
|
};
|
|
216
279
|
}
|
|
217
280
|
}
|
|
218
|
-
function moodFor(status) {
|
|
219
|
-
switch (status) {
|
|
220
|
-
case 'healthy':
|
|
221
|
-
return 'happy';
|
|
222
|
-
case 'warning':
|
|
223
|
-
return 'concerned';
|
|
224
|
-
case 'critical':
|
|
225
|
-
return 'stressed';
|
|
226
|
-
default:
|
|
227
|
-
return 'idle';
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
281
|
//# sourceMappingURL=index.js.map
|
|
@@ -167,6 +167,7 @@ export function pngToPixelImage(pngBuffer, maxColors = 32, targetW = TARGET_W, t
|
|
|
167
167
|
const { palette, indices } = quantize(rgba, w * h, maxColors);
|
|
168
168
|
const screenData = buildPixelImageScreen(w, h, palette, indices);
|
|
169
169
|
const byteSize = screenData.values.reduce((sum, v) => sum + v.value.length, 0);
|
|
170
|
-
|
|
170
|
+
// Hash the palette too: a recolour with the same layout is still a change.
|
|
171
|
+
return { screenData, byteSize, hash: fnv1a(Buffer.concat([palette, indices])) };
|
|
171
172
|
}
|
|
172
173
|
//# sourceMappingURL=pixelate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocville/npc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Put your Claude Code and Codex sessions (and your servers) in the Nocville office as live NPCs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"pngjs": "^7.0.0",
|
|
27
|
-
"@nocville/node": "^0.2.
|
|
27
|
+
"@nocville/node": "^0.2.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"puppeteer-core": "*"
|