tabletcommand-backend-models 7.4.90 → 7.4.92

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.
Files changed (42) hide show
  1. package/README.md +68 -1
  2. package/build/index.js +2 -0
  3. package/build/index.js.map +1 -1
  4. package/build/models/esri.js +26 -0
  5. package/build/models/esri.js.map +1 -1
  6. package/build/models/status-beacon-transition.js +51 -0
  7. package/build/models/status-beacon-transition.js.map +1 -0
  8. package/build/models/status-beacon.js +174 -0
  9. package/build/models/status-beacon.js.map +1 -0
  10. package/build/test/0index.js +2 -0
  11. package/build/test/0index.js.map +1 -1
  12. package/build/test/esri.js +38 -0
  13. package/build/test/esri.js.map +1 -1
  14. package/build/test/mock.js +64 -0
  15. package/build/test/mock.js.map +1 -1
  16. package/build/test/status-beacon.js +112 -0
  17. package/build/test/status-beacon.js.map +1 -0
  18. package/build/types/status-beacon.js +3 -0
  19. package/build/types/status-beacon.js.map +1 -0
  20. package/definitions/index.d.ts +12 -0
  21. package/definitions/index.d.ts.map +1 -1
  22. package/definitions/models/esri.d.ts.map +1 -1
  23. package/definitions/models/status-beacon-transition.d.ts +13 -0
  24. package/definitions/models/status-beacon-transition.d.ts.map +1 -0
  25. package/definitions/models/status-beacon.d.ts +13 -0
  26. package/definitions/models/status-beacon.d.ts.map +1 -0
  27. package/definitions/test/mock.d.ts +62 -0
  28. package/definitions/test/mock.d.ts.map +1 -1
  29. package/definitions/test/status-beacon.d.ts +2 -0
  30. package/definitions/test/status-beacon.d.ts.map +1 -0
  31. package/definitions/types/status-beacon.d.ts +72 -0
  32. package/definitions/types/status-beacon.d.ts.map +1 -0
  33. package/package.json +1 -1
  34. package/src/index.ts +4 -0
  35. package/src/models/esri.ts +28 -0
  36. package/src/models/status-beacon-transition.ts +60 -0
  37. package/src/models/status-beacon.ts +193 -0
  38. package/src/test/0index.ts +2 -0
  39. package/src/test/esri.ts +46 -0
  40. package/src/test/mock.ts +66 -0
  41. package/src/test/status-beacon.ts +121 -0
  42. package/src/types/status-beacon.ts +90 -0
@@ -0,0 +1,60 @@
1
+ import {
2
+ MongooseModule,
3
+ } from "../helpers";
4
+ import { Model } from "mongoose";
5
+ import { StatusBeaconTransitionType } from "../types/status-beacon";
6
+
7
+ export interface StatusBeaconTransition extends StatusBeaconTransitionType { }
8
+
9
+ export default async function StatusBeaconTransitionModule(mongoose: MongooseModule) {
10
+ const { Schema } = mongoose;
11
+
12
+ const modelSchema = new Schema<StatusBeaconTransitionType>({
13
+ _id: {
14
+ type: Schema.Types.ObjectId,
15
+ auto: true,
16
+ },
17
+ departmentId: {
18
+ type: String,
19
+ default: "",
20
+ required: true,
21
+ },
22
+ integration: {
23
+ type: String,
24
+ default: "",
25
+ required: true,
26
+ },
27
+ triggerReason: {
28
+ type: String,
29
+ default: "",
30
+ },
31
+ healthState: {
32
+ type: String,
33
+ default: "",
34
+ },
35
+ consecutiveFailures: {
36
+ type: Number,
37
+ default: 0,
38
+ },
39
+ totalFailuresSinceLastHealthy: {
40
+ type: Number,
41
+ default: 0,
42
+ },
43
+ lastError: {
44
+ type: String,
45
+ default: "",
46
+ },
47
+ }, {
48
+ autoIndex: false,
49
+ timestamps: true,
50
+ });
51
+
52
+ modelSchema.index({ departmentId: 1, integration: 1, createdAt: -1 }, {
53
+ name: "departmentId_1_integration_1_createdAt_-1",
54
+ });
55
+ modelSchema.index({ createdAt: 1 }, { name: "createdAt_1" });
56
+
57
+ return mongoose.model<StatusBeaconTransition>("StatusBeaconTransition", modelSchema, "massive_status_beacon_transition", { overwriteModels: true });
58
+ }
59
+
60
+ export interface StatusBeaconTransitionModel extends Model<StatusBeaconTransition> { }
@@ -0,0 +1,193 @@
1
+ import {
2
+ MongooseModule,
3
+ currentDate,
4
+ } from "../helpers";
5
+ import { Model } from "mongoose";
6
+ import {
7
+ StatusBeaconType,
8
+ QueueDepthsType,
9
+ TCSendHealthType,
10
+ WriteBackHealthType,
11
+ SyncHealthType,
12
+ } from "../types/status-beacon";
13
+
14
+ export interface StatusBeacon extends StatusBeaconType { }
15
+
16
+ export default async function StatusBeaconModule(mongoose: MongooseModule) {
17
+ const { Schema } = mongoose;
18
+
19
+ const QueueDepths = new Schema<QueueDepthsType>({
20
+ incident: { type: Number, default: 0 },
21
+ status: { type: Number, default: 0 },
22
+ vehicle: { type: Number, default: 0 },
23
+ total: { type: Number, default: 0 },
24
+ }, { _id: false, id: false });
25
+
26
+ const TCSendHealth = new Schema<TCSendHealthType>({
27
+ lastSuccessfulIncidentSend: { type: Date },
28
+ lastSuccessfulUnitStatusSend: { type: Date },
29
+ sendFailuresDelta: { type: Number, default: 0 },
30
+ lastSendError: { type: String, default: "" },
31
+ }, { _id: false, id: false });
32
+
33
+ const WriteBackHealth = new Schema<WriteBackHealthType>({
34
+ attemptsDelta: { type: Number, default: 0 },
35
+ failuresDelta: { type: Number, default: 0 },
36
+ lastSuccessfulWriteBack: { type: Date },
37
+ lastError: { type: String, default: "" },
38
+ }, { _id: false, id: false });
39
+
40
+ const SyncHealth = new Schema<SyncHealthType>({
41
+ lastCacheRefresh: { type: Date },
42
+ cacheAgeSeconds: { type: Number, default: 0 },
43
+ consecutiveEmptyPolls: { type: Number, default: 0 },
44
+ lastPollChanges: { type: Number, default: 0 },
45
+ incidentCacheGrowthDelta: { type: Number, default: 0 },
46
+ }, { _id: false, id: false });
47
+
48
+ const modelSchema = new Schema<StatusBeaconType>({
49
+ _id: {
50
+ type: Schema.Types.ObjectId,
51
+ auto: true,
52
+ },
53
+ departmentId: {
54
+ type: String,
55
+ default: "",
56
+ required: true,
57
+ },
58
+ // Identity
59
+ integration: {
60
+ type: String,
61
+ default: "",
62
+ required: true,
63
+ },
64
+ interfaceStartTime: {
65
+ type: Date,
66
+ default: currentDate,
67
+ },
68
+ // Health State
69
+ healthState: {
70
+ type: String,
71
+ default: "",
72
+ },
73
+ triggerReason: {
74
+ type: String,
75
+ default: "",
76
+ },
77
+ consecutiveFailures: {
78
+ type: Number,
79
+ default: 0,
80
+ },
81
+ consecutiveSuccesses: {
82
+ type: Number,
83
+ default: 0,
84
+ },
85
+ totalFailuresSinceLastHealthy: {
86
+ type: Number,
87
+ default: 0,
88
+ },
89
+ outageStartTime: {
90
+ type: Date,
91
+ },
92
+ // Cycle Metrics
93
+ pollCycle: {
94
+ type: Number,
95
+ default: 0,
96
+ },
97
+ lastPollMs: {
98
+ type: Number,
99
+ default: 0,
100
+ },
101
+ activeIncidents: {
102
+ type: Number,
103
+ default: 0,
104
+ },
105
+ activeUnits: {
106
+ type: Number,
107
+ default: 0,
108
+ },
109
+ // Data Flow Deltas
110
+ incidentsDelta: {
111
+ type: Number,
112
+ default: 0,
113
+ },
114
+ unitStatusDelta: {
115
+ type: Number,
116
+ default: 0,
117
+ },
118
+ // Queue Depths
119
+ queueDepths: {
120
+ type: QueueDepths,
121
+ default: () => ({}),
122
+ },
123
+ // Auth Token Health
124
+ tokenAgeSecs: {
125
+ type: Number,
126
+ },
127
+ tokenExpiresAt: {
128
+ type: Date,
129
+ },
130
+ // TC Send Health
131
+ tCSend: {
132
+ type: TCSendHealth,
133
+ default: () => ({}),
134
+ },
135
+ // Write-back Health
136
+ writeBack: {
137
+ type: WriteBackHealth,
138
+ default: () => ({}),
139
+ },
140
+ // Sync Health
141
+ sync: {
142
+ type: SyncHealth,
143
+ default: () => ({}),
144
+ },
145
+ // Debug & Config
146
+ debugPushActive: {
147
+ type: Boolean,
148
+ default: false,
149
+ },
150
+ lastConfigSave: {
151
+ type: Date,
152
+ },
153
+ lastConfigSaveFile: {
154
+ type: String,
155
+ default: "",
156
+ },
157
+ // Error & Version
158
+ lastError: {
159
+ type: String,
160
+ default: "",
161
+ },
162
+ interfaceVersion: {
163
+ type: String,
164
+ default: "",
165
+ },
166
+ cadVersion: {
167
+ type: String,
168
+ default: "",
169
+ },
170
+ unifiedVersion: {
171
+ type: String,
172
+ default: "",
173
+ },
174
+ }, {
175
+ autoIndex: false,
176
+ timestamps: true,
177
+ });
178
+
179
+ modelSchema.index({ departmentId: 1, integration: 1 }, {
180
+ name: "departmentId_1_integration_1",
181
+ unique: true,
182
+ });
183
+ modelSchema.index({ healthState: 1 }, { name: "healthState_1" });
184
+ modelSchema.index({ triggerReason: 1 }, { name: "triggerReason_1" });
185
+ modelSchema.index({ "queueDepths.total": 1 }, { name: "queueDepths_total_1" });
186
+ modelSchema.index({ "tCSend.sendFailuresDelta": 1 }, { name: "tCSend_sendFailuresDelta_1" });
187
+ modelSchema.index({ "writeBack.failuresDelta": 1 }, { name: "writeBack_failuresDelta_1" });
188
+ modelSchema.index({ "sync.consecutiveEmptyPolls": 1 }, { name: "sync_consecutiveEmptyPolls_1" });
189
+
190
+ return mongoose.model<StatusBeacon>("StatusBeacon", modelSchema, "massive_status_beacon", { overwriteModels: true });
191
+ }
192
+
193
+ export interface StatusBeaconModel extends Model<StatusBeacon> { }
@@ -67,6 +67,8 @@ describe(" Models", function() {
67
67
  assert.isFunction(models.RemoteLogStream, "Missing RemoteLogStream");
68
68
  assert.isFunction(models.Session, "Missing Session");
69
69
  assert.isFunction(models.SAML, "Missing SAML");
70
+ assert.isFunction(models.StatusBeacon, "Missing StatusBeacon");
71
+ assert.isFunction(models.StatusBeaconTransition, "Missing StatusBeaconTransition");
70
72
  assert.isFunction(models.SMSLog, "Missing SMSLog");
71
73
  assert.isFunction(models.SMTPUnhandled, "Missing SMTPUnhandled");
72
74
  assert.isFunction(models.Template, "Missing Template");
package/src/test/esri.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { assert } from "chai";
2
2
  import { describe, it, beforeEach, afterEach } from "node:test";
3
+ import { rejects as assertRejects } from "node:assert/strict";
3
4
  import * as m from "../index";
4
5
  import * as config from "./config";
5
6
  import mockModule from "./mock";
@@ -59,4 +60,49 @@ describe("Esri", function() {
59
60
  const props = mapPropsFound[0];
60
61
  assert.equal(props?.download, true);
61
62
  });
63
+
64
+ it("enforces unique arcGISAuth.username constraint", async function() {
65
+ await new models.Esri({
66
+ departmentId: new mongoose.Types.ObjectId(),
67
+ arcGISAuth: { username: "tc_dup_user" },
68
+ }).save();
69
+
70
+ // A second department claiming the same non-empty username must be rejected.
71
+ // assertRejects fails if the save does NOT reject, so there is no false-positive.
72
+ await assertRejects(
73
+ new models.Esri({
74
+ departmentId: new mongoose.Types.ObjectId(),
75
+ arcGISAuth: { username: "tc_dup_user" },
76
+ }).save(),
77
+ /duplicate key/
78
+ );
79
+ });
80
+
81
+ it("normalizes an empty arcGISAuth.username to null", async function() {
82
+ // EsriAuth.username defaults to "" (schema shared with auth); an arcGISAuth
83
+ // present without a real username collapses to null so it is never indexed.
84
+ const saved = await new models.Esri({
85
+ departmentId: new mongoose.Types.ObjectId(),
86
+ arcGISAuth: { username: "" },
87
+ }).save();
88
+ assert.isNull(saved.arcGISAuth);
89
+ });
90
+
91
+ it("allows multiple missing/empty arcGISAuth.username and distinct values", async function() {
92
+ // missing arcGISAuth entirely (defaults to null) — not indexed
93
+ await new models.Esri({ departmentId: new mongoose.Types.ObjectId() }).save();
94
+ await new models.Esri({ departmentId: new mongoose.Types.ObjectId() }).save();
95
+ // empty-string username — normalized to null by the pre-save hook, so not indexed.
96
+ // Both must coexist: without normalization the second would collide on "" under $exists.
97
+ const empty1 = await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "" } }).save();
98
+ const empty2 = await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "" } }).save();
99
+ assert.isNull(empty1.arcGISAuth);
100
+ assert.isNull(empty2.arcGISAuth);
101
+ // distinct non-empty usernames — allowed
102
+ await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "tc_user_a" } }).save();
103
+ await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "tc_user_b" } }).save();
104
+
105
+ const count = await models.Esri.countDocuments({});
106
+ assert.equal(count, 6);
107
+ });
62
108
  });
package/src/test/mock.ts CHANGED
@@ -1504,6 +1504,70 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
1504
1504
  sendNotification: false
1505
1505
  };
1506
1506
 
1507
+ const statusBeacon = {
1508
+ _id: new mongoose.Types.ObjectId(),
1509
+ departmentId,
1510
+ integration: "test-cad-integration",
1511
+ interfaceStartTime: new Date(),
1512
+ healthState: "healthy",
1513
+ triggerReason: "poll_success",
1514
+ consecutiveFailures: 0,
1515
+ consecutiveSuccesses: 5,
1516
+ totalFailuresSinceLastHealthy: 0,
1517
+ outageStartTime: undefined,
1518
+ pollCycle: 42,
1519
+ lastPollMs: 350,
1520
+ activeIncidents: 3,
1521
+ activeUnits: 12,
1522
+ incidentsDelta: 1,
1523
+ unitStatusDelta: 2,
1524
+ queueDepths: {
1525
+ incident: 0,
1526
+ status: 0,
1527
+ vehicle: 0,
1528
+ total: 0,
1529
+ },
1530
+ tokenAgeSecs: 1800,
1531
+ tokenExpiresAt: new Date(),
1532
+ tCSend: {
1533
+ lastSuccessfulIncidentSend: new Date(),
1534
+ lastSuccessfulUnitStatusSend: new Date(),
1535
+ sendFailuresDelta: 0,
1536
+ lastSendError: "",
1537
+ },
1538
+ writeBack: {
1539
+ attemptsDelta: 0,
1540
+ failuresDelta: 0,
1541
+ lastSuccessfulWriteBack: new Date(),
1542
+ lastError: "",
1543
+ },
1544
+ sync: {
1545
+ lastCacheRefresh: new Date(),
1546
+ cacheAgeSeconds: 30,
1547
+ consecutiveEmptyPolls: 0,
1548
+ lastPollChanges: 2,
1549
+ incidentCacheGrowthDelta: 1,
1550
+ },
1551
+ debugPushActive: false,
1552
+ lastConfigSave: new Date(),
1553
+ lastConfigSaveFile: "config-v1.json",
1554
+ lastError: "",
1555
+ interfaceVersion: "1.2.3",
1556
+ cadVersion: "4.5.6",
1557
+ unifiedVersion: "7.8.9",
1558
+ };
1559
+
1560
+ const statusBeaconTransition = {
1561
+ _id: new mongoose.Types.ObjectId(),
1562
+ departmentId,
1563
+ integration: "test-cad-integration",
1564
+ triggerReason: "consecutive_failures",
1565
+ healthState: "degraded",
1566
+ consecutiveFailures: 3,
1567
+ totalFailuresSinceLastHealthy: 5,
1568
+ lastError: "connection timeout",
1569
+ };
1570
+
1507
1571
  const validationReport = {
1508
1572
  _id: new mongoose.Types.ObjectId(),
1509
1573
  departmentId: new mongoose.Types.ObjectId("56131f724143487a10000001"),
@@ -1601,6 +1665,8 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
1601
1665
  releaseNote,
1602
1666
  session,
1603
1667
  saml,
1668
+ statusBeacon,
1669
+ statusBeaconTransition,
1604
1670
  template,
1605
1671
  user,
1606
1672
  userDevice,
@@ -0,0 +1,121 @@
1
+ import { assert } from "chai";
2
+ import { describe, it, beforeEach, afterEach } from "node:test";
3
+ import * as m from "../index";
4
+ import * as config from "./config";
5
+ import mockModule from "./mock";
6
+
7
+ describe("StatusBeacon", function() {
8
+ let models: m.BackendModels, mongoose: m.MongooseModule;
9
+ let testItem: Partial<m.StatusBeacon>;
10
+ beforeEach(async function() {
11
+ const c = await m.connect(config.url);
12
+ models = c.models;
13
+ mongoose = c.mongoose;
14
+
15
+ const mock = mockModule({ mongoose });
16
+ testItem = mock.statusBeacon;
17
+ await mock.beforeEach();
18
+ });
19
+ afterEach(async function() {
20
+ await mongoose.disconnect();
21
+ });
22
+
23
+ it("is saved", async function() {
24
+ const item = new models.StatusBeacon(testItem);
25
+ const sut = await item.save();
26
+
27
+ assert.isNotNull(sut._id);
28
+ assert.equal(testItem.departmentId, sut.departmentId);
29
+ assert.equal(testItem.integration, sut.integration);
30
+ assert.equal(testItem.healthState, sut.healthState);
31
+ assert.equal(testItem.triggerReason, sut.triggerReason);
32
+ assert.equal(testItem.consecutiveFailures, sut.consecutiveFailures);
33
+ assert.equal(testItem.consecutiveSuccesses, sut.consecutiveSuccesses);
34
+ assert.equal(testItem.totalFailuresSinceLastHealthy, sut.totalFailuresSinceLastHealthy);
35
+ assert.equal(testItem.pollCycle, sut.pollCycle);
36
+ assert.equal(testItem.lastPollMs, sut.lastPollMs);
37
+ assert.equal(testItem.activeIncidents, sut.activeIncidents);
38
+ assert.equal(testItem.activeUnits, sut.activeUnits);
39
+ assert.equal(testItem.incidentsDelta, sut.incidentsDelta);
40
+ assert.equal(testItem.unitStatusDelta, sut.unitStatusDelta);
41
+ assert.equal(testItem.tokenAgeSecs, sut.tokenAgeSecs);
42
+ assert.equal(testItem.debugPushActive, sut.debugPushActive);
43
+ assert.equal(testItem.lastConfigSaveFile, sut.lastConfigSaveFile);
44
+ assert.equal(testItem.lastError, sut.lastError);
45
+ assert.equal(testItem.interfaceVersion, sut.interfaceVersion);
46
+ assert.equal(testItem.cadVersion, sut.cadVersion);
47
+ assert.equal(testItem.unifiedVersion, sut.unifiedVersion);
48
+ assert.isNotNull(sut.createdAt);
49
+ assert.isNotNull(sut.updatedAt);
50
+ });
51
+
52
+ it("saves nested queueDepths", async function() {
53
+ const item = new models.StatusBeacon(testItem);
54
+ const sut = await item.save();
55
+
56
+ assert.equal(testItem.queueDepths?.incident, sut.queueDepths.incident);
57
+ assert.equal(testItem.queueDepths?.status, sut.queueDepths.status);
58
+ assert.equal(testItem.queueDepths?.vehicle, sut.queueDepths.vehicle);
59
+ assert.equal(testItem.queueDepths?.total, sut.queueDepths.total);
60
+ });
61
+
62
+ it("saves nested tCSend health", async function() {
63
+ const item = new models.StatusBeacon(testItem);
64
+ const sut = await item.save();
65
+
66
+ assert.equal(testItem.tCSend?.sendFailuresDelta, sut.tCSend.sendFailuresDelta);
67
+ assert.equal(testItem.tCSend?.lastSendError, sut.tCSend.lastSendError);
68
+ });
69
+
70
+ it("saves nested writeBack health", async function() {
71
+ const item = new models.StatusBeacon(testItem);
72
+ const sut = await item.save();
73
+
74
+ assert.equal(testItem.writeBack?.attemptsDelta, sut.writeBack.attemptsDelta);
75
+ assert.equal(testItem.writeBack?.failuresDelta, sut.writeBack.failuresDelta);
76
+ assert.equal(testItem.writeBack?.lastError, sut.writeBack.lastError);
77
+ });
78
+
79
+ it("saves nested sync health", async function() {
80
+ const item = new models.StatusBeacon(testItem);
81
+ const sut = await item.save();
82
+
83
+ assert.equal(testItem.sync?.cacheAgeSeconds, sut.sync.cacheAgeSeconds);
84
+ assert.equal(testItem.sync?.consecutiveEmptyPolls, sut.sync.consecutiveEmptyPolls);
85
+ assert.equal(testItem.sync?.lastPollChanges, sut.sync.lastPollChanges);
86
+ assert.equal(testItem.sync?.incidentCacheGrowthDelta, sut.sync.incidentCacheGrowthDelta);
87
+ });
88
+ });
89
+
90
+ describe("StatusBeaconTransition", function() {
91
+ let models: m.BackendModels, mongoose: m.MongooseModule;
92
+ let testItem: Partial<m.StatusBeaconTransition>;
93
+ beforeEach(async function() {
94
+ const c = await m.connect(config.url);
95
+ models = c.models;
96
+ mongoose = c.mongoose;
97
+
98
+ const mock = mockModule({ mongoose });
99
+ testItem = mock.statusBeaconTransition;
100
+ await mock.beforeEach();
101
+ });
102
+ afterEach(async function() {
103
+ await mongoose.disconnect();
104
+ });
105
+
106
+ it("is saved", async function() {
107
+ const item = new models.StatusBeaconTransition(testItem);
108
+ const sut = await item.save();
109
+
110
+ assert.isNotNull(sut._id);
111
+ assert.equal(testItem.departmentId, sut.departmentId);
112
+ assert.equal(testItem.integration, sut.integration);
113
+ assert.equal(testItem.triggerReason, sut.triggerReason);
114
+ assert.equal(testItem.healthState, sut.healthState);
115
+ assert.equal(testItem.consecutiveFailures, sut.consecutiveFailures);
116
+ assert.equal(testItem.totalFailuresSinceLastHealthy, sut.totalFailuresSinceLastHealthy);
117
+ assert.equal(testItem.lastError, sut.lastError);
118
+ assert.isNotNull(sut.createdAt);
119
+ assert.isNotNull(sut.updatedAt);
120
+ });
121
+ });
@@ -0,0 +1,90 @@
1
+ import { Types } from "mongoose";
2
+
3
+ export interface QueueDepthsType {
4
+ incident: number;
5
+ status: number;
6
+ vehicle: number;
7
+ total: number;
8
+ }
9
+
10
+ export interface TCSendHealthType {
11
+ lastSuccessfulIncidentSend?: Date;
12
+ lastSuccessfulUnitStatusSend?: Date;
13
+ sendFailuresDelta: number;
14
+ lastSendError: string;
15
+ }
16
+
17
+ export interface WriteBackHealthType {
18
+ attemptsDelta: number;
19
+ failuresDelta: number;
20
+ lastSuccessfulWriteBack?: Date;
21
+ lastError: string;
22
+ }
23
+
24
+ export interface SyncHealthType {
25
+ lastCacheRefresh?: Date;
26
+ cacheAgeSeconds: number;
27
+ consecutiveEmptyPolls: number;
28
+ lastPollChanges: number;
29
+ incidentCacheGrowthDelta: number;
30
+ }
31
+
32
+ export interface StatusBeaconType {
33
+ _id: Types.ObjectId;
34
+ departmentId: string;
35
+ // Identity
36
+ integration: string;
37
+ interfaceStartTime: Date; // UTC time the integration service process started; never resets — use for uptime
38
+ // Health State
39
+ healthState: string;
40
+ triggerReason: string;
41
+ consecutiveFailures: number;
42
+ consecutiveSuccesses: number;
43
+ totalFailuresSinceLastHealthy: number;
44
+ outageStartTime?: Date; // UTC time the current outage began; absent when healthy
45
+ // Cycle Metrics
46
+ pollCycle: number;
47
+ lastPollMs: number;
48
+ activeIncidents: number;
49
+ activeUnits: number;
50
+ // Data Flow Deltas
51
+ incidentsDelta: number;
52
+ unitStatusDelta: number;
53
+ // Queue Depths
54
+ queueDepths: QueueDepthsType;
55
+ // Auth Token Health (Bearer auth only; null otherwise)
56
+ tokenAgeSecs?: number;
57
+ tokenExpiresAt?: Date;
58
+ // TC Send Health
59
+ tCSend: TCSendHealthType;
60
+ // Write-back Health
61
+ writeBack: WriteBackHealthType;
62
+ // Sync Health
63
+ sync: SyncHealthType;
64
+ // Debug & Config
65
+ debugPushActive: boolean;
66
+ lastConfigSave?: Date;
67
+ lastConfigSaveFile: string;
68
+ // Error & Version
69
+ lastError: string;
70
+ interfaceVersion: string;
71
+ cadVersion: string;
72
+ unifiedVersion: string;
73
+ // System (managed by Mongoose timestamps)
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ }
77
+
78
+ export interface StatusBeaconTransitionType {
79
+ _id: Types.ObjectId;
80
+ departmentId: string;
81
+ integration: string;
82
+ triggerReason: string;
83
+ healthState: string;
84
+ consecutiveFailures: number;
85
+ totalFailuresSinceLastHealthy: number;
86
+ lastError: string;
87
+ // System (managed by Mongoose timestamps)
88
+ createdAt: Date;
89
+ updatedAt: Date;
90
+ }