@camstack/server 1.2.665 → 1.2.667
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/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.667] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Rebuilt against updated dependencies.
|
|
15
|
+
|
|
16
|
+
## [1.2.666] - 2026-10-28
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The hub raises a critical alert when npm or Hugging Face stays unreachable, and resolves it on the next success
|
|
21
|
+
|
|
10
22
|
## [1.2.665] - 2026-10-28
|
|
11
23
|
|
|
12
24
|
### Changed
|
|
13
25
|
|
|
14
26
|
- A shared events link can open a track's detail page, and shared links now use the operator's colour palette
|
|
27
|
+
- The hub raises a critical alert when npm cannot be reached (addon and server updates are paused) and resolves it when the registry answers again
|
|
15
28
|
|
|
16
29
|
## [1.2.664] - 2026-10-27
|
|
17
30
|
|
|
@@ -213,6 +213,7 @@ class AddonPackageService {
|
|
|
213
213
|
* the scheduler's dependencies back.
|
|
214
214
|
*/
|
|
215
215
|
updateCheckRescheduler = null;
|
|
216
|
+
npmReachability = null;
|
|
216
217
|
// -- Timing constants -----------------------------------------------------
|
|
217
218
|
// Short TTL — operators expect to see freshly-published versions
|
|
218
219
|
// within minutes of `npm publish`, not hours. The Addons page kicks
|
|
@@ -1439,6 +1440,10 @@ class AddonPackageService {
|
|
|
1439
1440
|
// =========================================================================
|
|
1440
1441
|
// Auto-update settings
|
|
1441
1442
|
// =========================================================================
|
|
1443
|
+
/** The hub's npm sweep reports whether the registry answered at all (alert on an outage). */
|
|
1444
|
+
setNpmReachabilityObserver(observer) {
|
|
1445
|
+
this.npmReachability = observer;
|
|
1446
|
+
}
|
|
1442
1447
|
/**
|
|
1443
1448
|
* Register the availability poller's re-arm hook. Called once at boot so a
|
|
1444
1449
|
* `setAutoUpdateSettings` write takes effect without a restart.
|
|
@@ -1695,8 +1700,11 @@ class AddonPackageService {
|
|
|
1695
1700
|
const addonsDir = this.resolveAddonsDir();
|
|
1696
1701
|
const updates = [];
|
|
1697
1702
|
let failures = 0;
|
|
1703
|
+
let lookups = 0;
|
|
1704
|
+
let reached = 0;
|
|
1705
|
+
let lastLookupError = '';
|
|
1698
1706
|
if (!fs.existsSync(addonsDir))
|
|
1699
|
-
return { updates, failures };
|
|
1707
|
+
return { updates, failures, lookups, reached };
|
|
1700
1708
|
// Collect all package.json paths -- handles both flat and scoped layouts
|
|
1701
1709
|
const pkgJsonPaths = [];
|
|
1702
1710
|
const topDirs = fs
|
|
@@ -1743,6 +1751,11 @@ class AddonPackageService {
|
|
|
1743
1751
|
continue;
|
|
1744
1752
|
}
|
|
1745
1753
|
const lookup = await this.lookupLatestVersion(name);
|
|
1754
|
+
lookups += 1;
|
|
1755
|
+
if (lookup.ok)
|
|
1756
|
+
reached += 1;
|
|
1757
|
+
else
|
|
1758
|
+
lastLookupError = lookup.error;
|
|
1746
1759
|
if (!lookup.ok) {
|
|
1747
1760
|
failures += 1;
|
|
1748
1761
|
this.logger.warn('Registry lookup failed for installed addon package', {
|
|
@@ -1772,7 +1785,21 @@ class AddonPackageService {
|
|
|
1772
1785
|
});
|
|
1773
1786
|
}
|
|
1774
1787
|
}
|
|
1775
|
-
|
|
1788
|
+
this.reportNpmReachability(lookups, reached, lastLookupError);
|
|
1789
|
+
return { updates, failures, lookups, reached };
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* One sweep is one probe: npm is unreachable only when lookups were made and
|
|
1793
|
+
* NONE got an answer. A sweep with no lookups says nothing either way.
|
|
1794
|
+
*/
|
|
1795
|
+
reportNpmReachability(lookups, reached, lastError) {
|
|
1796
|
+
const observer = this.npmReachability;
|
|
1797
|
+
if (observer === null || lookups === 0)
|
|
1798
|
+
return;
|
|
1799
|
+
const report = reached > 0 ? observer.reached() : observer.failed(lastError);
|
|
1800
|
+
void report.catch((error) => {
|
|
1801
|
+
this.logger.warn('npm reachability report failed', { meta: { error: (0, types_2.errMsg)(error) } });
|
|
1802
|
+
});
|
|
1776
1803
|
}
|
|
1777
1804
|
// =========================================================================
|
|
1778
1805
|
// Private: npm registry helpers
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RegistryReachabilityTracker = exports.REGISTRY_UNREACHABLE_CATEGORY = exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS = exports.REGISTRY_UNREACHABLE_MIN_FAILURES = void 0;
|
|
4
|
+
exports.registryUnreachableAlertId = registryUnreachableAlertId;
|
|
5
|
+
exports.buildRegistryUnreachableAlert = buildRegistryUnreachableAlert;
|
|
6
|
+
const types_1 = require("@camstack/types");
|
|
7
|
+
/** Consecutive failed probes before an episode may open. */
|
|
8
|
+
exports.REGISTRY_UNREACHABLE_MIN_FAILURES = 2;
|
|
9
|
+
/** The first failure must be at least this old when the threshold is met. */
|
|
10
|
+
exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS = 10 * 60_000;
|
|
11
|
+
exports.REGISTRY_UNREACHABLE_CATEGORY = 'registry.unreachable';
|
|
12
|
+
const EPISODE_KEY = 'registry-unreachable';
|
|
13
|
+
function registryUnreachableAlertId(registry) {
|
|
14
|
+
return `registry-unreachable:${registry}`;
|
|
15
|
+
}
|
|
16
|
+
const WORDS = {
|
|
17
|
+
npm: {
|
|
18
|
+
name: 'npm',
|
|
19
|
+
impact: 'Addon and server updates are paused',
|
|
20
|
+
},
|
|
21
|
+
huggingface: {
|
|
22
|
+
name: 'Hugging Face',
|
|
23
|
+
impact: 'Model downloads are failing',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
function buildRegistryUnreachableAlert(registry, reason, now) {
|
|
27
|
+
const words = WORDS[registry];
|
|
28
|
+
return {
|
|
29
|
+
id: registryUnreachableAlertId(registry),
|
|
30
|
+
category: exports.REGISTRY_UNREACHABLE_CATEGORY,
|
|
31
|
+
severity: 'critical',
|
|
32
|
+
title: `${words.name} cannot be reached`,
|
|
33
|
+
message: `CamStack cannot reach ${words.name}. ${words.impact} until the connection is back (${reason}). Check the hub's internet access, DNS and proxy.`,
|
|
34
|
+
status: 'active',
|
|
35
|
+
read: false,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
source: { type: 'registry', id: registry },
|
|
39
|
+
metadata: { episodeKey: EPISODE_KEY, registry },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
class RegistryReachabilityTracker {
|
|
43
|
+
#logger;
|
|
44
|
+
#getAlerts;
|
|
45
|
+
#now;
|
|
46
|
+
#state = new Map();
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.#logger = options.logger;
|
|
49
|
+
this.#getAlerts = options.getAlerts;
|
|
50
|
+
this.#now = options.now ?? Date.now;
|
|
51
|
+
}
|
|
52
|
+
/** A probe could not reach the registry. Idempotent per call: it only counts. */
|
|
53
|
+
async failed(registry, reason) {
|
|
54
|
+
const now = this.#now();
|
|
55
|
+
const state = this.#stateOf(registry);
|
|
56
|
+
if (state.failures === 0)
|
|
57
|
+
state.firstFailureAt = now;
|
|
58
|
+
state.failures += 1;
|
|
59
|
+
this.#logger.warn(`${WORDS[registry].name} unreachable`, {
|
|
60
|
+
meta: { registry, consecutiveFailures: state.failures, reason },
|
|
61
|
+
});
|
|
62
|
+
if (state.alerted)
|
|
63
|
+
return;
|
|
64
|
+
if (state.failures < exports.REGISTRY_UNREACHABLE_MIN_FAILURES)
|
|
65
|
+
return;
|
|
66
|
+
if (now - state.firstFailureAt < exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS)
|
|
67
|
+
return;
|
|
68
|
+
await this.#open(registry, state, reason, now);
|
|
69
|
+
}
|
|
70
|
+
/** A probe reached the registry (any answer below 5xx). Resolves an open episode. */
|
|
71
|
+
async reached(registry) {
|
|
72
|
+
const state = this.#stateOf(registry);
|
|
73
|
+
const wasAlerted = state.alerted;
|
|
74
|
+
const firstSuccess = !state.reconciled;
|
|
75
|
+
state.failures = 0;
|
|
76
|
+
state.firstFailureAt = 0;
|
|
77
|
+
state.alerted = false;
|
|
78
|
+
state.reconciled = true;
|
|
79
|
+
// First success after boot also closes an episode a previous run left open
|
|
80
|
+
// (resolving one that is not there is a no-op in the alert centre).
|
|
81
|
+
if (!wasAlerted && !firstSuccess)
|
|
82
|
+
return;
|
|
83
|
+
await this.#resolve(registry, wasAlerted ? 'registry reachable again' : 'startup reconcile');
|
|
84
|
+
}
|
|
85
|
+
#stateOf(registry) {
|
|
86
|
+
const existing = this.#state.get(registry);
|
|
87
|
+
if (existing !== undefined)
|
|
88
|
+
return existing;
|
|
89
|
+
const created = {
|
|
90
|
+
failures: 0,
|
|
91
|
+
firstFailureAt: 0,
|
|
92
|
+
alerted: false,
|
|
93
|
+
reconciled: false,
|
|
94
|
+
};
|
|
95
|
+
this.#state.set(registry, created);
|
|
96
|
+
return created;
|
|
97
|
+
}
|
|
98
|
+
async #open(registry, state, reason, now) {
|
|
99
|
+
const alerts = this.#getAlerts();
|
|
100
|
+
if (alerts === undefined) {
|
|
101
|
+
this.#logger.warn('Registry unreachable alert skipped — alerts capability missing', {
|
|
102
|
+
meta: { registry },
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await alerts.emit(buildRegistryUnreachableAlert(registry, reason, now));
|
|
108
|
+
state.alerted = true;
|
|
109
|
+
this.#logger.warn(`${WORDS[registry].name} unreachable — alert raised`, {
|
|
110
|
+
meta: { registry, consecutiveFailures: state.failures },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Not alerted: the next failed probe tries again.
|
|
115
|
+
this.#logger.warn('Registry unreachable alert emit failed', {
|
|
116
|
+
meta: { registry, error: (0, types_1.errMsg)(error) },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async #resolve(registry, why) {
|
|
121
|
+
const alerts = this.#getAlerts();
|
|
122
|
+
if (alerts === undefined) {
|
|
123
|
+
this.#logger.debug('Registry alert not resolved — alerts capability missing', {
|
|
124
|
+
meta: { registry, why },
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await alerts.update({
|
|
130
|
+
alertId: registryUnreachableAlertId(registry),
|
|
131
|
+
patch: { status: 'completed', read: true },
|
|
132
|
+
});
|
|
133
|
+
this.#logger.info(`${WORDS[registry].name} reachable — alert resolved`, {
|
|
134
|
+
meta: { registry, why },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.#logger.debug('Registry alert resolve skipped', {
|
|
139
|
+
meta: { registry, why, error: (0, types_1.errMsg)(error) },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.RegistryReachabilityTracker = RegistryReachabilityTracker;
|
package/dist/manual-boot.js
CHANGED
|
@@ -82,6 +82,7 @@ const topology_emitter_service_1 = require("./core/topology/topology-emitter.ser
|
|
|
82
82
|
const update_availability_emitter_js_1 = require("./core/update-availability-emitter.js");
|
|
83
83
|
const update_availability_store_js_1 = require("./core/update-availability-store.js");
|
|
84
84
|
const agent_installed_packages_js_1 = require("./core/updates/agent-installed-packages.js");
|
|
85
|
+
const registry_reachability_js_1 = require("./core/updates/registry-reachability.js");
|
|
85
86
|
const update_check_scheduler_js_1 = require("./core/updates/update-check-scheduler.js");
|
|
86
87
|
const wrapper_update_checker_js_1 = require("./core/updates/wrapper-update-checker.js");
|
|
87
88
|
// ---------------------------------------------------------------------------
|
|
@@ -319,6 +320,23 @@ async function bootManual(opts) {
|
|
|
319
320
|
return types_1.ServerPackageStatusSchema.parse(status);
|
|
320
321
|
},
|
|
321
322
|
});
|
|
323
|
+
// npm / Hugging Face unreachable → one critical alert episode. No clock of
|
|
324
|
+
// its own: npm is heard from the sweep below, Hugging Face from the model
|
|
325
|
+
// downloads this process makes anyway.
|
|
326
|
+
const registryReachability = new registry_reachability_js_1.RegistryReachabilityTracker({
|
|
327
|
+
logger: updateCheckLogger,
|
|
328
|
+
getAlerts: () => addonRegistryService.getCapabilityRegistry().getSingleton('alerts') ??
|
|
329
|
+
undefined,
|
|
330
|
+
});
|
|
331
|
+
addonPackageService.setNpmReachabilityObserver({
|
|
332
|
+
reached: () => registryReachability.reached('npm'),
|
|
333
|
+
failed: (reason) => registryReachability.failed('npm', reason),
|
|
334
|
+
});
|
|
335
|
+
(0, system_1.setHuggingFaceReachabilityObserver)((outcome) => {
|
|
336
|
+
void (outcome.reached
|
|
337
|
+
? registryReachability.reached('huggingface')
|
|
338
|
+
: registryReachability.failed('huggingface', outcome.detail));
|
|
339
|
+
});
|
|
322
340
|
const updateCheckScheduler = new update_check_scheduler_js_1.UpdateCheckScheduler({
|
|
323
341
|
logger: updateCheckLogger,
|
|
324
342
|
getIntervalSeconds: () => addonPackageService.getAutoUpdateSettings().updateCheckIntervalSeconds,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.667",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
52
|
+
"@camstack/addon-admin-ui": "1.2.500",
|
|
53
53
|
"@camstack/addon-agent-ui": "1.2.283",
|
|
54
54
|
"@camstack/addon-auth": "1.2.170",
|
|
55
55
|
"@camstack/addon-benchmark": "1.2.284",
|
|
56
56
|
"@camstack/addon-decoder-nodeav": "1.2.144",
|
|
57
57
|
"@camstack/addon-notifiers": "1.2.623",
|
|
58
58
|
"@camstack/addon-pipeline": "1.2.631",
|
|
59
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
60
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
59
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.634",
|
|
60
|
+
"@camstack/addon-post-analysis": "1.2.646",
|
|
61
61
|
"@camstack/develop": "0.1.38",
|
|
62
62
|
"@camstack/sdk": "1.2.154",
|
|
63
|
-
"@camstack/system": "1.2.
|
|
64
|
-
"@camstack/types": "1.2.
|
|
63
|
+
"@camstack/system": "1.2.503",
|
|
64
|
+
"@camstack/types": "1.2.433",
|
|
65
65
|
"@camstack/ui-library": "1.2.341",
|
|
66
66
|
"@fastify/compress": "^9.0.0",
|
|
67
67
|
"@fastify/cookie": "^11.0.2",
|