Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+88
-24
@@ -1,6 +1,12 @@
|
||||
import { hostOperationTitles } from '../host-operation-labels.js';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { normalizeActivityQuery, type ActivityPage, type ActivityQuery, type ActivityRow, type ActivitySource } from '../activity.js';
|
||||
import {
|
||||
normalizeActivityQuery,
|
||||
type ActivityPage,
|
||||
type ActivityQuery,
|
||||
type ActivityRow,
|
||||
type ActivitySource
|
||||
} from '../activity.js';
|
||||
import { serverTitle } from '../ui/server-labels.js';
|
||||
import { db } from './db/index.js';
|
||||
import { ensureHostSchema } from './hosts/schema.js';
|
||||
@@ -9,16 +15,22 @@ import { importantOperationSql } from './hosts/observations.js';
|
||||
const initialized = new WeakSet<Database.Database>();
|
||||
const quote = (value: string) => `'${value.replaceAll("'", "''")}'`;
|
||||
const labelCase = (column: string, labels: Record<string, string>, fallback: string) =>
|
||||
`CASE ${column} ${Object.entries(labels).map(([key, value]) => `WHEN ${quote(key)} THEN ${quote(value)}`).join(' ')} ELSE ${quote(fallback)} END`;
|
||||
`CASE ${column} ${Object.entries(labels)
|
||||
.map(([key, value]) => `WHEN ${quote(key)} THEN ${quote(value)}`)
|
||||
.join(' ')} ELSE ${quote(fallback)} END`;
|
||||
|
||||
// Old attempts may no longer be the native_provisioning pointer. Their stable
|
||||
// idempotency keys still identify the same retained controller operation.
|
||||
const lifecycleKey = `CASE
|
||||
WHEN substr(o.idempotency_key,-6)=':stage' THEN substr(o.idempotency_key,1,length(o.idempotency_key)-6)
|
||||
${[':migration-create:', ':migrate-remove:'].map(marker => `WHEN instr(o.idempotency_key,${quote(marker)})>0
|
||||
${[':migration-create:', ':migrate-remove:']
|
||||
.map(
|
||||
(marker) => `WHEN instr(o.idempotency_key,${quote(marker)})>0
|
||||
AND substr(o.idempotency_key,instr(o.idempotency_key,${quote(marker)})+${marker.length})<>''
|
||||
AND substr(o.idempotency_key,instr(o.idempotency_key,${quote(marker)})+${marker.length}) NOT GLOB '*[^0-9]*'
|
||||
THEN substr(o.idempotency_key,1,instr(o.idempotency_key,${quote(marker)})-1)`).join('\n')}
|
||||
THEN substr(o.idempotency_key,1,instr(o.idempotency_key,${quote(marker)})-1)`
|
||||
)
|
||||
.join('\n')}
|
||||
ELSE o.idempotency_key END`;
|
||||
|
||||
/** One row per user-level operation or finding; no command payloads or observations are read. */
|
||||
@@ -128,19 +140,50 @@ normalized AS (
|
||||
)
|
||||
`;
|
||||
|
||||
type SqlRow = Omit<ActivityRow, 'source' | 'sources' | 'href'> & { sources: string; route: string; recordId: string; secondaryHostId: string | null };
|
||||
const sourceOrder: ActivitySource[] = ['maintenance', 'recovery', 'campaign', 'beta', 'autoscaling', 'provisioning', 'host'];
|
||||
type SqlRow = Omit<ActivityRow, 'source' | 'sources' | 'href'> & {
|
||||
sources: string;
|
||||
route: string;
|
||||
recordId: string;
|
||||
secondaryHostId: string | null;
|
||||
};
|
||||
const sourceOrder: ActivitySource[] = [
|
||||
'maintenance',
|
||||
'recovery',
|
||||
'campaign',
|
||||
'beta',
|
||||
'autoscaling',
|
||||
'provisioning',
|
||||
'host'
|
||||
];
|
||||
// Use the same membership rule for Maintenance history and its exclusion from the main log.
|
||||
const maintenanceMembershipSql = "(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)";
|
||||
const maintenanceMembershipSql =
|
||||
"(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)";
|
||||
function present(row: SqlRow): ActivityRow {
|
||||
const sources = sourceOrder.filter(source => row.sources.includes(`|${source}|`));
|
||||
const href = row.route === 'maintenance-job' ? `/maintenance?view=overview&job=${encodeURIComponent(row.recordId)}#job-${row.recordId}`
|
||||
: row.route === 'recovery' ? `/maintenance/${encodeURIComponent(row.recordId)}`
|
||||
: row.route === 'host' ? `/hosts/operations?operation=${encodeURIComponent(row.recordId)}&filter=all`
|
||||
: `/${row.route === 'campaign' ? 'campaigns' : row.route === 'beta' ? 'betas' : 'provisioning'}/${encodeURIComponent(row.recordId)}`;
|
||||
return { id: row.id, at: row.at, type: row.type, source: sources[0] ?? 'provisioning', sources,
|
||||
state: row.state, rawState: row.rawState, title: row.title, detail: row.detail,
|
||||
server: row.server, serverDeletedAt: row.serverDeletedAt, hostId: row.hostId, hostName: row.hostName, href };
|
||||
const sources = sourceOrder.filter((source) => row.sources.includes(`|${source}|`));
|
||||
const href =
|
||||
row.route === 'maintenance-job'
|
||||
? `/maintenance?view=overview&job=${encodeURIComponent(row.recordId)}#job-${row.recordId}`
|
||||
: row.route === 'recovery'
|
||||
? `/maintenance/${encodeURIComponent(row.recordId)}`
|
||||
: row.route === 'host'
|
||||
? `/hosts/operations?operation=${encodeURIComponent(row.recordId)}&filter=all`
|
||||
: `/${row.route === 'campaign' ? 'campaigns' : row.route === 'beta' ? 'betas' : 'provisioning'}/${encodeURIComponent(row.recordId)}`;
|
||||
return {
|
||||
id: row.id,
|
||||
at: row.at,
|
||||
type: row.type,
|
||||
source: sources[0] ?? 'provisioning',
|
||||
sources,
|
||||
state: row.state,
|
||||
rawState: row.rawState,
|
||||
title: row.title,
|
||||
detail: row.detail,
|
||||
server: row.server,
|
||||
serverDeletedAt: row.serverDeletedAt,
|
||||
hostId: row.hostId,
|
||||
hostName: row.hostName,
|
||||
href
|
||||
};
|
||||
}
|
||||
|
||||
/** All predicates, totals and pagination operate on the same retained SQL union. */
|
||||
@@ -149,7 +192,9 @@ export function activityHistory(input: ActivityQuery = {}, now = Date.now()): Ac
|
||||
const handle = db();
|
||||
ensureHostSchema(handle);
|
||||
if (!initialized.has(handle)) {
|
||||
handle.function('activity_server_title', { deterministic: true }, (name: unknown) => typeof name === 'string' ? serverTitle(name) : '');
|
||||
handle.function('activity_server_title', { deterministic: true }, (name: unknown) =>
|
||||
typeof name === 'string' ? serverTitle(name) : ''
|
||||
);
|
||||
initialized.add(handle);
|
||||
}
|
||||
const conditions: string[] = [];
|
||||
@@ -158,25 +203,44 @@ export function activityHistory(input: ActivityQuery = {}, now = Date.now()): Ac
|
||||
if (query.excludeProvisioning) conditions.push("route <> 'provisioning' AND type <> 'restart'");
|
||||
if (query.excludeMaintenance) conditions.push(`NOT ${maintenanceMembershipSql}`);
|
||||
const bindings: Record<string, string | number> = {};
|
||||
for (const key of ['server', 'type', 'state'] as const) if (query[key]) { conditions.push(`${key}=@${key}`); bindings[key] = query[key]; }
|
||||
if (query.host) { conditions.push('(hostId=@host OR secondaryHostId=@host)'); bindings.host = query.host; }
|
||||
for (const key of ['server', 'type', 'state'] as const)
|
||||
if (query[key]) {
|
||||
conditions.push(`${key}=@${key}`);
|
||||
bindings[key] = query[key];
|
||||
}
|
||||
if (query.host) {
|
||||
conditions.push('(hostId=@host OR secondaryHostId=@host)');
|
||||
bindings.host = query.host;
|
||||
}
|
||||
if (query.source) {
|
||||
// Maintenance is the user-facing umbrella; Recovery remains available as a narrower filter.
|
||||
conditions.push(query.source === 'maintenance' ? maintenanceMembershipSql : 'instr(sources,@source)>0');
|
||||
conditions.push(
|
||||
query.source === 'maintenance' ? maintenanceMembershipSql : 'instr(sources,@source)>0'
|
||||
);
|
||||
if (query.source !== 'maintenance') bindings.source = `|${query.source}|`;
|
||||
}
|
||||
if (query.days) { conditions.push('julianday(at)>=julianday(@since)'); bindings.since = new Date(now - query.days * 86_400_000).toISOString(); }
|
||||
if (query.days) {
|
||||
conditions.push('julianday(at)>=julianday(@since)');
|
||||
bindings.since = new Date(now - query.days * 86_400_000).toISOString();
|
||||
}
|
||||
if (query.q) {
|
||||
conditions.push("instr(lower(COALESCE(server,'') || ' ' || activity_server_title(server) || ' ' || COALESCE(hostName,'') || ' ' || COALESCE(hostId,'') || ' ' || title || ' ' || COALESCE(detail,'') || ' ' || id),@q)>0");
|
||||
conditions.push(
|
||||
"instr(lower(COALESCE(server,'') || ' ' || activity_server_title(server) || ' ' || COALESCE(hostName,'') || ' ' || COALESCE(hostId,'') || ' ' || title || ' ' || COALESCE(detail,'') || ' ' || id),@q)>0"
|
||||
);
|
||||
bindings.q = query.q.toLocaleLowerCase();
|
||||
}
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const limit = query.limit!;
|
||||
return handle.transaction(() => {
|
||||
const { total } = handle.prepare(`${activitySql} SELECT COUNT(*) AS total FROM normalized ${where}`).get(bindings) as { total: number };
|
||||
const { total } = handle
|
||||
.prepare(`${activitySql} SELECT COUNT(*) AS total FROM normalized ${where}`)
|
||||
.get(bindings) as { total: number };
|
||||
const offset = Math.min(query.offset!, Math.max(0, Math.ceil(total / limit) - 1) * limit);
|
||||
const rows = handle.prepare(`${activitySql} SELECT id,at,type,sources,state,rawState,title,detail,server,serverDeletedAt,hostId,hostName,secondaryHostId,route,recordId
|
||||
FROM normalized ${where} ORDER BY julianday(at) DESC,id DESC LIMIT @limit OFFSET @offset`)
|
||||
const rows = handle
|
||||
.prepare(
|
||||
`${activitySql} SELECT id,at,type,sources,state,rawState,title,detail,server,serverDeletedAt,hostId,hostName,secondaryHostId,route,recordId
|
||||
FROM normalized ${where} ORDER BY julianday(at) DESC,id DESC LIMIT @limit OFFSET @offset`
|
||||
)
|
||||
.all({ ...bindings, limit, offset }) as SqlRow[];
|
||||
return { rows: rows.map(present), total, limit, offset, query: { ...query, offset } };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user