@xenon-device-management/xenon 2.10.0 → 2.11.0
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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-LzfXiiZJ.js → AnnotationOverlay-BQPQ5bPF.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BzFDp4mK.js → ApiKeyGate-Bd7Pumwv.js} +1 -1
- package/lib/public/assets/{BugReportButton-BpgWvmZo.js → BugReportButton-iC7fxPxl.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-CCB-egSH.js → DeviceMosaicView-CNdES7nm.js} +1 -1
- package/lib/public/assets/{EmptyState-DkUVmANo.js → EmptyState-wZeJbGhN.js} +1 -1
- package/lib/public/assets/{FieldGroup-mbfnfTql.js → FieldGroup-tpRfmhlE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BH5usz4Y.js → FilterMenu-BZwOppId.js} +1 -1
- package/lib/public/assets/{Menu-Cbo9mdTk.js → Menu-CahoAx8L.js} +1 -1
- package/lib/public/assets/{Modal-D6XfQHPr.js → Modal-mcM5W_Ji.js} +1 -1
- package/lib/public/assets/{RecordingPage-CmFPkWqH.js → RecordingPage-B4dFBndn.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CSlKk2qC.js → RecordingsPage-BOez3f_9.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BFW9Dlqd.js → SegmentedControl-qMSiMN1d.js} +1 -1
- package/lib/public/assets/{SettingCard-C4DioTTi.js → SettingCard-rcPtJ7HG.js} +1 -1
- package/lib/public/assets/{Table-DrKjlL00.js → Table-J3uognu0.js} +1 -1
- package/lib/public/assets/{activity-C9bE7Jm8.js → activity-_5pNSfKT.js} +1 -1
- package/lib/public/assets/{ai-settings-DRYic4oO.js → ai-settings-DTPRbkei.js} +1 -1
- package/lib/public/assets/{api-keys-CaQU0g7c.js → api-keys-CxkIUo76.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-c1gUFmmK.js → apps-Bv0pAfxz.js} +2 -2
- package/lib/public/assets/{arrow-left-DMvAWA8r.js → arrow-left-uY7F6z1K.js} +1 -1
- package/lib/public/assets/{arrow-right-BEQgxmKT.js → arrow-right-BbAm_2Wa.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Ba0e2GeU.js → arrow-up-right-x4H5NuvE.js} +1 -1
- package/lib/public/assets/{auth-shell-COODQnc7.js → auth-shell-CtMFhbnC.js} +2 -2
- package/lib/public/assets/{builds-page-DZQgWBUs.js → builds-page-B_O5uBSZ.js} +1 -1
- package/lib/public/assets/{button-WW9j43wR.js → button-CQoMQEj_.js} +1 -1
- package/lib/public/assets/{calendar-COUs9VxF.js → calendar-BZkpCqlL.js} +1 -1
- package/lib/public/assets/{check-D8ifHIJR.js → check-CAB6RYuc.js} +1 -1
- package/lib/public/assets/{chevron-right-BjKVUlB_.js → chevron-right-ec0unu3u.js} +1 -1
- package/lib/public/assets/{circle-check-CfP79QE2.js → circle-check-Dfg-gpjg.js} +1 -1
- package/lib/public/assets/{circle-x-BZbFCixZ.js → circle-x-cbPq6Btp.js} +1 -1
- package/lib/public/assets/{clock-BuTm35Qg.js → clock-DU9cBLi8.js} +1 -1
- package/lib/public/assets/{copy-W00zUBwN.js → copy-VM9kWPpE.js} +1 -1
- package/lib/public/assets/{device-explorer-BEpS-Y-_.js → device-explorer-ai0usQ8m.js} +1 -1
- package/lib/public/assets/{download-CGz-MS3y.js → download-WgW4AVid.js} +1 -1
- package/lib/public/assets/{forgot-password-DG7SqVgU.js → forgot-password-BGRpjTtt.js} +1 -1
- package/lib/public/assets/{index-EkaQM1DK.js → index-C33GjwPr.js} +2 -2
- package/lib/public/assets/{index-DWHTp1YP.js → index-DNDdHt3h.js} +1 -1
- package/lib/public/assets/{input-ChaTXeCq.js → input-DwiVcO3Q.js} +1 -1
- package/lib/public/assets/{keycodes-BhbgeW_r.js → keycodes-CvCtpVJD.js} +1 -1
- package/lib/public/assets/{line-chart-9P3RxScy.js → line-chart-PE5e9PeN.js} +1 -1
- package/lib/public/assets/{list-checks-_23IIEEL.js → list-checks-Bgn9GEEO.js} +1 -1
- package/lib/public/assets/{lock-B2JaqqRI.js → lock-Cb7My6hc.js} +1 -1
- package/lib/public/assets/{login-BEWIfLfC.js → login-CDmyzSpl.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BGJIJQ-k.js → maintenance-settings-Bq6pKoBt.js} +1 -1
- package/lib/public/assets/{monitor-BrwMY4G0.js → monitor-BZSwrnPV.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DsWDMyrY.js → mouse-pointer-2-BbFj8y8i.js} +1 -1
- package/lib/public/assets/{network-DfIhNVJv.js → network-DpwYGB0-.js} +1 -1
- package/lib/public/assets/{overview-B1mePY2_.js → overview-B4_IO-X8.js} +1 -1
- package/lib/public/assets/{page-header-DoD3TOzZ.js → page-header-BpLEuo7i.js} +1 -1
- package/lib/public/assets/{plus-BGvWvpBB.js → plus-DHZS0EGU.js} +1 -1
- package/lib/public/assets/{profile-page-CvV5aWvl.js → profile-page-DsHDIq-Y.js} +1 -1
- package/lib/public/assets/{reset-password-CGVd31LR.js → reset-password-DuUjhw_l.js} +1 -1
- package/lib/public/assets/{runbook-page-Bq2cvwb5.js → runbook-page-BI_ayvLd.js} +1 -1
- package/lib/public/assets/{select-CTcD4jWR.js → select-xiba9PSx.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-Emz2obK_.js +1 -0
- package/lib/public/assets/selector-health-page-zpwrgalC.js +11 -0
- package/lib/public/assets/session-detail-page-BhlTJ1Oq.js +4 -0
- package/lib/public/assets/{settings-C-EnGIqs.js → settings-B-mabJFm.js} +1 -1
- package/lib/public/assets/{stat-tile-BY0RDTRm.js → stat-tile-lT8WcPgu.js} +1 -1
- package/lib/public/assets/{tablet-B4cFM-5B.js → tablet-DM8dp75P.js} +1 -1
- package/lib/public/assets/{teams-BFe-RpYd.js → teams-CRkb5-Fe.js} +1 -1
- package/lib/public/assets/{trash-2-CJCKBOP9.js → trash-2-Dxgryn4o.js} +1 -1
- package/lib/public/assets/{upload-DidWkbld.js → upload-Ci0vz0BP.js} +1 -1
- package/lib/public/assets/{use-builds-data-fnfxatOd.js → use-builds-data-DaaqMjjL.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-iPen3tmC.js → use-password-reset-mode-BvIyZtaF.js} +1 -1
- package/lib/public/assets/{users-CrcjmRdf.js → users-BT8qx3ea.js} +1 -1
- package/lib/public/assets/{users-DBUv0CJx.js → users-CWYHk75q.js} +1 -1
- package/lib/public/assets/{video-off-eh69-FKB.js → video-off-BvY08g6I.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-CQoSshBn.js → webhook-settings-aHIlSJFP.js} +1 -1
- package/lib/public/assets/{zap-CICtWiLn.js → zap-qG2Li6rI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/dashboard/event-manager.js +4 -6
- package/lib/src/gateway/nodeSessionMetrics.js +39 -0
- package/lib/src/gateway/nodeSessionStatus.js +32 -23
- package/lib/src/generated/client/edge.js +2 -2
- package/lib/src/generated/client/index.js +2 -2
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +6 -0
- package/lib/src/plugin.js +3 -0
- package/lib/src/services/SelectorVerificationJob.js +50 -5
- package/lib/src/services/SessionLifecycleService.js +16 -0
- package/lib/src/services/metrics/NodeMetricsCollector.js +114 -0
- package/lib/src/services/metrics/NodeMetricsStore.js +76 -0
- package/lib/src/services/metrics/SessionMetricsService.js +71 -14
- package/lib/src/services/metrics/nodeMetrics.js +92 -0
- package/lib/src/services/selector-health/selectorDetail.js +36 -20
- package/lib/src/sessions/RemoteSession.js +32 -0
- package/lib/src/sessions/SessionManager.js +63 -0
- package/lib/test/helpers/scratch-database.js +17 -3
- package/lib/test/unit/dashboard-events-metrics.spec.js +10 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +330 -0
- package/lib/test/unit/node-metrics-collector.spec.js +226 -0
- package/lib/test/unit/node-metrics-store.spec.js +70 -0
- package/lib/test/unit/node-session-metrics-route.spec.js +97 -0
- package/lib/test/unit/selector-health-detail.spec.js +57 -0
- package/lib/test/unit/selector-verification-job.spec.js +3 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-lifecycle.spec.js +55 -0
- package/lib/test/unit/session-metrics-service.spec.js +97 -0
- package/lib/test/unit/session-recovery-metrics.spec.js +94 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +6 -0
- package/lib/public/assets/apps-DMhpZuKd.css +0 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +0 -1
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +0 -11
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +0 -4
- package/lib/public/assets/view-state-CP9dE1UI.js +0 -1
package/lib/src/app/index.js
CHANGED
|
@@ -89,6 +89,7 @@ const typedi_1 = require("typedi");
|
|
|
89
89
|
const PluginContext_1 = require("../PluginContext");
|
|
90
90
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
91
91
|
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
92
|
+
const nodeSessionMetrics_1 = require("../gateway/nodeSessionMetrics");
|
|
92
93
|
const dashboardPluginUrl = null;
|
|
93
94
|
const ASYNC_LOCK = new async_lock_1.default();
|
|
94
95
|
const router = express_1.default.Router(), apiRouter = express_1.default.Router(), staticFilesRouter = express_1.default.Router();
|
|
@@ -240,6 +241,7 @@ function createRouter(pluginArgs) {
|
|
|
240
241
|
// without sending the session a command. No login (the hub holds no
|
|
241
242
|
// credentials for the node); it takes the hub's session token instead.
|
|
242
243
|
(0, nodeSessionStatus_1.registerNodeSessionStatus)(apiRouter, pluginArgs);
|
|
244
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(apiRouter, pluginArgs);
|
|
243
245
|
// Dashboard login: unauthenticated (rate-limited internally via separate IP logic)
|
|
244
246
|
apiRouter.use('/auth', (0, auth_1.authPublicRouter)()); // login, logout — unauthenticated
|
|
245
247
|
// All remaining /api/* requires authentication (cookie session for
|
|
@@ -79,6 +79,7 @@ const types_1 = require("../services/healing/types");
|
|
|
79
79
|
const SelectorStateService_1 = require("../services/SelectorStateService");
|
|
80
80
|
const recording_store_1 = require("../services/recording/recording-store");
|
|
81
81
|
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
82
|
+
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
82
83
|
const typedi_2 = require("typedi");
|
|
83
84
|
/**
|
|
84
85
|
* What a Session row keeps of the capabilities the driver returned. The row
|
|
@@ -124,7 +125,8 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
124
125
|
// CPU and memory for the session page's Performance panel; sampling
|
|
125
126
|
// starts once the session's row exists, since the samples point at it.
|
|
126
127
|
const metrics = typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService);
|
|
127
|
-
const
|
|
128
|
+
const source = (0, nodeMetrics_1.nodeMetricsSourceOf)(session);
|
|
129
|
+
const sampled = metrics.appliesTo(device, source);
|
|
128
130
|
// If iOS real device, start performance recording (Time Profiler)
|
|
129
131
|
// Note: This only works on real devices with XCUITest driver 4.5+
|
|
130
132
|
let isIosProfilingStarted = false;
|
|
@@ -153,11 +155,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
153
155
|
data: createData,
|
|
154
156
|
});
|
|
155
157
|
if (sampled) {
|
|
156
|
-
metrics.start({
|
|
157
|
-
sessionId: session.getId(),
|
|
158
|
-
device,
|
|
159
|
-
capabilities: session.getCapabilities(),
|
|
160
|
-
});
|
|
158
|
+
metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
|
|
161
159
|
}
|
|
162
160
|
// Emit session started event
|
|
163
161
|
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseAfter = parseAfter;
|
|
7
|
+
exports.nodeSessionMetricsHandler = nodeSessionMetricsHandler;
|
|
8
|
+
exports.registerNodeSessionMetrics = registerNodeSessionMetrics;
|
|
9
|
+
const typedi_1 = require("typedi");
|
|
10
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const commandAuth_1 = require("../middleware/commandAuth");
|
|
13
|
+
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
14
|
+
const NodeMetricsStore_1 = require("../services/metrics/NodeMetricsStore");
|
|
15
|
+
const hubSessionToken_1 = require("./hubSessionToken");
|
|
16
|
+
const nodeSessionStatus_1 = require("./nodeSessionStatus");
|
|
17
|
+
/** `?after=`: a time in ms, else none. */
|
|
18
|
+
function parseAfter(raw) {
|
|
19
|
+
return typeof raw === 'string' && /^\d+(\.\d+)?$/.test(raw) ? Number(raw) : null;
|
|
20
|
+
}
|
|
21
|
+
function nodeSessionMetricsHandler(deps) {
|
|
22
|
+
return (req, res) => {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
res.setHeader(nodeMetrics_1.NODE_METRICS_HEADER, '1');
|
|
25
|
+
const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
|
|
26
|
+
(0, nodeSessionStatus_1.answerForHub)(deps, req, res, sessionId, 'Session metrics', () => { var _a; return res.status(200).json({ value: deps.read(sessionId, parseAfter((_a = req.query) === null || _a === void 0 ? void 0 : _a.after)) }); });
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A node's route, ahead of its login, like the session-status route. A hub or standalone server has none. */
|
|
30
|
+
function registerNodeSessionMetrics(router, pluginArgs) {
|
|
31
|
+
if (pluginArgs.hub === undefined)
|
|
32
|
+
return;
|
|
33
|
+
router.get(nodeMetrics_1.NODE_METRICS_ROUTE, nodeSessionMetricsHandler({
|
|
34
|
+
hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
|
|
35
|
+
enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
|
|
36
|
+
read: (sessionId, after) => typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore).read(sessionId, after),
|
|
37
|
+
logger: logger_1.default.scope('NodeSessionMetrics'),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.NodeSessionProbeSupport = exports.UNSUPPORTED_RECHECK_MS = exports.NODE_SESSION_STATUS_HEADER = exports.NODE_SESSION_STATUS_PATH = exports.NODE_SESSION_STATUS_ROUTE = void 0;
|
|
13
|
+
exports.answerForHub = answerForHub;
|
|
13
14
|
exports.nodeSessionStatusHandler = nodeSessionStatusHandler;
|
|
14
15
|
exports.registerNodeSessionStatus = registerNodeSessionStatus;
|
|
15
16
|
const typedi_1 = require("typedi");
|
|
@@ -54,34 +55,42 @@ exports.NODE_SESSION_STATUS_PATH = '/xenon/api/node/sessions';
|
|
|
54
55
|
exports.NODE_SESSION_STATUS_HEADER = 'x-xenon-node-sessions';
|
|
55
56
|
/** How long a hub keeps using the old probe for a node without the route before asking again. */
|
|
56
57
|
exports.UNSUPPORTED_RECHECK_MS = 10 * 60000;
|
|
58
|
+
/**
|
|
59
|
+
* Runs `answer` when the caller may know this session's state on the node:
|
|
60
|
+
* always with per-command auth off, else only with the hub's session token
|
|
61
|
+
* for it. Otherwise the unknown-session answer, or 503 when the token can't
|
|
62
|
+
* be checked. `what` names the route in the log.
|
|
63
|
+
*/
|
|
64
|
+
function answerForHub(deps, req, res, sessionId, what, answer) {
|
|
65
|
+
if (!deps.enforced())
|
|
66
|
+
return answer();
|
|
67
|
+
const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
|
|
68
|
+
const token = typeof presented === 'string' ? presented : undefined;
|
|
69
|
+
if (token === undefined) {
|
|
70
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
deps.hubTokens
|
|
74
|
+
.verify(token, sessionId)
|
|
75
|
+
.then((valid) => {
|
|
76
|
+
if (valid)
|
|
77
|
+
return answer();
|
|
78
|
+
deps.logger.warn(`${what} for ${sessionId} refused: the hub token is not valid for it`);
|
|
79
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
deps.logger.error(`${what} for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
|
|
83
|
+
if (!res.headersSent) {
|
|
84
|
+
res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
57
88
|
function nodeSessionStatusHandler(deps) {
|
|
58
89
|
return (req, res) => {
|
|
59
90
|
var _a, _b;
|
|
60
91
|
res.setHeader(exports.NODE_SESSION_STATUS_HEADER, '1');
|
|
61
92
|
const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
|
|
62
|
-
|
|
63
|
-
if (!deps.enforced())
|
|
64
|
-
return answer();
|
|
65
|
-
const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
|
|
66
|
-
const token = typeof presented === 'string' ? presented : undefined;
|
|
67
|
-
if (token === undefined) {
|
|
68
|
-
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
deps.hubTokens
|
|
72
|
-
.verify(token, sessionId)
|
|
73
|
-
.then((valid) => {
|
|
74
|
-
if (valid)
|
|
75
|
-
return answer();
|
|
76
|
-
deps.logger.warn(`Session status for ${sessionId} refused: the hub token is not valid for it`);
|
|
77
|
-
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
78
|
-
})
|
|
79
|
-
.catch((error) => {
|
|
80
|
-
deps.logger.error(`Session status for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
|
|
81
|
-
if (!res.headersSent) {
|
|
82
|
-
res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
93
|
+
answerForHub(deps, req, res, sessionId, 'Session status', () => res.status(200).json({ value: { sessionId, exists: deps.hasSession(sessionId) } }));
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
/**
|
|
@@ -592,8 +592,8 @@ const config = {
|
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
594
|
},
|
|
595
|
-
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"darwin\", \"darwin-arm64\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\", \"debian-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Build {\n id String @id @default(uuid())\n name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n sessions Session[]\n}\n\nmodel Session {\n id String @id\n build_id String?\n name String?\n status String @default(\"running\")\n desired_capabilities String\n session_capabilities String\n node_id String\n has_live_video Boolean\n video_recording_enabled Boolean @default(true)\n video_recording String?\n startTime DateTime @default(now())\n endTime DateTime?\n failure_reason String?\n is_profiling_available Boolean @default(false)\n device_info String?\n device_udid String\n device_platform String\n device_version String\n device_name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n performance_trace String? @map(\"performance_trace\")\n failure_category String?\n ai_analysis String?\n tags String?\n trace_id String?\n last_heartbeat_at DateTime?\n heartbeat_pid Int?\n heartbeat_host String?\n api_key_id String?\n user_id String?\n Log Log[]\n Profiling Profiling[]\n SessionMetric SessionMetric[]\n build Build? @relation(fields: [build_id], references: [id])\n SessionLog SessionLog[]\n Recording Recording[]\n\n @@index([status, last_heartbeat_at])\n @@index([build_id])\n @@index([device_udid])\n @@index([device_platform, status])\n @@index([api_key_id])\n}\n\nmodel SessionLog {\n id String @id @default(uuid())\n session_id String\n command_name String?\n url String\n method String\n title String\n subtitle String?\n body String?\n response String\n screenshot String?\n is_success Boolean?\n is_error Boolean @default(false)\n is_healed Boolean @default(false)\n original_selector String?\n healed_selector String?\n healing_confidence Float?\n healing_tier String?\n original_strategy String?\n healed_strategy String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n duration Int?\n span_id String?\n trace_id String?\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Log {\n id String @id @default(uuid())\n session_id String\n log_type String\n message String\n timestamp DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n}\n\nmodel Profiling {\n id Int @id @default(autoincrement())\n session_id String\n cpu String?\n memory String?\n total_cpu_used String?\n total_memory_used String?\n raw_cpu_log String?\n raw_memory_log String?\n timestamp DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n}\n\n/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).\nmodel SessionMetric {\n id Int @id @default(autoincrement())\n session_id String\n /// epoch ms\n at Float\n device_cpu_pct Float?\n device_mem_mb Float?\n device_mem_total Float?\n app_cpu_pct Float?\n app_mem_mb Float?\n /// The package the app figures are for (Android), else null.\n app_id String?\n session Session @relation(fields: [session_id], references: [id], onDelete: Cascade)\n\n @@index([session_id, at])\n}\n\nmodel App {\n id String @id @default(uuid())\n name String\n filename String\n filepath String\n mimetype String\n size Int\n packageName String?\n version String?\n platform String?\n md5 String? @unique\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n // The team whose members may list, download and run this app. Null is the\n // shared pool, which everyone sees: the device team rule (isDeviceVisible).\n teamId String?\n team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)\n\n @@index([teamId])\n}\n\nmodel Device {\n udid String\n host String\n systemPort Int?\n proxyPort Int?\n proxyHost String?\n wdaLocalPort Int?\n name String? @default(\"unknown\")\n state String? @default(\"available\")\n sdk String? @default(\"unknown\")\n platform String? @default(\"unknown\")\n deviceType String? @default(\"real\")\n busy Boolean? @default(false)\n userBlocked Boolean? @default(false)\n realDevice Boolean? @default(true)\n session_id String?\n offline Boolean? @default(false)\n mjpegServerPort Int?\n lastCmdExecutedAt Float?\n totalUtilizationTimeMilliSec Float @default(0)\n sessionStartTime Float @default(0)\n newCommandTimeout Int?\n cloud String?\n derivedDataPath String?\n chromeDriverPath String?\n capability String?\n adbRemoteHost String?\n adbPort Int?\n nodeId String?\n screenWidth String?\n screenHeight String?\n dashboard_link String?\n total_session_count Int? @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n healthCheckError String?\n healthStatus String? @default(\"Healthy\")\n lastHealthCheckAt Float?\n batteryLevel Int?\n reservationReason String?\n reservedBy String?\n reservedUntil Float?\n storageFree String?\n tags String?\n thermalStatus String?\n marketingName String?\n model String?\n manufacturer String?\n formFactor String?\n sessionProgress String? @default(\"\")\n totalHealedCount Int? @default(0)\n ip String? @default(\"\")\n cpuArchitecture String?\n owningSessionId String? @map(\"owning_session_id\")\n lockedAt Float? @map(\"locked_at\")\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n // This server's claim on the phone for a session: when it was taken, and\n // the session's id once the session exists (null while it is created).\n // See src/data-service/deviceClaims.ts.\n claimSessionId String?\n claimedAt Float?\n // On a hub, what the phone's node last reported: busy there. `busy` is the\n // claim OR this. Always false for a phone this server drives.\n nodeBusy Boolean? @default(false)\n // On a hub, who holds the phone there: the preview hold (manual_<user>_<udid>)\n // in the node's report of its session_id, else null. Shown, never judged:\n // the node decides access. Always null for a phone this server drives.\n nodeHold String?\n\n @@id([udid, host])\n @@index([owningSessionId])\n @@index([teamId])\n @@index([claimSessionId])\n}\n\nmodel PendingSession {\n id Int @id @default(autoincrement())\n capability_id String @unique\n capability String\n createdAt Float\n}\n\nmodel CLIArgs {\n id Int @id @default(autoincrement())\n args String\n createdAt DateTime @default(now())\n}\n\nmodel WebhookConfig {\n id String @id @default(uuid())\n url String\n type String @default(\"slack\")\n events String\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n payloadTemplate String?\n}\n\nmodel WebConfig {\n id String @id @default(\"global\")\n name String @unique\n value String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel LocatorEtalon {\n id String @id @default(uuid())\n selector String @unique\n strategy String\n attributes String\n nodeName String\n lastSeen DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel PortLease {\n port Int @id\n purpose String\n leasedToUdid String\n leasedToHost String @default(\"\")\n leaseId String?\n leasedToPid Int?\n leasedAt Float\n expiresAt Float\n\n @@index([purpose, expiresAt])\n @@index([leasedToUdid])\n @@index([leaseId])\n @@index([expiresAt])\n}\n\nmodel Lease {\n id String @id @default(cuid())\n tokenHash String\n deviceUdid String\n deviceHost String\n actorId String\n teamId String?\n buildId String?\n reason String?\n status String @default(\"active\")\n createdAt DateTime @default(now())\n expiresAt Float\n heartbeatSeconds Int @default(30)\n lastHeartbeatAt Float\n allocatedPorts String\n capabilityBag String\n\n @@index([status, expiresAt])\n @@index([status, lastHeartbeatAt, heartbeatSeconds])\n @@index([actorId])\n @@index([deviceUdid, deviceHost])\n}\n\nmodel ApiKey {\n id String @id @default(uuid())\n name String\n keyHash String @unique\n scopes String\n rateLimit Int @default(300)\n createdAt DateTime @default(now())\n revokedAt DateTime?\n lastUsedAt DateTime?\n expiresAt DateTime?\n teamId String?\n role String @default(\"member\")\n team Team? @relation(fields: [teamId], references: [id])\n\n // NEW\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([teamId])\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n createdAt DateTime @default(now())\n devices Device[]\n apiKeys ApiKey[]\n members TeamMember[]\n apps App[]\n}\n\nmodel SelectorState {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n status String\n fixed_at DateTime?\n fixed_by_api_key String?\n resolved_at DateTime?\n muted_at DateTime?\n muted_by_api_key String?\n regression_count Int @default(0)\n clean_builds_count Int @default(0)\n last_event_at DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([original_strategy, original_selector])\n @@index([status])\n @@index([fixed_at])\n}\n\n// What happened to a selector, and who did it: one row per status change.\n// No relation to SelectorState (unmute and cancel can delete that row; the\n// history stays) or to User (a deleted user's actions stay, without a name).\nmodel SelectorEvent {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n action String\n user_id String?\n reason String?\n createdAt DateTime @default(now())\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Recording {\n id String @id @default(uuid())\n group_id String\n device_udid String\n device_host String @default(\"127.0.0.1\")\n session_id String?\n started_at DateTime\n ended_at DateTime?\n status String\n file_path String\n duration_ms Int?\n size_bytes Int?\n device_snapshot String?\n fail_reason String?\n /// The user who started the recording (User.id); null on rows from before 1.29.\n started_by String?\n bookmarks Bookmark[]\n annotations Annotation[]\n Session Session? @relation(fields: [session_id], references: [id])\n\n @@index([group_id])\n @@index([device_udid])\n @@index([started_at])\n}\n\nmodel Bookmark {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n label String\n note String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\nmodel Annotation {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n end_timecode_ms Int?\n shape String\n geometry String\n color String\n text String?\n author String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\n// NOTE: SQLite does not support Prisma enums. UserRole / UserStatus are\n// modeled as TypeScript string-literal types in the application layer and\n// stored as plain TEXT here. Allowed values:\n// role: \"SUPER_ADMIN\" | \"ADMIN\" | \"MEMBER\" (default \"MEMBER\")\n// status: \"ACTIVE\" | \"INACTIVE\" (default \"ACTIVE\")\nmodel User {\n id String @id @default(uuid())\n email String @unique\n name String\n passwordHash String\n accessKey String @unique\n role String @default(\"MEMBER\")\n status String @default(\"ACTIVE\")\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n lastLoginAt DateTime?\n passwordChangedAt DateTime?\n\n apiKeys ApiKey[]\n sessions UserSession[]\n passwordResetTokens PasswordResetToken[]\n teamMemberships TeamMember[]\n}\n\nmodel UserSession {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n expiresAt DateTime\n lastSeenAt DateTime @default(now())\n userAgent String?\n ipHash String?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel PasswordResetToken {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n tokenHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime\n usedAt DateTime?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel TeamMember {\n teamId String\n team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([teamId, userId])\n @@index([userId])\n}\n\nmodel EventLog {\n id String @id @default(cuid())\n type String\n payload String // JSON string — portable, no SQLite-isms\n correlationId String?\n teamId String?\n occurredAt DateTime @default(now())\n\n @@index([type, occurredAt])\n @@index([occurredAt])\n}\n\nmodel Project {\n id String @id @default(cuid())\n name String\n teamId String?\n createdAt DateTime @default(now())\n\n @@index([teamId])\n}\n",
|
|
596
|
-
"inlineSchemaHash": "
|
|
595
|
+
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"darwin\", \"darwin-arm64\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\", \"debian-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Build {\n id String @id @default(uuid())\n name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n sessions Session[]\n}\n\nmodel Session {\n id String @id\n build_id String?\n name String?\n status String @default(\"running\")\n desired_capabilities String\n session_capabilities String\n node_id String\n has_live_video Boolean\n video_recording_enabled Boolean @default(true)\n video_recording String?\n startTime DateTime @default(now())\n endTime DateTime?\n failure_reason String?\n is_profiling_available Boolean @default(false)\n device_info String?\n device_udid String\n device_platform String\n device_version String\n device_name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n performance_trace String? @map(\"performance_trace\")\n failure_category String?\n ai_analysis String?\n tags String?\n trace_id String?\n last_heartbeat_at DateTime?\n heartbeat_pid Int?\n heartbeat_host String?\n api_key_id String?\n user_id String?\n Log Log[]\n Profiling Profiling[]\n SessionMetric SessionMetric[]\n build Build? @relation(fields: [build_id], references: [id])\n SessionLog SessionLog[]\n Recording Recording[]\n\n @@index([status, last_heartbeat_at])\n @@index([build_id])\n @@index([device_udid])\n @@index([device_platform, status])\n @@index([api_key_id])\n}\n\nmodel SessionLog {\n id String @id @default(uuid())\n session_id String\n command_name String?\n url String\n method String\n title String\n subtitle String?\n body String?\n response String\n screenshot String?\n is_success Boolean?\n is_error Boolean @default(false)\n is_healed Boolean @default(false)\n original_selector String?\n healed_selector String?\n healing_confidence Float?\n healing_tier String?\n original_strategy String?\n healed_strategy String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n duration Int?\n span_id String?\n trace_id String?\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([original_strategy, original_selector, createdAt])\n @@index([is_healed, createdAt])\n @@index([session_id, createdAt])\n}\n\nmodel Log {\n id String @id @default(uuid())\n session_id String\n log_type String\n message String\n timestamp DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([session_id, log_type, createdAt])\n}\n\nmodel Profiling {\n id Int @id @default(autoincrement())\n session_id String\n cpu String?\n memory String?\n total_cpu_used String?\n total_memory_used String?\n raw_cpu_log String?\n raw_memory_log String?\n timestamp DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([session_id, timestamp])\n}\n\n/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).\nmodel SessionMetric {\n id Int @id @default(autoincrement())\n session_id String\n /// epoch ms\n at Float\n device_cpu_pct Float?\n device_mem_mb Float?\n device_mem_total Float?\n app_cpu_pct Float?\n app_mem_mb Float?\n /// The package the app figures are for (Android), else null.\n app_id String?\n session Session @relation(fields: [session_id], references: [id], onDelete: Cascade)\n\n @@index([session_id, at])\n}\n\nmodel App {\n id String @id @default(uuid())\n name String\n filename String\n filepath String\n mimetype String\n size Int\n packageName String?\n version String?\n platform String?\n md5 String? @unique\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n // The team whose members may list, download and run this app. Null is the\n // shared pool, which everyone sees: the device team rule (isDeviceVisible).\n teamId String?\n team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)\n\n @@index([teamId])\n}\n\nmodel Device {\n udid String\n host String\n systemPort Int?\n proxyPort Int?\n proxyHost String?\n wdaLocalPort Int?\n name String? @default(\"unknown\")\n state String? @default(\"available\")\n sdk String? @default(\"unknown\")\n platform String? @default(\"unknown\")\n deviceType String? @default(\"real\")\n busy Boolean? @default(false)\n userBlocked Boolean? @default(false)\n realDevice Boolean? @default(true)\n session_id String?\n offline Boolean? @default(false)\n mjpegServerPort Int?\n lastCmdExecutedAt Float?\n totalUtilizationTimeMilliSec Float @default(0)\n sessionStartTime Float @default(0)\n newCommandTimeout Int?\n cloud String?\n derivedDataPath String?\n chromeDriverPath String?\n capability String?\n adbRemoteHost String?\n adbPort Int?\n nodeId String?\n screenWidth String?\n screenHeight String?\n dashboard_link String?\n total_session_count Int? @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n healthCheckError String?\n healthStatus String? @default(\"Healthy\")\n lastHealthCheckAt Float?\n batteryLevel Int?\n reservationReason String?\n reservedBy String?\n reservedUntil Float?\n storageFree String?\n tags String?\n thermalStatus String?\n marketingName String?\n model String?\n manufacturer String?\n formFactor String?\n sessionProgress String? @default(\"\")\n totalHealedCount Int? @default(0)\n ip String? @default(\"\")\n cpuArchitecture String?\n owningSessionId String? @map(\"owning_session_id\")\n lockedAt Float? @map(\"locked_at\")\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n // This server's claim on the phone for a session: when it was taken, and\n // the session's id once the session exists (null while it is created).\n // See src/data-service/deviceClaims.ts.\n claimSessionId String?\n claimedAt Float?\n // On a hub, what the phone's node last reported: busy there. `busy` is the\n // claim OR this. Always false for a phone this server drives.\n nodeBusy Boolean? @default(false)\n // On a hub, who holds the phone there: the preview hold (manual_<user>_<udid>)\n // in the node's report of its session_id, else null. Shown, never judged:\n // the node decides access. Always null for a phone this server drives.\n nodeHold String?\n\n @@id([udid, host])\n @@index([owningSessionId])\n @@index([teamId])\n @@index([claimSessionId])\n}\n\nmodel PendingSession {\n id Int @id @default(autoincrement())\n capability_id String @unique\n capability String\n createdAt Float\n}\n\nmodel CLIArgs {\n id Int @id @default(autoincrement())\n args String\n createdAt DateTime @default(now())\n}\n\nmodel WebhookConfig {\n id String @id @default(uuid())\n url String\n type String @default(\"slack\")\n events String\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n payloadTemplate String?\n}\n\nmodel WebConfig {\n id String @id @default(\"global\")\n name String @unique\n value String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel LocatorEtalon {\n id String @id @default(uuid())\n selector String @unique\n strategy String\n attributes String\n nodeName String\n lastSeen DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel PortLease {\n port Int @id\n purpose String\n leasedToUdid String\n leasedToHost String @default(\"\")\n leaseId String?\n leasedToPid Int?\n leasedAt Float\n expiresAt Float\n\n @@index([purpose, expiresAt])\n @@index([leasedToUdid])\n @@index([leaseId])\n @@index([expiresAt])\n}\n\nmodel Lease {\n id String @id @default(cuid())\n tokenHash String\n deviceUdid String\n deviceHost String\n actorId String\n teamId String?\n buildId String?\n reason String?\n status String @default(\"active\")\n createdAt DateTime @default(now())\n expiresAt Float\n heartbeatSeconds Int @default(30)\n lastHeartbeatAt Float\n allocatedPorts String\n capabilityBag String\n\n @@index([status, expiresAt])\n @@index([status, lastHeartbeatAt, heartbeatSeconds])\n @@index([actorId])\n @@index([deviceUdid, deviceHost])\n}\n\nmodel ApiKey {\n id String @id @default(uuid())\n name String\n keyHash String @unique\n scopes String\n rateLimit Int @default(300)\n createdAt DateTime @default(now())\n revokedAt DateTime?\n lastUsedAt DateTime?\n expiresAt DateTime?\n teamId String?\n role String @default(\"member\")\n team Team? @relation(fields: [teamId], references: [id])\n\n // NEW\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([teamId])\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n createdAt DateTime @default(now())\n devices Device[]\n apiKeys ApiKey[]\n members TeamMember[]\n apps App[]\n}\n\nmodel SelectorState {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n status String\n fixed_at DateTime?\n fixed_by_api_key String?\n resolved_at DateTime?\n muted_at DateTime?\n muted_by_api_key String?\n regression_count Int @default(0)\n clean_builds_count Int @default(0)\n last_event_at DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([original_strategy, original_selector])\n @@index([status])\n @@index([fixed_at])\n}\n\n// What happened to a selector, and who did it: one row per status change.\n// No relation to SelectorState (unmute and cancel can delete that row; the\n// history stays) or to User (a deleted user's actions stay, without a name).\nmodel SelectorEvent {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n action String\n user_id String?\n reason String?\n createdAt DateTime @default(now())\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Recording {\n id String @id @default(uuid())\n group_id String\n device_udid String\n device_host String @default(\"127.0.0.1\")\n session_id String?\n started_at DateTime\n ended_at DateTime?\n status String\n file_path String\n duration_ms Int?\n size_bytes Int?\n device_snapshot String?\n fail_reason String?\n /// The user who started the recording (User.id); null on rows from before 1.29.\n started_by String?\n bookmarks Bookmark[]\n annotations Annotation[]\n Session Session? @relation(fields: [session_id], references: [id])\n\n @@index([group_id])\n @@index([device_udid])\n @@index([started_at])\n}\n\nmodel Bookmark {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n label String\n note String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\nmodel Annotation {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n end_timecode_ms Int?\n shape String\n geometry String\n color String\n text String?\n author String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\n// NOTE: SQLite does not support Prisma enums. UserRole / UserStatus are\n// modeled as TypeScript string-literal types in the application layer and\n// stored as plain TEXT here. Allowed values:\n// role: \"SUPER_ADMIN\" | \"ADMIN\" | \"MEMBER\" (default \"MEMBER\")\n// status: \"ACTIVE\" | \"INACTIVE\" (default \"ACTIVE\")\nmodel User {\n id String @id @default(uuid())\n email String @unique\n name String\n passwordHash String\n accessKey String @unique\n role String @default(\"MEMBER\")\n status String @default(\"ACTIVE\")\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n lastLoginAt DateTime?\n passwordChangedAt DateTime?\n\n apiKeys ApiKey[]\n sessions UserSession[]\n passwordResetTokens PasswordResetToken[]\n teamMemberships TeamMember[]\n}\n\nmodel UserSession {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n expiresAt DateTime\n lastSeenAt DateTime @default(now())\n userAgent String?\n ipHash String?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel PasswordResetToken {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n tokenHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime\n usedAt DateTime?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel TeamMember {\n teamId String\n team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([teamId, userId])\n @@index([userId])\n}\n\nmodel EventLog {\n id String @id @default(cuid())\n type String\n payload String // JSON string — portable, no SQLite-isms\n correlationId String?\n teamId String?\n occurredAt DateTime @default(now())\n\n @@index([type, occurredAt])\n @@index([occurredAt])\n}\n\nmodel Project {\n id String @id @default(cuid())\n name String\n teamId String?\n createdAt DateTime @default(now())\n\n @@index([teamId])\n}\n",
|
|
596
|
+
"inlineSchemaHash": "f64f65a5c04029fcf48b9a7435bacb277188cd67aa012d504c043a090fa93582",
|
|
597
597
|
"copyEngine": true
|
|
598
598
|
}
|
|
599
599
|
config.dirname = '/'
|
|
@@ -593,8 +593,8 @@ const config = {
|
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
595
|
},
|
|
596
|
-
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"darwin\", \"darwin-arm64\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\", \"debian-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Build {\n id String @id @default(uuid())\n name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n sessions Session[]\n}\n\nmodel Session {\n id String @id\n build_id String?\n name String?\n status String @default(\"running\")\n desired_capabilities String\n session_capabilities String\n node_id String\n has_live_video Boolean\n video_recording_enabled Boolean @default(true)\n video_recording String?\n startTime DateTime @default(now())\n endTime DateTime?\n failure_reason String?\n is_profiling_available Boolean @default(false)\n device_info String?\n device_udid String\n device_platform String\n device_version String\n device_name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n performance_trace String? @map(\"performance_trace\")\n failure_category String?\n ai_analysis String?\n tags String?\n trace_id String?\n last_heartbeat_at DateTime?\n heartbeat_pid Int?\n heartbeat_host String?\n api_key_id String?\n user_id String?\n Log Log[]\n Profiling Profiling[]\n SessionMetric SessionMetric[]\n build Build? @relation(fields: [build_id], references: [id])\n SessionLog SessionLog[]\n Recording Recording[]\n\n @@index([status, last_heartbeat_at])\n @@index([build_id])\n @@index([device_udid])\n @@index([device_platform, status])\n @@index([api_key_id])\n}\n\nmodel SessionLog {\n id String @id @default(uuid())\n session_id String\n command_name String?\n url String\n method String\n title String\n subtitle String?\n body String?\n response String\n screenshot String?\n is_success Boolean?\n is_error Boolean @default(false)\n is_healed Boolean @default(false)\n original_selector String?\n healed_selector String?\n healing_confidence Float?\n healing_tier String?\n original_strategy String?\n healed_strategy String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n duration Int?\n span_id String?\n trace_id String?\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Log {\n id String @id @default(uuid())\n session_id String\n log_type String\n message String\n timestamp DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n}\n\nmodel Profiling {\n id Int @id @default(autoincrement())\n session_id String\n cpu String?\n memory String?\n total_cpu_used String?\n total_memory_used String?\n raw_cpu_log String?\n raw_memory_log String?\n timestamp DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n}\n\n/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).\nmodel SessionMetric {\n id Int @id @default(autoincrement())\n session_id String\n /// epoch ms\n at Float\n device_cpu_pct Float?\n device_mem_mb Float?\n device_mem_total Float?\n app_cpu_pct Float?\n app_mem_mb Float?\n /// The package the app figures are for (Android), else null.\n app_id String?\n session Session @relation(fields: [session_id], references: [id], onDelete: Cascade)\n\n @@index([session_id, at])\n}\n\nmodel App {\n id String @id @default(uuid())\n name String\n filename String\n filepath String\n mimetype String\n size Int\n packageName String?\n version String?\n platform String?\n md5 String? @unique\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n // The team whose members may list, download and run this app. Null is the\n // shared pool, which everyone sees: the device team rule (isDeviceVisible).\n teamId String?\n team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)\n\n @@index([teamId])\n}\n\nmodel Device {\n udid String\n host String\n systemPort Int?\n proxyPort Int?\n proxyHost String?\n wdaLocalPort Int?\n name String? @default(\"unknown\")\n state String? @default(\"available\")\n sdk String? @default(\"unknown\")\n platform String? @default(\"unknown\")\n deviceType String? @default(\"real\")\n busy Boolean? @default(false)\n userBlocked Boolean? @default(false)\n realDevice Boolean? @default(true)\n session_id String?\n offline Boolean? @default(false)\n mjpegServerPort Int?\n lastCmdExecutedAt Float?\n totalUtilizationTimeMilliSec Float @default(0)\n sessionStartTime Float @default(0)\n newCommandTimeout Int?\n cloud String?\n derivedDataPath String?\n chromeDriverPath String?\n capability String?\n adbRemoteHost String?\n adbPort Int?\n nodeId String?\n screenWidth String?\n screenHeight String?\n dashboard_link String?\n total_session_count Int? @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n healthCheckError String?\n healthStatus String? @default(\"Healthy\")\n lastHealthCheckAt Float?\n batteryLevel Int?\n reservationReason String?\n reservedBy String?\n reservedUntil Float?\n storageFree String?\n tags String?\n thermalStatus String?\n marketingName String?\n model String?\n manufacturer String?\n formFactor String?\n sessionProgress String? @default(\"\")\n totalHealedCount Int? @default(0)\n ip String? @default(\"\")\n cpuArchitecture String?\n owningSessionId String? @map(\"owning_session_id\")\n lockedAt Float? @map(\"locked_at\")\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n // This server's claim on the phone for a session: when it was taken, and\n // the session's id once the session exists (null while it is created).\n // See src/data-service/deviceClaims.ts.\n claimSessionId String?\n claimedAt Float?\n // On a hub, what the phone's node last reported: busy there. `busy` is the\n // claim OR this. Always false for a phone this server drives.\n nodeBusy Boolean? @default(false)\n // On a hub, who holds the phone there: the preview hold (manual_<user>_<udid>)\n // in the node's report of its session_id, else null. Shown, never judged:\n // the node decides access. Always null for a phone this server drives.\n nodeHold String?\n\n @@id([udid, host])\n @@index([owningSessionId])\n @@index([teamId])\n @@index([claimSessionId])\n}\n\nmodel PendingSession {\n id Int @id @default(autoincrement())\n capability_id String @unique\n capability String\n createdAt Float\n}\n\nmodel CLIArgs {\n id Int @id @default(autoincrement())\n args String\n createdAt DateTime @default(now())\n}\n\nmodel WebhookConfig {\n id String @id @default(uuid())\n url String\n type String @default(\"slack\")\n events String\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n payloadTemplate String?\n}\n\nmodel WebConfig {\n id String @id @default(\"global\")\n name String @unique\n value String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel LocatorEtalon {\n id String @id @default(uuid())\n selector String @unique\n strategy String\n attributes String\n nodeName String\n lastSeen DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel PortLease {\n port Int @id\n purpose String\n leasedToUdid String\n leasedToHost String @default(\"\")\n leaseId String?\n leasedToPid Int?\n leasedAt Float\n expiresAt Float\n\n @@index([purpose, expiresAt])\n @@index([leasedToUdid])\n @@index([leaseId])\n @@index([expiresAt])\n}\n\nmodel Lease {\n id String @id @default(cuid())\n tokenHash String\n deviceUdid String\n deviceHost String\n actorId String\n teamId String?\n buildId String?\n reason String?\n status String @default(\"active\")\n createdAt DateTime @default(now())\n expiresAt Float\n heartbeatSeconds Int @default(30)\n lastHeartbeatAt Float\n allocatedPorts String\n capabilityBag String\n\n @@index([status, expiresAt])\n @@index([status, lastHeartbeatAt, heartbeatSeconds])\n @@index([actorId])\n @@index([deviceUdid, deviceHost])\n}\n\nmodel ApiKey {\n id String @id @default(uuid())\n name String\n keyHash String @unique\n scopes String\n rateLimit Int @default(300)\n createdAt DateTime @default(now())\n revokedAt DateTime?\n lastUsedAt DateTime?\n expiresAt DateTime?\n teamId String?\n role String @default(\"member\")\n team Team? @relation(fields: [teamId], references: [id])\n\n // NEW\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([teamId])\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n createdAt DateTime @default(now())\n devices Device[]\n apiKeys ApiKey[]\n members TeamMember[]\n apps App[]\n}\n\nmodel SelectorState {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n status String\n fixed_at DateTime?\n fixed_by_api_key String?\n resolved_at DateTime?\n muted_at DateTime?\n muted_by_api_key String?\n regression_count Int @default(0)\n clean_builds_count Int @default(0)\n last_event_at DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([original_strategy, original_selector])\n @@index([status])\n @@index([fixed_at])\n}\n\n// What happened to a selector, and who did it: one row per status change.\n// No relation to SelectorState (unmute and cancel can delete that row; the\n// history stays) or to User (a deleted user's actions stay, without a name).\nmodel SelectorEvent {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n action String\n user_id String?\n reason String?\n createdAt DateTime @default(now())\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Recording {\n id String @id @default(uuid())\n group_id String\n device_udid String\n device_host String @default(\"127.0.0.1\")\n session_id String?\n started_at DateTime\n ended_at DateTime?\n status String\n file_path String\n duration_ms Int?\n size_bytes Int?\n device_snapshot String?\n fail_reason String?\n /// The user who started the recording (User.id); null on rows from before 1.29.\n started_by String?\n bookmarks Bookmark[]\n annotations Annotation[]\n Session Session? @relation(fields: [session_id], references: [id])\n\n @@index([group_id])\n @@index([device_udid])\n @@index([started_at])\n}\n\nmodel Bookmark {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n label String\n note String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\nmodel Annotation {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n end_timecode_ms Int?\n shape String\n geometry String\n color String\n text String?\n author String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\n// NOTE: SQLite does not support Prisma enums. UserRole / UserStatus are\n// modeled as TypeScript string-literal types in the application layer and\n// stored as plain TEXT here. Allowed values:\n// role: \"SUPER_ADMIN\" | \"ADMIN\" | \"MEMBER\" (default \"MEMBER\")\n// status: \"ACTIVE\" | \"INACTIVE\" (default \"ACTIVE\")\nmodel User {\n id String @id @default(uuid())\n email String @unique\n name String\n passwordHash String\n accessKey String @unique\n role String @default(\"MEMBER\")\n status String @default(\"ACTIVE\")\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n lastLoginAt DateTime?\n passwordChangedAt DateTime?\n\n apiKeys ApiKey[]\n sessions UserSession[]\n passwordResetTokens PasswordResetToken[]\n teamMemberships TeamMember[]\n}\n\nmodel UserSession {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n expiresAt DateTime\n lastSeenAt DateTime @default(now())\n userAgent String?\n ipHash String?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel PasswordResetToken {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n tokenHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime\n usedAt DateTime?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel TeamMember {\n teamId String\n team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([teamId, userId])\n @@index([userId])\n}\n\nmodel EventLog {\n id String @id @default(cuid())\n type String\n payload String // JSON string — portable, no SQLite-isms\n correlationId String?\n teamId String?\n occurredAt DateTime @default(now())\n\n @@index([type, occurredAt])\n @@index([occurredAt])\n}\n\nmodel Project {\n id String @id @default(cuid())\n name String\n teamId String?\n createdAt DateTime @default(now())\n\n @@index([teamId])\n}\n",
|
|
597
|
-
"inlineSchemaHash": "
|
|
596
|
+
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"darwin\", \"darwin-arm64\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\", \"debian-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Build {\n id String @id @default(uuid())\n name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n sessions Session[]\n}\n\nmodel Session {\n id String @id\n build_id String?\n name String?\n status String @default(\"running\")\n desired_capabilities String\n session_capabilities String\n node_id String\n has_live_video Boolean\n video_recording_enabled Boolean @default(true)\n video_recording String?\n startTime DateTime @default(now())\n endTime DateTime?\n failure_reason String?\n is_profiling_available Boolean @default(false)\n device_info String?\n device_udid String\n device_platform String\n device_version String\n device_name String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n performance_trace String? @map(\"performance_trace\")\n failure_category String?\n ai_analysis String?\n tags String?\n trace_id String?\n last_heartbeat_at DateTime?\n heartbeat_pid Int?\n heartbeat_host String?\n api_key_id String?\n user_id String?\n Log Log[]\n Profiling Profiling[]\n SessionMetric SessionMetric[]\n build Build? @relation(fields: [build_id], references: [id])\n SessionLog SessionLog[]\n Recording Recording[]\n\n @@index([status, last_heartbeat_at])\n @@index([build_id])\n @@index([device_udid])\n @@index([device_platform, status])\n @@index([api_key_id])\n}\n\nmodel SessionLog {\n id String @id @default(uuid())\n session_id String\n command_name String?\n url String\n method String\n title String\n subtitle String?\n body String?\n response String\n screenshot String?\n is_success Boolean?\n is_error Boolean @default(false)\n is_healed Boolean @default(false)\n original_selector String?\n healed_selector String?\n healing_confidence Float?\n healing_tier String?\n original_strategy String?\n healed_strategy String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n duration Int?\n span_id String?\n trace_id String?\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([original_strategy, original_selector, createdAt])\n @@index([is_healed, createdAt])\n @@index([session_id, createdAt])\n}\n\nmodel Log {\n id String @id @default(uuid())\n session_id String\n log_type String\n message String\n timestamp DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([session_id, log_type, createdAt])\n}\n\nmodel Profiling {\n id Int @id @default(autoincrement())\n session_id String\n cpu String?\n memory String?\n total_cpu_used String?\n total_memory_used String?\n raw_cpu_log String?\n raw_memory_log String?\n timestamp DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n session Session @relation(fields: [session_id], references: [id])\n\n @@index([session_id, timestamp])\n}\n\n/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).\nmodel SessionMetric {\n id Int @id @default(autoincrement())\n session_id String\n /// epoch ms\n at Float\n device_cpu_pct Float?\n device_mem_mb Float?\n device_mem_total Float?\n app_cpu_pct Float?\n app_mem_mb Float?\n /// The package the app figures are for (Android), else null.\n app_id String?\n session Session @relation(fields: [session_id], references: [id], onDelete: Cascade)\n\n @@index([session_id, at])\n}\n\nmodel App {\n id String @id @default(uuid())\n name String\n filename String\n filepath String\n mimetype String\n size Int\n packageName String?\n version String?\n platform String?\n md5 String? @unique\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n // The team whose members may list, download and run this app. Null is the\n // shared pool, which everyone sees: the device team rule (isDeviceVisible).\n teamId String?\n team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)\n\n @@index([teamId])\n}\n\nmodel Device {\n udid String\n host String\n systemPort Int?\n proxyPort Int?\n proxyHost String?\n wdaLocalPort Int?\n name String? @default(\"unknown\")\n state String? @default(\"available\")\n sdk String? @default(\"unknown\")\n platform String? @default(\"unknown\")\n deviceType String? @default(\"real\")\n busy Boolean? @default(false)\n userBlocked Boolean? @default(false)\n realDevice Boolean? @default(true)\n session_id String?\n offline Boolean? @default(false)\n mjpegServerPort Int?\n lastCmdExecutedAt Float?\n totalUtilizationTimeMilliSec Float @default(0)\n sessionStartTime Float @default(0)\n newCommandTimeout Int?\n cloud String?\n derivedDataPath String?\n chromeDriverPath String?\n capability String?\n adbRemoteHost String?\n adbPort Int?\n nodeId String?\n screenWidth String?\n screenHeight String?\n dashboard_link String?\n total_session_count Int? @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n healthCheckError String?\n healthStatus String? @default(\"Healthy\")\n lastHealthCheckAt Float?\n batteryLevel Int?\n reservationReason String?\n reservedBy String?\n reservedUntil Float?\n storageFree String?\n tags String?\n thermalStatus String?\n marketingName String?\n model String?\n manufacturer String?\n formFactor String?\n sessionProgress String? @default(\"\")\n totalHealedCount Int? @default(0)\n ip String? @default(\"\")\n cpuArchitecture String?\n owningSessionId String? @map(\"owning_session_id\")\n lockedAt Float? @map(\"locked_at\")\n teamId String?\n team Team? @relation(fields: [teamId], references: [id])\n // This server's claim on the phone for a session: when it was taken, and\n // the session's id once the session exists (null while it is created).\n // See src/data-service/deviceClaims.ts.\n claimSessionId String?\n claimedAt Float?\n // On a hub, what the phone's node last reported: busy there. `busy` is the\n // claim OR this. Always false for a phone this server drives.\n nodeBusy Boolean? @default(false)\n // On a hub, who holds the phone there: the preview hold (manual_<user>_<udid>)\n // in the node's report of its session_id, else null. Shown, never judged:\n // the node decides access. Always null for a phone this server drives.\n nodeHold String?\n\n @@id([udid, host])\n @@index([owningSessionId])\n @@index([teamId])\n @@index([claimSessionId])\n}\n\nmodel PendingSession {\n id Int @id @default(autoincrement())\n capability_id String @unique\n capability String\n createdAt Float\n}\n\nmodel CLIArgs {\n id Int @id @default(autoincrement())\n args String\n createdAt DateTime @default(now())\n}\n\nmodel WebhookConfig {\n id String @id @default(uuid())\n url String\n type String @default(\"slack\")\n events String\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n payloadTemplate String?\n}\n\nmodel WebConfig {\n id String @id @default(\"global\")\n name String @unique\n value String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel LocatorEtalon {\n id String @id @default(uuid())\n selector String @unique\n strategy String\n attributes String\n nodeName String\n lastSeen DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel PortLease {\n port Int @id\n purpose String\n leasedToUdid String\n leasedToHost String @default(\"\")\n leaseId String?\n leasedToPid Int?\n leasedAt Float\n expiresAt Float\n\n @@index([purpose, expiresAt])\n @@index([leasedToUdid])\n @@index([leaseId])\n @@index([expiresAt])\n}\n\nmodel Lease {\n id String @id @default(cuid())\n tokenHash String\n deviceUdid String\n deviceHost String\n actorId String\n teamId String?\n buildId String?\n reason String?\n status String @default(\"active\")\n createdAt DateTime @default(now())\n expiresAt Float\n heartbeatSeconds Int @default(30)\n lastHeartbeatAt Float\n allocatedPorts String\n capabilityBag String\n\n @@index([status, expiresAt])\n @@index([status, lastHeartbeatAt, heartbeatSeconds])\n @@index([actorId])\n @@index([deviceUdid, deviceHost])\n}\n\nmodel ApiKey {\n id String @id @default(uuid())\n name String\n keyHash String @unique\n scopes String\n rateLimit Int @default(300)\n createdAt DateTime @default(now())\n revokedAt DateTime?\n lastUsedAt DateTime?\n expiresAt DateTime?\n teamId String?\n role String @default(\"member\")\n team Team? @relation(fields: [teamId], references: [id])\n\n // NEW\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([teamId])\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel Team {\n id String @id @default(uuid())\n name String @unique\n createdAt DateTime @default(now())\n devices Device[]\n apiKeys ApiKey[]\n members TeamMember[]\n apps App[]\n}\n\nmodel SelectorState {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n status String\n fixed_at DateTime?\n fixed_by_api_key String?\n resolved_at DateTime?\n muted_at DateTime?\n muted_by_api_key String?\n regression_count Int @default(0)\n clean_builds_count Int @default(0)\n last_event_at DateTime @default(now())\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([original_strategy, original_selector])\n @@index([status])\n @@index([fixed_at])\n}\n\n// What happened to a selector, and who did it: one row per status change.\n// No relation to SelectorState (unmute and cancel can delete that row; the\n// history stays) or to User (a deleted user's actions stay, without a name).\nmodel SelectorEvent {\n id String @id @default(uuid())\n original_strategy String\n original_selector String\n action String\n user_id String?\n reason String?\n createdAt DateTime @default(now())\n\n @@index([original_strategy, original_selector, createdAt])\n}\n\nmodel Recording {\n id String @id @default(uuid())\n group_id String\n device_udid String\n device_host String @default(\"127.0.0.1\")\n session_id String?\n started_at DateTime\n ended_at DateTime?\n status String\n file_path String\n duration_ms Int?\n size_bytes Int?\n device_snapshot String?\n fail_reason String?\n /// The user who started the recording (User.id); null on rows from before 1.29.\n started_by String?\n bookmarks Bookmark[]\n annotations Annotation[]\n Session Session? @relation(fields: [session_id], references: [id])\n\n @@index([group_id])\n @@index([device_udid])\n @@index([started_at])\n}\n\nmodel Bookmark {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n label String\n note String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\nmodel Annotation {\n id String @id @default(uuid())\n recording_id String\n timecode_ms Int\n end_timecode_ms Int?\n shape String\n geometry String\n color String\n text String?\n author String?\n created_at DateTime @default(now())\n Recording Recording @relation(fields: [recording_id], references: [id], onDelete: Cascade)\n\n @@index([recording_id, timecode_ms])\n}\n\n// NOTE: SQLite does not support Prisma enums. UserRole / UserStatus are\n// modeled as TypeScript string-literal types in the application layer and\n// stored as plain TEXT here. Allowed values:\n// role: \"SUPER_ADMIN\" | \"ADMIN\" | \"MEMBER\" (default \"MEMBER\")\n// status: \"ACTIVE\" | \"INACTIVE\" (default \"ACTIVE\")\nmodel User {\n id String @id @default(uuid())\n email String @unique\n name String\n passwordHash String\n accessKey String @unique\n role String @default(\"MEMBER\")\n status String @default(\"ACTIVE\")\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n lastLoginAt DateTime?\n passwordChangedAt DateTime?\n\n apiKeys ApiKey[]\n sessions UserSession[]\n passwordResetTokens PasswordResetToken[]\n teamMemberships TeamMember[]\n}\n\nmodel UserSession {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n expiresAt DateTime\n lastSeenAt DateTime @default(now())\n userAgent String?\n ipHash String?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel PasswordResetToken {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n tokenHash String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime\n usedAt DateTime?\n\n @@index([userId])\n @@index([expiresAt])\n}\n\nmodel TeamMember {\n teamId String\n team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([teamId, userId])\n @@index([userId])\n}\n\nmodel EventLog {\n id String @id @default(cuid())\n type String\n payload String // JSON string — portable, no SQLite-isms\n correlationId String?\n teamId String?\n occurredAt DateTime @default(now())\n\n @@index([type, occurredAt])\n @@index([occurredAt])\n}\n\nmodel Project {\n id String @id @default(cuid())\n name String\n teamId String?\n createdAt DateTime @default(now())\n\n @@index([teamId])\n}\n",
|
|
597
|
+
"inlineSchemaHash": "f64f65a5c04029fcf48b9a7435bacb277188cd67aa012d504c043a090fa93582",
|
|
598
598
|
"copyEngine": true
|
|
599
599
|
}
|
|
600
600
|
|
|
@@ -91,6 +91,8 @@ model SessionLog {
|
|
|
91
91
|
session Session @relation(fields: [session_id], references: [id])
|
|
92
92
|
|
|
93
93
|
@@index([original_strategy, original_selector, createdAt])
|
|
94
|
+
@@index([is_healed, createdAt])
|
|
95
|
+
@@index([session_id, createdAt])
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
model Log {
|
|
@@ -102,6 +104,8 @@ model Log {
|
|
|
102
104
|
createdAt DateTime @default(now())
|
|
103
105
|
updatedAt DateTime @updatedAt
|
|
104
106
|
session Session @relation(fields: [session_id], references: [id])
|
|
107
|
+
|
|
108
|
+
@@index([session_id, log_type, createdAt])
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
model Profiling {
|
|
@@ -117,6 +121,8 @@ model Profiling {
|
|
|
117
121
|
createdAt DateTime @default(now())
|
|
118
122
|
updatedAt DateTime @updatedAt
|
|
119
123
|
session Session @relation(fields: [session_id], references: [id])
|
|
124
|
+
|
|
125
|
+
@@index([session_id, timestamp])
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).
|
package/lib/src/plugin.js
CHANGED
|
@@ -66,6 +66,7 @@ const IPluginArgs_1 = require("./interfaces/IPluginArgs");
|
|
|
66
66
|
const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
|
|
67
67
|
const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
|
|
68
68
|
const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
|
|
69
|
+
const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
|
|
69
70
|
const config_1 = require("./config");
|
|
70
71
|
const SessionManager_1 = require("./sessions/SessionManager");
|
|
71
72
|
const event_manager_1 = require("./dashboard/event-manager");
|
|
@@ -98,6 +99,8 @@ class XenonPlugin extends base_plugin_1.default {
|
|
|
98
99
|
var _a;
|
|
99
100
|
const sessionId = driver.sessionId;
|
|
100
101
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
102
|
+
if (sessionId)
|
|
103
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
101
104
|
const deviceFilter = {
|
|
102
105
|
session_id: sessionId ? sessionId : undefined,
|
|
103
106
|
udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
|
|
@@ -28,6 +28,7 @@ const SocketServer_1 = require("./SocketServer");
|
|
|
28
28
|
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
29
29
|
const logger_1 = __importDefault(require("../logger"));
|
|
30
30
|
const SelectorStateService_1 = require("./SelectorStateService");
|
|
31
|
+
const selectorKeys_1 = require("./selector-health/selectorKeys");
|
|
31
32
|
const log = logger_1.default.scope('SelectorVerification');
|
|
32
33
|
/**
|
|
33
34
|
* Threshold of distinct clean build_ids required to promote a Pending
|
|
@@ -51,24 +52,68 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
51
52
|
this.socket = socket !== null && socket !== void 0 ? socket : typedi_1.Container.get(SocketServer_1.SocketServer);
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
|
-
* Process every row currently in `'pending'
|
|
55
|
-
*
|
|
56
|
-
* remainder of the run.
|
|
55
|
+
* Process every row currently in `'pending'`, then look for a heal of every
|
|
56
|
+
* `'resolved'` one. Each row is wrapped in its own try/catch so a query
|
|
57
|
+
* failure on one selector can't take down the remainder of the run.
|
|
57
58
|
*/
|
|
58
59
|
run() {
|
|
59
60
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
-
var _a;
|
|
61
|
+
var _a, _b;
|
|
61
62
|
const pending = yield this.prisma.selectorState.findMany({
|
|
62
63
|
where: { status: 'pending' },
|
|
63
64
|
});
|
|
64
65
|
for (const row of pending) {
|
|
65
66
|
try {
|
|
66
|
-
yield this.
|
|
67
|
+
if (!(yield this.brokeAgain(row)))
|
|
68
|
+
yield this.processOne(row);
|
|
67
69
|
}
|
|
68
70
|
catch (err) {
|
|
69
71
|
log.error(`[${row.id}] processing failed (${row.original_strategy}=${row.original_selector}): ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
74
|
+
const resolved = yield this.prisma.selectorState.findMany({
|
|
75
|
+
where: { status: 'resolved' },
|
|
76
|
+
});
|
|
77
|
+
for (const row of resolved) {
|
|
78
|
+
try {
|
|
79
|
+
yield this.brokeAgain(row);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
log.error(`[${row.id}] heal check failed (${row.original_strategy}=${row.original_selector}): ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A heal since the selector was marked fixed means it broke again. The heal
|
|
89
|
+
* write path says so at once (`onHealRecorded`), but that call can fail and
|
|
90
|
+
* isn't tried again, and the clean-build count below doesn't look at healed
|
|
91
|
+
* builds: a selector with three clean builds was promoted however often it
|
|
92
|
+
* healed in others. So the job checks too, and makes the same change.
|
|
93
|
+
*/
|
|
94
|
+
brokeAgain(row) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
if (!row.fixed_at)
|
|
97
|
+
return false;
|
|
98
|
+
const heal = yield this.prisma.sessionLog.findFirst({
|
|
99
|
+
where: {
|
|
100
|
+
AND: [
|
|
101
|
+
{ is_healed: true, createdAt: { gte: row.fixed_at } },
|
|
102
|
+
(0, selectorKeys_1.tupleWhere)(row.original_strategy, row.original_selector),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
orderBy: { createdAt: 'desc' },
|
|
106
|
+
select: { session_id: true },
|
|
107
|
+
});
|
|
108
|
+
if (!heal)
|
|
109
|
+
return false;
|
|
110
|
+
log.warn(`[${row.id}] healed again after being marked fixed, unnoticed when it healed (${row.original_strategy}=${row.original_selector}, session=${heal.session_id})`);
|
|
111
|
+
yield new SelectorStateService_1.SelectorStateService(this.prisma, this.socket).onHealRecorded({
|
|
112
|
+
strategy: row.original_strategy,
|
|
113
|
+
selector: row.original_selector,
|
|
114
|
+
sessionId: heal.session_id,
|
|
115
|
+
});
|
|
116
|
+
return true;
|
|
72
117
|
});
|
|
73
118
|
}
|
|
74
119
|
processOne(row) {
|
|
@@ -80,6 +80,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
|
80
80
|
const queueVisibility_1 = require("./device-access/queueVisibility");
|
|
81
81
|
const appVisibility_1 = require("./device-access/appVisibility");
|
|
82
82
|
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
83
|
+
const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
|
|
83
84
|
const appCapability_1 = require("./session/appCapability");
|
|
84
85
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
85
86
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
@@ -816,6 +817,16 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
816
817
|
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
817
818
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).record(sessionId, userId);
|
|
818
819
|
}
|
|
820
|
+
// A node samples its own phones for its hub, whatever its dashboard
|
|
821
|
+
// setting (the hub collects the figures). A hub and a standalone server
|
|
822
|
+
// start sampling in EventManager.onSessionStarted, once the row exists.
|
|
823
|
+
if (!this.isHub(context.pluginArgs) && sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
824
|
+
typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).start({
|
|
825
|
+
sessionId,
|
|
826
|
+
device: freshDevice,
|
|
827
|
+
capabilities: sessionResponse,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
819
830
|
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
820
831
|
});
|
|
821
832
|
}
|
|
@@ -1104,6 +1115,10 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1104
1115
|
finally {
|
|
1105
1116
|
if (sessionId) {
|
|
1106
1117
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1118
|
+
// Before the lock's "still in memory?" check, which a dashboard-off
|
|
1119
|
+
// node fails. On a node this ends the figures it holds for its hub;
|
|
1120
|
+
// on a hub it collects a node session's last ones. Idempotent.
|
|
1121
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1107
1122
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
1108
1123
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
1109
1124
|
if (!session) {
|
|
@@ -1238,6 +1253,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1238
1253
|
}
|
|
1239
1254
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1240
1255
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1256
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1241
1257
|
}));
|
|
1242
1258
|
});
|
|
1243
1259
|
}
|