Compare commits

..

10 Commits

Author SHA1 Message Date
Owen Schwartz 2957d6592d Merge pull request #2919 from fosrl/dev
Add missing imports
2026-04-28 16:52:53 -07:00
Owen de2a22aad8 Add missing imports 2026-04-28 16:52:35 -07:00
Owen Schwartz b96db4f133 Merge pull request #2918 from fosrl/dev
Use logsDb for the status history
2026-04-28 16:38:43 -07:00
Owen 2a29062659 Use logsDb for the status history 2026-04-28 16:38:13 -07:00
Owen Schwartz 98406f63af Merge pull request #2916 from fosrl/dev
1.18.0
2026-04-28 15:53:31 -07:00
Owen 85415176ab Clean imports 2026-04-28 15:41:00 -07:00
Owen b81ae3d998 Seed satatus data for resources, sites, and hc 2026-04-28 15:15:09 -07:00
Owen 208289f498 Select all networks to prevent delete issues 2026-04-28 12:02:21 -07:00
Owen 8783c47a3c Dont allow clicking the wildcard resource link 2026-04-28 11:21:32 -07:00
Owen 592ca64253 Fix delete 2026-04-28 09:57:46 -07:00
11 changed files with 289 additions and 97 deletions
+33 -28
View File
@@ -180,36 +180,41 @@ export async function rebuildClientAssociationsFromSiteResource(
/////////// process the client-siteResource associations /////////// /////////// process the client-siteResource associations ///////////
// get all of the clients associated with other resources in the same network, // get all of the clients associated with other site resources that share
// joined through siteNetworks so we know which siteId each client belongs to // any of the same sites as this site resource (via siteNetworks). We can't
const allUpdatedClientsFromOtherResourcesOnThisSite = siteResource.networkId // simply filter by networkId since each site resource has its own network;
? await trx // two site resources serving the same site typically belong to different
.select({ // networks that both happen to include the site through siteNetworks.
clientId: clientSiteResourcesAssociationsCache.clientId, const sitesListSiteIds = sitesList.map((s) => s.siteId);
siteId: siteNetworks.siteId const allUpdatedClientsFromOtherResourcesOnThisSite =
}) sitesListSiteIds.length > 0
.from(clientSiteResourcesAssociationsCache) ? await trx
.innerJoin( .select({
siteResources, clientId: clientSiteResourcesAssociationsCache.clientId,
eq( siteId: siteNetworks.siteId
clientSiteResourcesAssociationsCache.siteResourceId, })
siteResources.siteResourceId .from(clientSiteResourcesAssociationsCache)
) .innerJoin(
) siteResources,
.innerJoin( eq(
siteNetworks, clientSiteResourcesAssociationsCache.siteResourceId,
eq(siteNetworks.networkId, siteResources.networkId) siteResources.siteResourceId
)
.where(
and(
eq(siteResources.networkId, siteResource.networkId),
ne(
siteResources.siteResourceId,
siteResource.siteResourceId
) )
) )
) .innerJoin(
: []; siteNetworks,
eq(siteNetworks.networkId, siteResources.networkId)
)
.where(
and(
inArray(siteNetworks.siteId, sitesListSiteIds),
ne(
siteResources.siteResourceId,
siteResource.siteResourceId
)
)
)
: [];
// Build a per-site map so the loop below can check by siteId rather than // Build a per-site map so the loop below can check by siteId rather than
// across the entire network. // across the entire network.
+21 -15
View File
@@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { db, statusHistory } from "@server/db"; import { db, logsDb, statusHistory } from "@server/db";
import { and, eq, gte, asc } from "drizzle-orm"; import { and, eq, gte, asc } from "drizzle-orm";
import cache from "@server/lib/cache"; import cache from "@server/lib/cache";
@@ -27,7 +27,7 @@ export async function getCachedStatusHistory(
const nowSec = Math.floor(Date.now() / 1000); const nowSec = Math.floor(Date.now() / 1000);
const startSec = nowSec - days * 86400; const startSec = nowSec - days * 86400;
const events = await db const events = await logsDb
.select() .select()
.from(statusHistory) .from(statusHistory)
.where( .where(
@@ -74,11 +74,11 @@ export const statusHistoryQuerySchema = z
days: z days: z
.string() .string()
.optional() .optional()
.transform((v) => (v ? parseInt(v, 10) : 90)), .transform((v) => (v ? parseInt(v, 10) : 90))
}) })
.pipe( .pipe(
z.object({ z.object({
days: z.number().int().min(1).max(365), days: z.number().int().min(1).max(365)
}) })
); );
@@ -99,7 +99,14 @@ export interface StatusHistoryResponse {
} }
export function computeBuckets( export function computeBuckets(
events: { entityType: string; entityId: number; orgId: string; status: string; timestamp: number; id: number }[], events: {
entityType: string;
entityId: number;
orgId: string;
status: string;
timestamp: number;
id: number;
}[],
days: number days: number
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } { ): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
const nowSec = Math.floor(Date.now() / 1000); const nowSec = Math.floor(Date.now() / 1000);
@@ -121,7 +128,8 @@ export function computeBuckets(
const currentStatus = lastBeforeDay?.status ?? null; const currentStatus = lastBeforeDay?.status ?? null;
const windows: { start: number; end: number | null; status: string }[] = []; const windows: { start: number; end: number | null; status: string }[] =
[];
let dayDowntime = 0; let dayDowntime = 0;
let dayDegradedTime = 0; let dayDegradedTime = 0;
@@ -132,22 +140,21 @@ export function computeBuckets(
if (windowStatus !== null && windowStatus !== evt.status) { if (windowStatus !== null && windowStatus !== evt.status) {
const windowEnd = evt.timestamp; const windowEnd = evt.timestamp;
const isDown = const isDown =
windowStatus === "offline" || windowStatus === "offline" || windowStatus === "unhealthy";
windowStatus === "unhealthy";
const isDegraded = windowStatus === "degraded"; const isDegraded = windowStatus === "degraded";
if (isDown) { if (isDown) {
dayDowntime += windowEnd - windowStart; dayDowntime += windowEnd - windowStart;
windows.push({ windows.push({
start: windowStart, start: windowStart,
end: windowEnd, end: windowEnd,
status: windowStatus, status: windowStatus
}); });
} else if (isDegraded) { } else if (isDegraded) {
dayDegradedTime += windowEnd - windowStart; dayDegradedTime += windowEnd - windowStart;
windows.push({ windows.push({
start: windowStart, start: windowStart,
end: windowEnd, end: windowEnd,
status: windowStatus, status: windowStatus
}); });
} }
} }
@@ -159,22 +166,21 @@ export function computeBuckets(
if (windowStatus !== null) { if (windowStatus !== null) {
const finalEnd = Math.min(dayEndSec, nowSec); const finalEnd = Math.min(dayEndSec, nowSec);
const isDown = const isDown =
windowStatus === "offline" || windowStatus === "offline" || windowStatus === "unhealthy";
windowStatus === "unhealthy";
const isDegraded = windowStatus === "degraded"; const isDegraded = windowStatus === "degraded";
if (isDown && finalEnd > windowStart) { if (isDown && finalEnd > windowStart) {
dayDowntime += finalEnd - windowStart; dayDowntime += finalEnd - windowStart;
windows.push({ windows.push({
start: windowStart, start: windowStart,
end: finalEnd, end: finalEnd,
status: windowStatus, status: windowStatus
}); });
} else if (isDegraded && finalEnd > windowStart) { } else if (isDegraded && finalEnd > windowStart) {
dayDegradedTime += finalEnd - windowStart; dayDegradedTime += finalEnd - windowStart;
windows.push({ windows.push({
start: windowStart, start: windowStart,
end: finalEnd, end: finalEnd,
status: windowStatus, status: windowStatus
}); });
} }
} }
@@ -225,7 +231,7 @@ export function computeBuckets(
uptimePercent: Math.round(uptimePct * 100) / 100, uptimePercent: Math.round(uptimePct * 100) / 100,
totalDowntimeSeconds: dayDowntime, totalDowntimeSeconds: dayDowntime,
downtimeWindows: windows, downtimeWindows: windows,
status, status
}); });
} }
@@ -19,7 +19,8 @@ import {
targetHealthCheck, targetHealthCheck,
targets, targets,
resources, resources,
Transaction Transaction,
logsDb
} from "@server/db"; } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory"; import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
@@ -52,10 +53,10 @@ export async function fireHealthCheckHealthyAlert(
healthCheckTargetId?: number | null, healthCheckTargetId?: number | null,
extra?: Record<string, unknown>, extra?: Record<string, unknown>,
send: boolean = true, send: boolean = true,
trx: Transaction | typeof db = db, trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "health_check", entityType: "health_check",
entityId: healthCheckId, entityId: healthCheckId,
orgId: orgId, orgId: orgId,
@@ -119,7 +120,7 @@ export async function fireHealthCheckUnhealthyAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "health_check", entityType: "health_check",
entityId: healthCheckId, entityId: healthCheckId,
orgId: orgId, orgId: orgId,
@@ -172,7 +173,7 @@ export async function fireHealthCheckUnknownAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "health_check", entityType: "health_check",
entityId: healthCheckId, entityId: healthCheckId,
orgId: orgId, orgId: orgId,
@@ -194,7 +195,12 @@ export async function fireHealthCheckUnknownAlert(
} }
} }
async function handleResource(orgId: string, healthCheckTargetId?: number | null, send: boolean = true, trx: Transaction | typeof db = db) { async function handleResource(
orgId: string,
healthCheckTargetId?: number | null,
send: boolean = true,
trx: Transaction | typeof db = db
) {
if (!healthCheckTargetId) { if (!healthCheckTargetId) {
return; return;
} }
@@ -222,7 +228,10 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
const otherTargets = await trx const otherTargets = await trx
.select({ hcHealth: targetHealthCheck.hcHealth }) .select({ hcHealth: targetHealthCheck.hcHealth })
.from(targets) .from(targets)
.innerJoin(targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId)) .innerJoin(
targetHealthCheck,
eq(targetHealthCheck.targetId, targets.targetId)
)
.where(eq(targets.resourceId, resource.resourceId)); .where(eq(targets.resourceId, resource.resourceId));
let health = "healthy"; let health = "healthy";
@@ -13,7 +13,7 @@
import logger from "@server/logger"; import logger from "@server/logger";
import { processAlerts } from "../processAlerts"; import { processAlerts } from "../processAlerts";
import { db, statusHistory, Transaction } from "@server/db"; import { db, logsDb, statusHistory, Transaction } from "@server/db";
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory"; import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -40,7 +40,7 @@ export async function fireResourceHealthyAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "resource", entityType: "resource",
entityId: resourceId, entityId: resourceId,
orgId: orgId, orgId: orgId,
@@ -101,7 +101,7 @@ export async function fireResourceUnhealthyAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "resource", entityType: "resource",
entityId: resourceId, entityId: resourceId,
orgId: orgId, orgId: orgId,
@@ -162,7 +162,7 @@ export async function fireResourceDegradedAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "resource", entityType: "resource",
entityId: resourceId, entityId: resourceId,
orgId: orgId, orgId: orgId,
@@ -223,7 +223,7 @@ export async function fireResourceUnknownAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "resource", entityType: "resource",
entityId: resourceId, entityId: resourceId,
orgId: orgId, orgId: orgId,
@@ -13,7 +13,13 @@
import logger from "@server/logger"; import logger from "@server/logger";
import { processAlerts } from "../processAlerts"; import { processAlerts } from "../processAlerts";
import { db, sites, statusHistory, targetHealthCheck, Transaction } from "@server/db"; import {
db,
logsDb,
statusHistory,
targetHealthCheck,
Transaction
} from "@server/db";
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory"; import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { fireHealthCheckUnhealthyAlert } from "./healthCheckEvents"; import { fireHealthCheckUnhealthyAlert } from "./healthCheckEvents";
@@ -41,7 +47,7 @@ export async function fireSiteOnlineAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "site", entityType: "site",
entityId: siteId, entityId: siteId,
orgId: orgId, orgId: orgId,
@@ -97,7 +103,7 @@ export async function fireSiteOfflineAlert(
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<void> {
try { try {
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "site", entityType: "site",
entityId: siteId, entityId: siteId,
orgId: orgId, orgId: orgId,
+16 -8
View File
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, statusHistory } from "@server/db"; import { db, logsDb, statusHistory } from "@server/db";
import { import {
siteProvisioningKeys, siteProvisioningKeys,
siteProvisioningKeyOrg, siteProvisioningKeyOrg,
@@ -84,7 +84,7 @@ export async function registerNewt(
maxBatchSize: siteProvisioningKeys.maxBatchSize, maxBatchSize: siteProvisioningKeys.maxBatchSize,
numUsed: siteProvisioningKeys.numUsed, numUsed: siteProvisioningKeys.numUsed,
validUntil: siteProvisioningKeys.validUntil, validUntil: siteProvisioningKeys.validUntil,
approveNewSites: siteProvisioningKeys.approveNewSites, approveNewSites: siteProvisioningKeys.approveNewSites
}) })
.from(siteProvisioningKeys) .from(siteProvisioningKeys)
.innerJoin( .innerJoin(
@@ -125,7 +125,10 @@ export async function registerNewt(
); );
} }
if (keyRecord.maxBatchSize && keyRecord.numUsed >= keyRecord.maxBatchSize) { if (
keyRecord.maxBatchSize &&
keyRecord.numUsed >= keyRecord.maxBatchSize
) {
return next( return next(
createHttpError( createHttpError(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,
@@ -134,7 +137,10 @@ export async function registerNewt(
); );
} }
if (keyRecord.validUntil && new Date(keyRecord.validUntil) < new Date()) { if (
keyRecord.validUntil &&
new Date(keyRecord.validUntil) < new Date()
) {
return next( return next(
createHttpError( createHttpError(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,
@@ -154,7 +160,10 @@ export async function registerNewt(
} }
if (!org.subnet) { if (!org.subnet) {
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Organization subnet not found") createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Organization subnet not found"
)
); );
} }
@@ -195,7 +204,6 @@ export async function registerNewt(
let newSiteId: number | undefined; let newSiteId: number | undefined;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const newClientAddress = await getNextAvailableClientSubnet(orgId); const newClientAddress = await getNextAvailableClientSubnet(orgId);
if (!newClientAddress) { if (!newClientAddress) {
return next( return next(
@@ -219,11 +227,11 @@ export async function registerNewt(
address: clientAddress, address: clientAddress,
type: "newt", type: "newt",
dockerSocketEnabled: true, dockerSocketEnabled: true,
status: keyRecord.approveNewSites ? "approved" : "pending", status: keyRecord.approveNewSites ? "approved" : "pending"
}) })
.returning(); .returning();
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "site", entityType: "site",
entityId: newSite.siteId, entityId: newSite.siteId,
orgId: orgId, orgId: orgId,
+2 -2
View File
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { clients, db, exitNodes, statusHistory } from "@server/db"; import { clients, db, exitNodes, logsDb, statusHistory } from "@server/db";
import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db"; import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -351,7 +351,7 @@ export async function createSite(
}) })
.returning(); .returning();
await trx.insert(statusHistory).values({ await logsDb.insert(statusHistory).values({
entityType: "site", entityType: "site",
entityId: newSite.siteId, entityId: newSite.siteId,
orgId: orgId, orgId: orgId,
@@ -67,22 +67,9 @@ export async function deleteSiteResource(
// Delete the site resource // Delete the site resource
const [removedSiteResource] = await trx const [removedSiteResource] = await trx
.delete(siteResources) .delete(siteResources)
.where(and(eq(siteResources.siteResourceId, siteResourceId))) .where(eq(siteResources.siteResourceId, siteResourceId))
.returning(); .returning();
// not sure why this is here...
// const [newt] = await trx
// .select()
// .from(newts)
// .where(eq(newts.siteId, removedSiteResource.siteId))
// .limit(1);
// if (!newt) {
// return next(
// createHttpError(HttpCode.NOT_FOUND, "Newt not found")
// );
// }
await rebuildClientAssociationsFromSiteResource( await rebuildClientAssociationsFromSiteResource(
removedSiteResource, removedSiteResource,
trx trx
+92 -9
View File
@@ -67,11 +67,12 @@ export default async function migration() {
FROM "siteResources" sr FROM "siteResources" sr
WHERE sr."siteId" IS NOT NULL` WHERE sr."siteId" IS NOT NULL`
); );
const existingSiteResourcesForNetwork = siteResourcesForNetworkQuery.rows as { const existingSiteResourcesForNetwork =
siteResourceId: number; siteResourcesForNetworkQuery.rows as {
orgId: string; siteResourceId: number;
siteId: number; orgId: string;
}[]; siteId: number;
}[];
console.log( console.log(
`Found ${existingSiteResourcesForNetwork.length} existing siteResource(s) to migrate to networks` `Found ${existingSiteResourcesForNetwork.length} existing siteResource(s) to migrate to networks`
@@ -446,10 +447,7 @@ export default async function migration() {
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs` `Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
); );
} catch (e) { } catch (e) {
console.error( console.error("Error while migrating targetHealthCheck rows:", e);
"Error while migrating targetHealthCheck rows:",
e
);
throw e; throw e;
} }
} }
@@ -493,5 +491,90 @@ export default async function migration() {
} }
} }
// Seed statusHistory for all existing sites
try {
const sitesQuery = await db.execute(
sql`SELECT "siteId", "orgId", "online" FROM "sites"`
);
const allSites = sitesQuery.rows as {
siteId: number;
orgId: string;
online: boolean;
}[];
const now = Math.floor(Date.now() / 1000);
for (const site of allSites) {
await db.execute(sql`
INSERT INTO "statusHistory" ("entityType", "entityId", "orgId", "status", "timestamp")
VALUES ('site', ${site.siteId}, ${site.orgId}, ${site.online ? "online" : "offline"}, ${now})
`);
}
console.log(`Seeded statusHistory for ${allSites.length} site(s)`);
} catch (e) {
console.error("Error while seeding statusHistory for sites:", e);
throw e;
}
// Seed statusHistory for all existing resources
try {
const resourcesQuery = await db.execute(
sql`SELECT "resourceId", "orgId", "health" FROM "resources"`
);
const allResources = resourcesQuery.rows as {
resourceId: number;
orgId: string;
health: string | null;
}[];
const now = Math.floor(Date.now() / 1000);
for (const resource of allResources) {
await db.execute(sql`
INSERT INTO "statusHistory" ("entityType", "entityId", "orgId", "status", "timestamp")
VALUES ('resource', ${resource.resourceId}, ${resource.orgId}, ${resource.health ?? "unknown"}, ${now})
`);
}
console.log(
`Seeded statusHistory for ${allResources.length} resource(s)`
);
} catch (e) {
console.error("Error while seeding statusHistory for resources:", e);
throw e;
}
// Seed statusHistory for all existing health checks
try {
const healthChecksQuery = await db.execute(
sql`SELECT "targetHealthCheckId", "orgId", "hcHealth" FROM "targetHealthCheck"`
);
const allHealthChecks = healthChecksQuery.rows as {
targetHealthCheckId: number;
orgId: string;
hcHealth: string | null;
}[];
const now = Math.floor(Date.now() / 1000);
for (const hc of allHealthChecks) {
await db.execute(sql`
INSERT INTO "statusHistory" ("entityType", "entityId", "orgId", "status", "timestamp")
VALUES ('health_check', ${hc.targetHealthCheckId}, ${hc.orgId}, ${hc.hcHealth ?? "unknown"}, ${now})
`);
}
console.log(
`Seeded statusHistory for ${allHealthChecks.length} health check(s)`
);
} catch (e) {
console.error(
"Error while seeding statusHistory for health checks:",
e
);
throw e;
}
console.log(`${version} migration complete`); console.log(`${version} migration complete`);
} }
+86 -2
View File
@@ -340,7 +340,6 @@ export default async function migration() {
ALTER TABLE 'resources' ADD 'wildcard' integer DEFAULT false NOT NULL; ALTER TABLE 'resources' ADD 'wildcard' integer DEFAULT false NOT NULL;
` `
).run(); ).run();
})(); })();
db.pragma("foreign_keys = ON"); db.pragma("foreign_keys = ON");
@@ -364,7 +363,11 @@ export default async function migration() {
const result = insertNetwork.run("resource", sr.orgId); const result = insertNetwork.run("resource", sr.orgId);
const networkId = result.lastInsertRowid as number; const networkId = result.lastInsertRowid as number;
insertSiteNetwork.run(sr.siteId, networkId); insertSiteNetwork.run(sr.siteId, networkId);
updateSiteResource.run(networkId, networkId, sr.siteResourceId); updateSiteResource.run(
networkId,
networkId,
sr.siteResourceId
);
} }
}); });
@@ -454,6 +457,87 @@ export default async function migration() {
} }
console.log(`Migrated database`); console.log(`Migrated database`);
// Seed statusHistory for all existing sites
const allSites = db
.prepare(`SELECT "siteId", "orgId", "online" FROM 'sites'`)
.all() as { siteId: number; orgId: string; online: number }[];
const insertSiteHistory = db.prepare(
`INSERT INTO 'statusHistory' ("entityType", "entityId", "orgId", "status", "timestamp") VALUES (?, ?, ?, ?, ?)`
);
const now = Math.floor(Date.now() / 1000);
const seedSites = db.transaction(() => {
for (const site of allSites) {
insertSiteHistory.run(
"site",
site.siteId,
site.orgId,
site.online ? "online" : "offline",
now
);
}
});
seedSites();
console.log(`Seeded statusHistory for ${allSites.length} site(s)`);
// Seed statusHistory for all existing resources
const allResources = db
.prepare(`SELECT "resourceId", "orgId", "health" FROM 'resources'`)
.all() as {
resourceId: number;
orgId: string;
health: string | null;
}[];
const insertResourceHistory = db.prepare(
`INSERT INTO 'statusHistory' ("entityType", "entityId", "orgId", "status", "timestamp") VALUES (?, ?, ?, ?, ?)`
);
const seedResources = db.transaction(() => {
for (const resource of allResources) {
insertResourceHistory.run(
"resource",
resource.resourceId,
resource.orgId,
resource.health ?? "unknown",
now
);
}
});
seedResources();
console.log(
`Seeded statusHistory for ${allResources.length} resource(s)`
);
// Seed statusHistory for all existing health checks
const allHealthChecks = db
.prepare(
`SELECT "targetHealthCheckId", "orgId", "hcHealth" FROM 'targetHealthCheck'`
)
.all() as {
targetHealthCheckId: number;
orgId: string;
hcHealth: string | null;
}[];
const insertHealthCheckHistory = db.prepare(
`INSERT INTO 'statusHistory' ("entityType", "entityId", "orgId", "status", "timestamp") VALUES (?, ?, ?, ?, ?)`
);
const seedHealthChecks = db.transaction(() => {
for (const hc of allHealthChecks) {
insertHealthCheckHistory.run(
"health_check",
hc.targetHealthCheckId,
hc.orgId,
hc.hcHealth ?? "unknown",
now
);
}
});
seedHealthChecks();
console.log(
`Seeded statusHistory for ${allHealthChecks.length} health check(s)`
);
} catch (e) { } catch (e) {
console.log("Failed to migrate db:", e); console.log("Failed to migrate db:", e);
throw e; throw e;
+8 -4
View File
@@ -40,10 +40,14 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSection> <InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle> <InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<CopyToClipboard {resource.wildcard ? (
text={fullUrl} <span>{fullUrl}</span>
isLink={true} ) : (
/> <CopyToClipboard
text={fullUrl}
isLink={true}
/>
)}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>