@camstack/addon-provider-unraid 0.2.153 → 0.2.154
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 +6 -0
- package/dist/addon.js +137 -1
- package/dist/addon.mjs +137 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.154] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- An integration deleted while its provider was down now has its orphan devices removed after two concordant reads of the integration list
|
|
15
|
+
|
|
10
16
|
## [0.2.153] - 2026-11-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -94497,6 +94497,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94497
94497
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94498
94498
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94499
94499
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94500
|
+
/**
|
|
94501
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94502
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94503
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94504
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94505
|
+
*/
|
|
94506
|
+
var OrphanIntegrationSweep = class {
|
|
94507
|
+
#logger;
|
|
94508
|
+
#minIntervalMs;
|
|
94509
|
+
#now;
|
|
94510
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94511
|
+
constructor(options) {
|
|
94512
|
+
this.#logger = options.logger;
|
|
94513
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94514
|
+
this.#now = options.now ?? Date.now;
|
|
94515
|
+
}
|
|
94516
|
+
get minIntervalMs() {
|
|
94517
|
+
return this.#minIntervalMs;
|
|
94518
|
+
}
|
|
94519
|
+
async sweep(input) {
|
|
94520
|
+
if (input.listedIds === null) return {
|
|
94521
|
+
newSuspects: 0,
|
|
94522
|
+
removed: []
|
|
94523
|
+
};
|
|
94524
|
+
const listed = new Set(input.listedIds);
|
|
94525
|
+
const owners = new Set(input.ownerIds);
|
|
94526
|
+
const now = this.#now();
|
|
94527
|
+
let newSuspects = 0;
|
|
94528
|
+
const removed = [];
|
|
94529
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94530
|
+
this.#suspects.delete(id);
|
|
94531
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94532
|
+
integrationId: id,
|
|
94533
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94534
|
+
} });
|
|
94535
|
+
}
|
|
94536
|
+
for (const id of owners) {
|
|
94537
|
+
if (listed.has(id)) continue;
|
|
94538
|
+
const since = this.#suspects.get(id);
|
|
94539
|
+
if (since === void 0) {
|
|
94540
|
+
this.#suspects.set(id, now);
|
|
94541
|
+
newSuspects += 1;
|
|
94542
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94543
|
+
integrationId: id,
|
|
94544
|
+
minIntervalMs: this.#minIntervalMs
|
|
94545
|
+
} });
|
|
94546
|
+
continue;
|
|
94547
|
+
}
|
|
94548
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94549
|
+
try {
|
|
94550
|
+
const count = await input.removeDevices(id);
|
|
94551
|
+
this.#suspects.delete(id);
|
|
94552
|
+
removed.push(id);
|
|
94553
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94554
|
+
integrationId: id,
|
|
94555
|
+
removed: count,
|
|
94556
|
+
firstMissingAt: since
|
|
94557
|
+
} });
|
|
94558
|
+
} catch (err) {
|
|
94559
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94560
|
+
integrationId: id,
|
|
94561
|
+
error: errMsg(err)
|
|
94562
|
+
} });
|
|
94563
|
+
}
|
|
94564
|
+
}
|
|
94565
|
+
return {
|
|
94566
|
+
newSuspects,
|
|
94567
|
+
removed
|
|
94568
|
+
};
|
|
94569
|
+
}
|
|
94570
|
+
};
|
|
94571
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94572
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94573
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94574
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94575
|
+
if (d.parentDeviceId !== null) continue;
|
|
94576
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94577
|
+
const owner = input.ownerOf(cfg);
|
|
94578
|
+
if (owner !== null) ids.add(owner);
|
|
94579
|
+
}
|
|
94580
|
+
return [...ids];
|
|
94581
|
+
}
|
|
94500
94582
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94501
94583
|
var MB = 1024 * 1024;
|
|
94502
94584
|
1024 * MB, 3072 * MB;
|
|
@@ -96085,8 +96167,14 @@ function buildUnraidDeviceAdopter(input) {
|
|
|
96085
96167
|
*/
|
|
96086
96168
|
var UnraidReconciler = class {
|
|
96087
96169
|
#deps;
|
|
96170
|
+
#sweep;
|
|
96171
|
+
#rerun = null;
|
|
96088
96172
|
constructor(deps) {
|
|
96089
96173
|
this.#deps = deps;
|
|
96174
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
96175
|
+
logger: deps.logger,
|
|
96176
|
+
...deps.sweepOptions
|
|
96177
|
+
});
|
|
96090
96178
|
}
|
|
96091
96179
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
96092
96180
|
async reconcile() {
|
|
@@ -96109,6 +96197,37 @@ var UnraidReconciler = class {
|
|
|
96109
96197
|
}
|
|
96110
96198
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
96111
96199
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
96200
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
96201
|
+
}
|
|
96202
|
+
/** Stop the scheduled second read (addon stop). */
|
|
96203
|
+
dispose() {
|
|
96204
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
96205
|
+
this.#rerun = null;
|
|
96206
|
+
}
|
|
96207
|
+
/**
|
|
96208
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
96209
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
96210
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
96211
|
+
*/
|
|
96212
|
+
async #sweepOrphans(listedIds) {
|
|
96213
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
96214
|
+
if (ownerIntegrationIds === void 0) return;
|
|
96215
|
+
try {
|
|
96216
|
+
const ownerIds = await ownerIntegrationIds();
|
|
96217
|
+
if ((await this.#sweep.sweep({
|
|
96218
|
+
ownerIds,
|
|
96219
|
+
listedIds,
|
|
96220
|
+
removeDevices
|
|
96221
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
96222
|
+
this.#rerun = setTimeout(() => {
|
|
96223
|
+
this.#rerun = null;
|
|
96224
|
+
this.reconcile();
|
|
96225
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
96226
|
+
this.#rerun.unref();
|
|
96227
|
+
}
|
|
96228
|
+
} catch (err) {
|
|
96229
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
96230
|
+
}
|
|
96112
96231
|
}
|
|
96113
96232
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
96114
96233
|
async onLifecycle(kind, integrationId) {
|
|
@@ -96233,6 +96352,7 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96233
96352
|
this.ctx.logger.warn("Unraid shutdown error", { meta: { error: errMsg(err) } });
|
|
96234
96353
|
}
|
|
96235
96354
|
this.clients = null;
|
|
96355
|
+
this.integrationReconciler?.dispose();
|
|
96236
96356
|
this.integrationReconciler = null;
|
|
96237
96357
|
unraidInstances.clear();
|
|
96238
96358
|
await super.onShutdown();
|
|
@@ -96267,7 +96387,8 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96267
96387
|
await reg.upsert(integration.id, integration.name, read.connection, read.apiKeyLocked);
|
|
96268
96388
|
return true;
|
|
96269
96389
|
},
|
|
96270
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
96390
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
96391
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
96271
96392
|
});
|
|
96272
96393
|
return this.integrationReconciler;
|
|
96273
96394
|
}
|
|
@@ -96286,6 +96407,21 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96286
96407
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
96287
96408
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
96288
96409
|
}
|
|
96410
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
96411
|
+
async ownerIntegrationIds() {
|
|
96412
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
96413
|
+
const devices = this.ctx.kernel.devices;
|
|
96414
|
+
if (!reg || !devices) return [];
|
|
96415
|
+
return await collectOwnerIntegrationIds({
|
|
96416
|
+
reg,
|
|
96417
|
+
devices,
|
|
96418
|
+
addonId: this.addonId,
|
|
96419
|
+
ownerOf: (cfg) => {
|
|
96420
|
+
const owner = cfg["integrationId"];
|
|
96421
|
+
return cfg["system"] === "unraid" && typeof owner === "string" ? owner : null;
|
|
96422
|
+
}
|
|
96423
|
+
});
|
|
96424
|
+
}
|
|
96289
96425
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
96290
96426
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
96291
96427
|
const devices = this.ctx.kernel.devices;
|
package/dist/addon.mjs
CHANGED
|
@@ -94496,6 +94496,88 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
94496
94496
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
94497
94497
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
94498
94498
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
94499
|
+
/**
|
|
94500
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
94501
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
94502
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
94503
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
94504
|
+
*/
|
|
94505
|
+
var OrphanIntegrationSweep = class {
|
|
94506
|
+
#logger;
|
|
94507
|
+
#minIntervalMs;
|
|
94508
|
+
#now;
|
|
94509
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
94510
|
+
constructor(options) {
|
|
94511
|
+
this.#logger = options.logger;
|
|
94512
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
94513
|
+
this.#now = options.now ?? Date.now;
|
|
94514
|
+
}
|
|
94515
|
+
get minIntervalMs() {
|
|
94516
|
+
return this.#minIntervalMs;
|
|
94517
|
+
}
|
|
94518
|
+
async sweep(input) {
|
|
94519
|
+
if (input.listedIds === null) return {
|
|
94520
|
+
newSuspects: 0,
|
|
94521
|
+
removed: []
|
|
94522
|
+
};
|
|
94523
|
+
const listed = new Set(input.listedIds);
|
|
94524
|
+
const owners = new Set(input.ownerIds);
|
|
94525
|
+
const now = this.#now();
|
|
94526
|
+
let newSuspects = 0;
|
|
94527
|
+
const removed = [];
|
|
94528
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
94529
|
+
this.#suspects.delete(id);
|
|
94530
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
94531
|
+
integrationId: id,
|
|
94532
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
94533
|
+
} });
|
|
94534
|
+
}
|
|
94535
|
+
for (const id of owners) {
|
|
94536
|
+
if (listed.has(id)) continue;
|
|
94537
|
+
const since = this.#suspects.get(id);
|
|
94538
|
+
if (since === void 0) {
|
|
94539
|
+
this.#suspects.set(id, now);
|
|
94540
|
+
newSuspects += 1;
|
|
94541
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
94542
|
+
integrationId: id,
|
|
94543
|
+
minIntervalMs: this.#minIntervalMs
|
|
94544
|
+
} });
|
|
94545
|
+
continue;
|
|
94546
|
+
}
|
|
94547
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
94548
|
+
try {
|
|
94549
|
+
const count = await input.removeDevices(id);
|
|
94550
|
+
this.#suspects.delete(id);
|
|
94551
|
+
removed.push(id);
|
|
94552
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
94553
|
+
integrationId: id,
|
|
94554
|
+
removed: count,
|
|
94555
|
+
firstMissingAt: since
|
|
94556
|
+
} });
|
|
94557
|
+
} catch (err) {
|
|
94558
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
94559
|
+
integrationId: id,
|
|
94560
|
+
error: errMsg(err)
|
|
94561
|
+
} });
|
|
94562
|
+
}
|
|
94563
|
+
}
|
|
94564
|
+
return {
|
|
94565
|
+
newSuspects,
|
|
94566
|
+
removed
|
|
94567
|
+
};
|
|
94568
|
+
}
|
|
94569
|
+
};
|
|
94570
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
94571
|
+
async function collectOwnerIntegrationIds(input) {
|
|
94572
|
+
const ids = /* @__PURE__ */ new Set();
|
|
94573
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
94574
|
+
if (d.parentDeviceId !== null) continue;
|
|
94575
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
94576
|
+
const owner = input.ownerOf(cfg);
|
|
94577
|
+
if (owner !== null) ids.add(owner);
|
|
94578
|
+
}
|
|
94579
|
+
return [...ids];
|
|
94580
|
+
}
|
|
94499
94581
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
94500
94582
|
var MB = 1024 * 1024;
|
|
94501
94583
|
1024 * MB, 3072 * MB;
|
|
@@ -96084,8 +96166,14 @@ function buildUnraidDeviceAdopter(input) {
|
|
|
96084
96166
|
*/
|
|
96085
96167
|
var UnraidReconciler = class {
|
|
96086
96168
|
#deps;
|
|
96169
|
+
#sweep;
|
|
96170
|
+
#rerun = null;
|
|
96087
96171
|
constructor(deps) {
|
|
96088
96172
|
this.#deps = deps;
|
|
96173
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
96174
|
+
logger: deps.logger,
|
|
96175
|
+
...deps.sweepOptions
|
|
96176
|
+
});
|
|
96089
96177
|
}
|
|
96090
96178
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
96091
96179
|
async reconcile() {
|
|
@@ -96108,6 +96196,37 @@ var UnraidReconciler = class {
|
|
|
96108
96196
|
}
|
|
96109
96197
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
96110
96198
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
96199
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
96200
|
+
}
|
|
96201
|
+
/** Stop the scheduled second read (addon stop). */
|
|
96202
|
+
dispose() {
|
|
96203
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
96204
|
+
this.#rerun = null;
|
|
96205
|
+
}
|
|
96206
|
+
/**
|
|
96207
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
96208
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
96209
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
96210
|
+
*/
|
|
96211
|
+
async #sweepOrphans(listedIds) {
|
|
96212
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
96213
|
+
if (ownerIntegrationIds === void 0) return;
|
|
96214
|
+
try {
|
|
96215
|
+
const ownerIds = await ownerIntegrationIds();
|
|
96216
|
+
if ((await this.#sweep.sweep({
|
|
96217
|
+
ownerIds,
|
|
96218
|
+
listedIds,
|
|
96219
|
+
removeDevices
|
|
96220
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
96221
|
+
this.#rerun = setTimeout(() => {
|
|
96222
|
+
this.#rerun = null;
|
|
96223
|
+
this.reconcile();
|
|
96224
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
96225
|
+
this.#rerun.unref();
|
|
96226
|
+
}
|
|
96227
|
+
} catch (err) {
|
|
96228
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
96229
|
+
}
|
|
96111
96230
|
}
|
|
96112
96231
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
96113
96232
|
async onLifecycle(kind, integrationId) {
|
|
@@ -96232,6 +96351,7 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96232
96351
|
this.ctx.logger.warn("Unraid shutdown error", { meta: { error: errMsg(err) } });
|
|
96233
96352
|
}
|
|
96234
96353
|
this.clients = null;
|
|
96354
|
+
this.integrationReconciler?.dispose();
|
|
96235
96355
|
this.integrationReconciler = null;
|
|
96236
96356
|
unraidInstances.clear();
|
|
96237
96357
|
await super.onShutdown();
|
|
@@ -96266,7 +96386,8 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96266
96386
|
await reg.upsert(integration.id, integration.name, read.connection, read.apiKeyLocked);
|
|
96267
96387
|
return true;
|
|
96268
96388
|
},
|
|
96269
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
96389
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
96390
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
96270
96391
|
});
|
|
96271
96392
|
return this.integrationReconciler;
|
|
96272
96393
|
}
|
|
@@ -96285,6 +96406,21 @@ var UnraidProviderAddon = class extends BaseDeviceProvider {
|
|
|
96285
96406
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
96286
96407
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
96287
96408
|
}
|
|
96409
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
96410
|
+
async ownerIntegrationIds() {
|
|
96411
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
96412
|
+
const devices = this.ctx.kernel.devices;
|
|
96413
|
+
if (!reg || !devices) return [];
|
|
96414
|
+
return await collectOwnerIntegrationIds({
|
|
96415
|
+
reg,
|
|
96416
|
+
devices,
|
|
96417
|
+
addonId: this.addonId,
|
|
96418
|
+
ownerOf: (cfg) => {
|
|
96419
|
+
const owner = cfg["integrationId"];
|
|
96420
|
+
return cfg["system"] === "unraid" && typeof owner === "string" ? owner : null;
|
|
96421
|
+
}
|
|
96422
|
+
});
|
|
96423
|
+
}
|
|
96288
96424
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
96289
96425
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
96290
96426
|
const devices = this.ctx.kernel.devices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-unraid",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.154",
|
|
4
4
|
"description": "Unraid device-provider addon for CamStack — one Container per Unraid instance fanning out array, disk, docker and notification entities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
}
|
|
60
60
|
],
|
|
61
61
|
"requires": {
|
|
62
|
-
"server": ">=1.2.
|
|
62
|
+
"server": ">=1.2.722"
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"files": [
|