merge: close #524 (drop dead resilience-status table)
#525 remains open — the delete recommendation was withdrawn mid-execution after the evidence it rested on turned out to be wrong; see deferment.md §9. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
+26
-3
@@ -228,9 +228,12 @@ highest-risk doc error found.
|
|||||||
> The table is dropped (`DropDriverHostStatusTable`). Live-gated on docker-dev, **including the cross-mesh
|
> The table is dropped (`DropDriverHostStatusTable`). Live-gated on docker-dev, **including the cross-mesh
|
||||||
> gRPC path** — site-a first showed `—`, which is also what a broken mapper produces, so it was rebuilt to
|
> gRPC path** — site-a first showed `—`, which is also what a broken mapper produces, so it was rebuilt to
|
||||||
> prove the `has_host_statuses` presence flag round-trips rather than assuming version skew.
|
> prove the `has_host_statuses` presence flag round-trips rather than assuming version skew.
|
||||||
> ⏳ **A third table, same defect, found en route:** `DriverInstanceResilienceStatus` also has no writer and
|
> ✅ **A third table, same defect, found en route — now also RESOLVED** (`d979ac69`, Gitea **#524**):
|
||||||
> no reader — only its DbSet declaration — while the live data rides the `driver-resilience-status` channel.
|
> `DriverInstanceResilienceStatus` had no writer and no reader either — only its DbSet declaration — while
|
||||||
> Documented in place, filed as Gitea **#524** rather than widening the schema change beyond the ask.
|
> the live data rides the `driver-resilience-status` telemetry channel into `IDriverResilienceStatusStore`
|
||||||
|
> (verified consumed: `DriverStatusPanel.razor` injects it, fed by both the DPS bridge and the gRPC dialer).
|
||||||
|
> Dropped by migration `DropDriverInstanceResilienceStatusTable`. Filed separately rather than folded into
|
||||||
|
> #521 so a second schema change was an explicit decision, not a side effect.
|
||||||
|
|
||||||
Gitea #518/#507. Nine drivers raise; nothing consumes. `DriverHostStatus` table exists
|
Gitea #518/#507. Nine drivers raise; nothing consumes. `DriverHostStatus` table exists
|
||||||
(`OtOpcUaConfigDbContext.cs:48`, migration `20260715230637_V3Initial.cs:108`) with a doc-comment
|
(`OtOpcUaConfigDbContext.cs:48`, migration `20260715230637_V3Initial.cs:108`) with a doc-comment
|
||||||
@@ -520,6 +523,26 @@ also `--no-ff`) carrying the items §8 did not cover.
|
|||||||
| **#521** `IHostConnectivityProbe` | ✅ **Done — resolved differently from how it was filed.** Neither "build the publisher" nor "delete": the publisher as designed was **unbuildable** after Phase 4, so the capability was kept and the transport changed to the Phase 5 telemetry stream. Table dropped. | `30d0697c` |
|
| **#521** `IHostConnectivityProbe` | ✅ **Done — resolved differently from how it was filed.** Neither "build the publisher" nor "delete": the publisher as designed was **unbuildable** after Phase 4, so the capability was kept and the transport changed to the Phase 5 telemetry stream. Table dropped. | `30d0697c` |
|
||||||
| **#522** driver stability tiers | ✅ **Done — deleted.** The inertness was broader than either the issue or this register said: nothing constructed `MemoryTracking` either, so "pass real tiers" was never a flag flip. | `cbb88229` |
|
| **#522** driver stability tiers | ✅ **Done — deleted.** The inertness was broader than either the issue or this register said: nothing constructed `MemoryTracking` either, so "pass real tiers" was never a flag flip. | `cbb88229` |
|
||||||
| Live gate on docker-dev | ✅ **Passed**, one fix (`1 hosts` → `1 host`) | `ce589569` |
|
| Live gate on docker-dev | ✅ **Passed**, one fix (`1 hosts` → `1 host`) | `ce589569` |
|
||||||
|
| **#524** `DriverInstanceResilienceStatus` | ✅ **Done — deleted.** Sibling of #521, dead for the same Phase-4 reason. | `d979ac69` |
|
||||||
|
| **#525** `IDriver` memory members | ⏳ **OPEN — recommendation withdrawn mid-execution.** See below. | — |
|
||||||
|
|
||||||
|
⚠️ **#525 is a worked example of the register's own recurring failure mode, committed by me.** I recommended
|
||||||
|
deleting `IDriver.GetMemoryFootprint`/`FlushOptionalCachesAsync` on the stated evidence that *"1 of 12
|
||||||
|
actually frees memory"*. That number came from grepping for **two specific cache-clear calls** and treating
|
||||||
|
their absence elsewhere as proof of absence. The real inventory: **8 of 12 flushes are no-ops, 3 free real
|
||||||
|
memory** (AbCip's UDT template cache, Calculation's compiled scripts, **MTConnect's cached probe model** —
|
||||||
|
which is a *designed* path with compare-and-swap discipline and tests asserting the footprint drops), and 1
|
||||||
|
is cosmetic. The sweep had already edited 12 drivers, `IDriver` and ~15 test files; it was **reverted**
|
||||||
|
rather than completed on a justification that no longer held.
|
||||||
|
|
||||||
|
What survives of the argument is narrower: `GetMemoryFootprint` returns `0` in 7 of 12, its computed
|
||||||
|
constants were invented and never validated against real memory because nothing ever consumed them, and
|
||||||
|
**the two halves are coherent in exactly one driver** (MTConnect) — AbCip and Calculation free real memory
|
||||||
|
while reporting footprint `0`; TwinCAT, Galaxy and Mqtt report footprints behind no-op flushes.
|
||||||
|
|
||||||
|
**The lesson, which is the same one §6 already records about this file:** a grep for the shapes you expect
|
||||||
|
is not an inventory. Enumerate the whole set and read each member before drawing a count — especially when
|
||||||
|
the count is the thing your recommendation rests on.
|
||||||
|
|
||||||
**Both closures produced a residual that was filed rather than folded in** — a second dead table
|
**Both closures produced a residual that was filed rather than folded in** — a second dead table
|
||||||
(#524 `DriverInstanceResilienceStatus`) and two now-consumerless `IDriver` members (#525). Neither was
|
(#524 `DriverInstanceResilienceStatus`) and two now-consumerless `IDriver` members (#525). Neither was
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runtime resilience counters per <c>(DriverInstanceId, HostName)</c> pair.
|
|
||||||
/// <para><b>⚠️ This table is DEAD: nothing writes it and nothing reads it.</b> The only reference in
|
|
||||||
/// the repo is the <c>DriverInstanceResilienceStatuses</c> DbSet declaration. Do not treat a query
|
|
||||||
/// against it as a source of runtime state — it returns empty on every deployment.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// The original design (<c>docs/v2/implementation/phase-6-1-resilience-and-observability.md</c>
|
|
||||||
/// §Stream E.1) called for a HostedService sampling the tracker every ~5 s into this table, and
|
|
||||||
/// an Admin UI that left-joined it on <c>DriverHostStatus</c> for display. Neither was built:
|
|
||||||
/// there is no sampler, the join exists in no razor file, and <c>DriverHostStatus</c> itself was
|
|
||||||
/// removed in Gitea #521.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// The live data does exist — it just never goes through SQL. Resilience state reaches the
|
|
||||||
/// AdminUI as <c>DriverResilienceStatusChanged</c> over the Phase 5 telemetry stream into the
|
|
||||||
/// in-memory <c>IDriverResilienceStatusStore</c>, the same shape #521 adopted for per-host
|
|
||||||
/// connectivity, and for the same reason: per-cluster mesh Phase 4 leaves a driver-only node with
|
|
||||||
/// no ConfigDb connection to write rows with. Keep-or-delete is tracked as Gitea #524.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DriverInstanceResilienceStatus
|
|
||||||
{
|
|
||||||
/// <summary>Gets or sets the driver instance identifier.</summary>
|
|
||||||
public required string DriverInstanceId { get; set; }
|
|
||||||
/// <summary>Gets or sets the host name.</summary>
|
|
||||||
public required string HostName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Most recent time the circuit breaker for this (instance, host) opened; null if never.</summary>
|
|
||||||
public DateTime? LastCircuitBreakerOpenUtc { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Rolling count of consecutive Polly pipeline failures for this (instance, host).</summary>
|
|
||||||
public int ConsecutiveFailures { get; set; }
|
|
||||||
|
|
||||||
/// <summary>In-flight capability-call depth for this (instance, host). Formerly fed a Polly
|
|
||||||
/// concurrency-limiter strategy that was removed (R2-02/01/U-6); the column name is retained to avoid a
|
|
||||||
/// migration on a reader-less entity — it now carries the tracker's in-flight-call count.</summary>
|
|
||||||
public int CurrentBulkheadDepth { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Most recent process recycle time (Tier C only; null for in-process tiers).</summary>
|
|
||||||
public DateTime? LastRecycleUtc { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Post-init memory baseline captured by <c>MemoryTracking</c> (median of first
|
|
||||||
/// BaselineWindow samples). Zero while still warming up.
|
|
||||||
/// </summary>
|
|
||||||
public long BaselineFootprintBytes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Most recent footprint sample the tracker saw (steady-state read).</summary>
|
|
||||||
public long CurrentFootprintBytes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Row last-write timestamp — advances on every sampling tick.</summary>
|
|
||||||
public DateTime LastSampledUtc { get; set; }
|
|
||||||
}
|
|
||||||
+1560
File diff suppressed because it is too large
Load Diff
+55
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Drops the DriverInstanceResilienceStatus table (Gitea #524) — the sibling of #521's
|
||||||
|
/// DriverHostStatus, dead for the same reason. No code ever inserted a row: the HostedService
|
||||||
|
/// sampler its entity doc described was never written, and per-cluster mesh Phase 4 made it
|
||||||
|
/// unbuildable as designed, since a driver-only node has no ConfigDb connection. Every
|
||||||
|
/// deployment's copy of this table is empty, so the scaffolder's data-loss warning cannot bite.
|
||||||
|
/// Live breaker / in-flight counters reach the AdminUI's driver status panel as
|
||||||
|
/// DriverResilienceStatusChanged over the driver-resilience-status telemetry channel.
|
||||||
|
/// Down() recreates the table exactly, so the rollback is lossless too.
|
||||||
|
/// </summary>
|
||||||
|
public partial class DropDriverInstanceResilienceStatusTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DriverInstanceResilienceStatus");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DriverInstanceResilienceStatus",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
HostName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||||
|
BaselineFootprintBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ConsecutiveFailures = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CurrentBulkheadDepth = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CurrentFootprintBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
LastCircuitBreakerOpenUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||||
|
LastRecycleUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||||
|
LastSampledUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DriverInstanceResilienceStatus", x => new { x.DriverInstanceId, x.HostName });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DriverResilience_LastSampled",
|
||||||
|
table: "DriverInstanceResilienceStatus",
|
||||||
|
column: "LastSampledUtc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-39
@@ -471,45 +471,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstanceResilienceStatus", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DriverInstanceId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("nvarchar(64)");
|
|
||||||
|
|
||||||
b.Property<string>("HostName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<long>("BaselineFootprintBytes")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<int>("ConsecutiveFailures")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("CurrentBulkheadDepth")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<long>("CurrentFootprintBytes")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("LastCircuitBreakerOpenUtc")
|
|
||||||
.HasColumnType("datetime2(3)");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("LastRecycleUtc")
|
|
||||||
.HasColumnType("datetime2(3)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastSampledUtc")
|
|
||||||
.HasColumnType("datetime2(3)");
|
|
||||||
|
|
||||||
b.HasKey("DriverInstanceId", "HostName");
|
|
||||||
|
|
||||||
b.HasIndex("LastSampledUtc")
|
|
||||||
.HasDatabaseName("IX_DriverResilience_LastSampled");
|
|
||||||
|
|
||||||
b.ToTable("DriverInstanceResilienceStatus", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("EquipmentRowId")
|
b.Property<Guid>("EquipmentRowId")
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
public DbSet<ConfigAuditLog> ConfigAuditLogs => Set<ConfigAuditLog>();
|
public DbSet<ConfigAuditLog> ConfigAuditLogs => Set<ConfigAuditLog>();
|
||||||
/// <summary>Gets the DbSet of external ID reservations.</summary>
|
/// <summary>Gets the DbSet of external ID reservations.</summary>
|
||||||
public DbSet<ExternalIdReservation> ExternalIdReservations => Set<ExternalIdReservation>();
|
public DbSet<ExternalIdReservation> ExternalIdReservations => Set<ExternalIdReservation>();
|
||||||
/// <summary>Gets the DbSet of driver instance resilience statuses.</summary>
|
|
||||||
public DbSet<DriverInstanceResilienceStatus> DriverInstanceResilienceStatuses => Set<DriverInstanceResilienceStatus>();
|
|
||||||
/// <summary>Gets the DbSet of LDAP group role mappings.</summary>
|
/// <summary>Gets the DbSet of LDAP group role mappings.</summary>
|
||||||
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
||||||
/// <summary>Gets the DbSet of scripts.</summary>
|
/// <summary>Gets the DbSet of scripts.</summary>
|
||||||
@@ -88,7 +86,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
ConfigureNodeAcl(modelBuilder);
|
ConfigureNodeAcl(modelBuilder);
|
||||||
ConfigureConfigAuditLog(modelBuilder);
|
ConfigureConfigAuditLog(modelBuilder);
|
||||||
ConfigureExternalIdReservation(modelBuilder);
|
ConfigureExternalIdReservation(modelBuilder);
|
||||||
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
|
||||||
ConfigureLdapGroupRoleMapping(modelBuilder);
|
ConfigureLdapGroupRoleMapping(modelBuilder);
|
||||||
ConfigureScript(modelBuilder);
|
ConfigureScript(modelBuilder);
|
||||||
ConfigureVirtualTag(modelBuilder);
|
ConfigureVirtualTag(modelBuilder);
|
||||||
@@ -545,23 +542,14 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
// is registered only on the admin role, so a driver-only node has no connection to write rows with.
|
// is registered only on the admin role, so a driver-only node has no connection to write rows with.
|
||||||
// Per-host connectivity now rides DriverHealthChanged.HostStatuses to /hosts over the Phase 5
|
// Per-host connectivity now rides DriverHealthChanged.HostStatuses to /hosts over the Phase 5
|
||||||
// telemetry stream, which needs no DB and survives the mesh split.
|
// telemetry stream, which needs no DB and survives the mesh split.
|
||||||
|
//
|
||||||
private static void ConfigureDriverInstanceResilienceStatus(ModelBuilder modelBuilder)
|
// ConfigureDriverInstanceResilienceStatus is GONE for the same reason (Gitea #524). That table held
|
||||||
{
|
// per-(instance, host) breaker/in-flight counters and was equally never written: its doc described a
|
||||||
modelBuilder.Entity<DriverInstanceResilienceStatus>(e =>
|
// HostedService sampler and an AdminUI left-join on DriverHostStatus, neither of which was built. The
|
||||||
{
|
// live data reaches the AdminUI's DriverStatusPanel as DriverResilienceStatusChanged over the
|
||||||
e.ToTable("DriverInstanceResilienceStatus");
|
// driver-resilience-status telemetry channel into the in-memory IDriverResilienceStatusStore.
|
||||||
e.HasKey(x => new { x.DriverInstanceId, x.HostName });
|
// If breaker HISTORY across a central restart is ever wanted, it needs a CENTRAL-side writer fed by
|
||||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
// that stream — never a driver-node writer, which Phase 4 rules out.
|
||||||
e.Property(x => x.HostName).HasMaxLength(256);
|
|
||||||
e.Property(x => x.LastCircuitBreakerOpenUtc).HasColumnType("datetime2(3)");
|
|
||||||
e.Property(x => x.LastRecycleUtc).HasColumnType("datetime2(3)");
|
|
||||||
e.Property(x => x.LastSampledUtc).HasColumnType("datetime2(3)");
|
|
||||||
// LastSampledUtc drives the Admin UI's stale-sample filter same way DriverHostStatus's
|
|
||||||
// LastSeenUtc index does for connectivity rows.
|
|
||||||
e.HasIndex(x => x.LastSampledUtc).HasDatabaseName("IX_DriverResilience_LastSampled");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureLdapGroupRoleMapping(ModelBuilder modelBuilder)
|
private static void ConfigureLdapGroupRoleMapping(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -192,9 +192,12 @@ public sealed record ResilienceStatusSnapshot
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-flight capability calls against this (instance, host). Bumped on call entry +
|
/// In-flight capability calls against this (instance, host). Bumped on call entry +
|
||||||
/// decremented on completion. Feeds <c>DriverInstanceResilienceStatus.CurrentBulkheadDepth</c>
|
/// decremented on completion. Surfaced to the AdminUI's driver status panel as
|
||||||
/// for Admin <c>/hosts</c> — in-flight capability-call depth until the full
|
/// <c>DriverResilienceStatusChanged.CurrentInFlight</c> over the
|
||||||
/// telemetry observer lands.
|
/// <c>driver-resilience-status</c> telemetry channel.
|
||||||
|
/// <para>It used to say this fed <c>DriverInstanceResilienceStatus.CurrentBulkheadDepth</c> "until
|
||||||
|
/// the full telemetry observer lands". The telemetry observer landed (Phase 5); that table never
|
||||||
|
/// had a writer and was dropped in Gitea #524.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int CurrentInFlight { get; init; }
|
public int CurrentInFlight { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ public sealed class SchemaComplianceTests
|
|||||||
// DriverHostStatus was dropped in Gitea #521 — nothing ever wrote a row, and per-cluster
|
// DriverHostStatus was dropped in Gitea #521 — nothing ever wrote a row, and per-cluster
|
||||||
// mesh Phase 4 made the publisher its entity doc described unbuildable (a driver-only node
|
// mesh Phase 4 made the publisher its entity doc described unbuildable (a driver-only node
|
||||||
// has no ConfigDb connection). Per-host connectivity now rides DriverHealthChanged to /hosts.
|
// has no ConfigDb connection). Per-host connectivity now rides DriverHealthChanged to /hosts.
|
||||||
"DriverInstanceResilienceStatus",
|
// DriverInstanceResilienceStatus dropped in Gitea #524 — same defect as DriverHostStatus
|
||||||
|
// above: no writer, no reader, and Phase 4 leaves a driver-only node with no ConfigDb to
|
||||||
|
// write from. Live resilience counters ride the driver-resilience-status telemetry channel.
|
||||||
"LdapGroupRoleMapping",
|
"LdapGroupRoleMapping",
|
||||||
// v3 dropped the orphaned EquipmentImportBatch / EquipmentImportRow tables.
|
// v3 dropped the orphaned EquipmentImportBatch / EquipmentImportRow tables.
|
||||||
// ScriptedAlarmState was dropped in per-cluster mesh Phase 4 (Task 9) — scripted-alarm
|
// ScriptedAlarmState was dropped in per-cluster mesh Phase 4 (Task 9) — scripted-alarm
|
||||||
|
|||||||
Reference in New Issue
Block a user