feat: convert tmux monitoring into a plugin (B10)

This commit is contained in:
LukeGus committed 2026-09-23 22:42:56 -05:00
1 parent 79d77678c9
commit 708eedb861
145 files changed
+30308 -4458

No files matched your search

-73
View File
@@ -1950,79 +1950,6 @@ const migrateSchema = () => {
}
}
// --- tmux-monitor begin ---
try {
sqlite.prepare("SELECT id FROM tmux_session_tags LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS tmux_session_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
session_name TEXT NOT NULL,
tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create tmux_session_tags table", {
operation: "schema_migration",
error: createError,
});
}
}
// Rebuild pre-release tables created without the host FK so deleting a
// host cascades to its tags (SQLite cannot add a FK via ALTER TABLE).
try {
const tagFks = sqlite
.prepare("PRAGMA foreign_key_list(tmux_session_tags)")
.all() as Array<{ table: string; from: string }>;
const hasHostFk = tagFks.some(
(fk) => fk.from === "host_id" && fk.table === "ssh_data",
);
if (!hasHostFk) {
sqlite.exec(`
BEGIN;
CREATE TABLE tmux_session_tags_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
session_name TEXT NOT NULL,
tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
INSERT INTO tmux_session_tags_new (id, user_id, host_id, session_name, tag, created_at)
SELECT id, user_id, host_id, session_name, tag, created_at
FROM tmux_session_tags
WHERE user_id IN (SELECT id FROM users)
AND host_id IN (SELECT id FROM ssh_data);
DROP TABLE tmux_session_tags;
ALTER TABLE tmux_session_tags_new RENAME TO tmux_session_tags;
COMMIT;
`);
databaseLogger.info("Rebuilt tmux_session_tags with host FK", {
operation: "schema_migration",
});
}
} catch (rebuildError) {
try {
sqlite.exec("ROLLBACK;");
} catch {
// no transaction open
}
databaseLogger.warn("Failed to add host FK to tmux_session_tags", {
operation: "schema_migration",
error: rebuildError,
});
}
// --- tmux-monitor end ---
try {
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
-17
View File
@@ -1204,23 +1204,6 @@ export const termixIdentityCa = mysqlTable("termix_identity_ca", {
});
// --- termix-id end ---
// --- tmux-monitor begin ---
export const tmuxSessionTags = mysqlTable("tmux_session_tags", {
id: int("id").autoincrement().primaryKey(),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
hostId: int("host_id")
.notNull()
.references(() => hosts.id, { onDelete: "cascade" }),
sessionName: text("session_name").notNull(),
tag: text("tag").notNull(),
createdAt: varchar("created_at", { length: 255 })
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
});
// --- tmux-monitor end ---
// --- metrics-history begin ---
export const hostMetricsHistory = mysqlTable("host_metrics_history", {
id: int("id").autoincrement().primaryKey(),
-17
View File
@@ -1205,23 +1205,6 @@ export const termixIdentityCa = pgTable("termix_identity_ca", {
});
// --- termix-id end ---
// --- tmux-monitor begin ---
export const tmuxSessionTags = pgTable("tmux_session_tags", {
id: serial("id").primaryKey(),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => hosts.id, { onDelete: "cascade" }),
sessionName: text("session_name").notNull(),
tag: text("tag").notNull(),
createdAt: varchar("created_at", { length: 255 })
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// --- tmux-monitor end ---
// --- metrics-history begin ---
export const hostMetricsHistory = pgTable("host_metrics_history", {
id: serial("id").primaryKey(),
-17
View File
@@ -1203,23 +1203,6 @@ export const termixIdentityCa = sqliteTable("termix_identity_ca", {
});
// --- termix-id end ---
// --- tmux-monitor begin ---
export const tmuxSessionTags = sqliteTable("tmux_session_tags", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => hosts.id, { onDelete: "cascade" }),
sessionName: text("session_name").notNull(),
tag: text("tag").notNull(),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// --- tmux-monitor end ---
// --- metrics-history begin ---
export const hostMetricsHistory = sqliteTable("host_metrics_history", {
id: integer("id").primaryKey({ autoIncrement: true }),
+1 -8
View File
@@ -53,7 +53,6 @@ import { SyncTombstoneRepository } from "./sync-tombstone-repository.js";
import { SsoProviderRepository } from "./sso-provider-repository.js";
import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js";
import { TermixIdentityRepository } from "./termix-identity-repository.js";
import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js";
import { TrustedDeviceRepository } from "./trusted-device-repository.js";
import { UserDataExportRepository } from "./user-data-export-repository.js";
import { UserPreferenceRepository } from "./user-preference-repository.js";
@@ -482,14 +481,8 @@ export function createCurrentTermixIdentityRepository(): TermixIdentityRepositor
);
}
export function createCurrentTmuxSessionTagRepository(): TmuxSessionTagRepository {
return new TmuxSessionTagRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("tmux_session_tag_repository_write"),
);
}
// transfer_recent moved to the file-manager plugin.
// tmux_session_tags moved to the tmux-monitor plugin.
export function createCurrentTrustedDeviceRepository(): TrustedDeviceRepository {
return new TrustedDeviceRepository(
@@ -1,136 +0,0 @@
import { and, eq } from "drizzle-orm";
import { tmuxSessionTags } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
export type TmuxSessionTagRecord = typeof tmuxSessionTags.$inferSelect;
export interface TmuxSessionTagInput {
userId: string;
hostId: number;
sessionName: string;
tag: string;
}
export class TmuxSessionTagRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async listByUserAndHost(
userId: string,
hostId: number,
): Promise<Map<string, string[]>> {
const rows = await this.context.drizzle
.select()
.from(tmuxSessionTags)
.where(
and(
eq(tmuxSessionTags.userId, userId),
eq(tmuxSessionTags.hostId, hostId),
),
);
const bySession = new Map<string, string[]>();
for (const row of rows) {
const tags = bySession.get(row.sessionName) ?? [];
tags.push(row.tag);
bySession.set(row.sessionName, tags);
}
return bySession;
}
async renameSessionForHost(
hostId: number,
sessionName: string,
newSessionName: string,
): Promise<number> {
const result = await this.context.drizzle
.update(tmuxSessionTags)
.set({ sessionName: newSessionName })
.where(
and(
eq(tmuxSessionTags.hostId, hostId),
eq(tmuxSessionTags.sessionName, sessionName),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async deleteSessionForHost(
hostId: number,
sessionName: string,
): Promise<number> {
const result = await this.context.drizzle
.delete(tmuxSessionTags)
.where(
and(
eq(tmuxSessionTags.hostId, hostId),
eq(tmuxSessionTags.sessionName, sessionName),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async replaceForUserHostSession(
userId: string,
hostId: number,
sessionName: string,
tags: string[],
): Promise<number> {
const result = await this.context.drizzle
.delete(tmuxSessionTags)
.where(
and(
eq(tmuxSessionTags.userId, userId),
eq(tmuxSessionTags.hostId, hostId),
eq(tmuxSessionTags.sessionName, sessionName),
),
);
if (tags.length > 0) {
await this.context.drizzle.insert(tmuxSessionTags).values(
tags.map((tag) => ({
userId,
hostId,
sessionName,
tag,
})),
);
}
const changedRows = rowsAffected(result) + tags.length;
if (changedRows > 0) {
await this.afterWrite();
}
return changedRows;
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(tmuxSessionTags)
.where(eq(tmuxSessionTags.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
@@ -28,7 +28,6 @@ import {
createCurrentSshCredentialUsageRepository,
createCurrentTermixIdentityCaRepository,
createCurrentTermixIdentityRepository,
createCurrentTmuxSessionTagRepository,
createCurrentTrustedDeviceRepository,
createCurrentUserPreferenceRepository,
createCurrentUserRepository,
@@ -112,7 +111,6 @@ export async function deleteUserAndRelatedData(
await createCurrentSecretSourceRepository().deleteByUserId(userId);
await createCurrentTermixIdentityCaRepository().deleteByUserId(userId);
await createCurrentTermixIdentityRepository().deleteByUserId(userId);
await createCurrentTmuxSessionTagRepository().deleteByUserId(userId);
await createCurrentOpenTabRepository().deleteByUserId(userId);
await createCurrentUserPreferenceRepository().deleteByUserId(userId);