mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
import { databaseLogger } from "../../utils/logger.js";
|
|
|
|
/**
|
|
* Indexes for the columns the app filters, joins and sorts on.
|
|
*
|
|
* Most tables here are scoped per user or per host and were previously reached
|
|
* with a full table scan on every read: the foreign keys existed, but SQLite
|
|
* does not index the child side of a foreign key on its own. That is fine for a
|
|
* home install with a handful of hosts and invisible in testing; on an install
|
|
* with thousands of hosts and a large audit trail it is the dominant cost of a
|
|
* request.
|
|
*
|
|
* Ordering within a composite matters: the equality column comes first and the
|
|
* range/sort column second, so the same index serves both the filter and the
|
|
* ORDER BY.
|
|
*/
|
|
export interface PerformanceIndex {
|
|
name: string;
|
|
table: string;
|
|
columns: string;
|
|
/** Enforces a constraint as well as serving reads, e.g. one row per key. */
|
|
unique?: boolean;
|
|
}
|
|
|
|
export const PERFORMANCE_INDEXES: PerformanceIndex[] = [
|
|
// Host list: the single hottest read in the app.
|
|
{ name: "idx_ssh_data_user_id", table: "ssh_data", columns: "user_id" },
|
|
{
|
|
name: "idx_ssh_data_parent_host",
|
|
table: "ssh_data",
|
|
columns: "parent_host_id",
|
|
},
|
|
{
|
|
name: "idx_ssh_data_credential",
|
|
table: "ssh_data",
|
|
columns: "credential_id",
|
|
},
|
|
|
|
// Sharing: resolved for every host list request and every permission check.
|
|
{ name: "idx_host_access_user_id", table: "host_access", columns: "user_id" },
|
|
{ name: "idx_host_access_role_id", table: "host_access", columns: "role_id" },
|
|
{ name: "idx_host_access_host_id", table: "host_access", columns: "host_id" },
|
|
{
|
|
name: "idx_host_access_expires_at",
|
|
table: "host_access",
|
|
columns: "expires_at",
|
|
},
|
|
|
|
// Audit log: grows without bound and is always read newest-first.
|
|
{
|
|
name: "idx_audit_logs_timestamp",
|
|
table: "audit_logs",
|
|
columns: "timestamp",
|
|
},
|
|
{
|
|
name: "idx_audit_logs_user_ts",
|
|
table: "audit_logs",
|
|
columns: "user_id, timestamp",
|
|
},
|
|
{
|
|
name: "idx_audit_logs_action_ts",
|
|
table: "audit_logs",
|
|
columns: "action, timestamp",
|
|
},
|
|
{
|
|
name: "idx_audit_logs_resource_ts",
|
|
table: "audit_logs",
|
|
columns: "resource_type, timestamp",
|
|
},
|
|
|
|
// Auth hot path: touched on every authenticated request.
|
|
{ name: "idx_sessions_user_id", table: "sessions", columns: "user_id" },
|
|
{ name: "idx_sessions_expires_at", table: "sessions", columns: "expires_at" },
|
|
{ name: "idx_user_roles_user_id", table: "user_roles", columns: "user_id" },
|
|
{ name: "idx_user_roles_role_id", table: "user_roles", columns: "role_id" },
|
|
{
|
|
name: "idx_trusted_devices_user_id",
|
|
table: "trusted_devices",
|
|
columns: "user_id",
|
|
},
|
|
{ name: "idx_api_keys_user_id", table: "api_keys", columns: "user_id" },
|
|
|
|
// Credentials and folders.
|
|
{
|
|
name: "idx_ssh_credentials_user_id",
|
|
table: "ssh_credentials",
|
|
columns: "user_id",
|
|
},
|
|
{ name: "idx_ssh_folders_user_id", table: "ssh_folders", columns: "user_id" },
|
|
{
|
|
name: "idx_ssh_credential_usage_credential",
|
|
table: "ssh_credential_usage",
|
|
columns: "credential_id",
|
|
},
|
|
{
|
|
name: "idx_ssh_credential_usage_user",
|
|
table: "ssh_credential_usage",
|
|
columns: "user_id",
|
|
},
|
|
|
|
// Per-user open tabs.
|
|
{
|
|
name: "idx_user_open_tabs_user_id",
|
|
table: "user_open_tabs",
|
|
columns: "user_id",
|
|
},
|
|
];
|
|
|
|
interface IndexableDatabase {
|
|
exec(sql: string): unknown;
|
|
}
|
|
|
|
export interface IndexCreationSummary {
|
|
created: number;
|
|
skipped: number;
|
|
failed: number;
|
|
}
|
|
|
|
/**
|
|
* Creates any missing index, leaving existing ones untouched.
|
|
*
|
|
* A failure here is logged and skipped rather than thrown: an index is a pure
|
|
* optimisation, and a table that a given install has not created yet (or a
|
|
* column an older schema is missing) must not stop the server from booting.
|
|
*/
|
|
export function createPerformanceIndexes(
|
|
db: IndexableDatabase,
|
|
indexes: PerformanceIndex[] = PERFORMANCE_INDEXES,
|
|
): IndexCreationSummary {
|
|
const summary: IndexCreationSummary = { created: 0, skipped: 0, failed: 0 };
|
|
const startedAt = Date.now();
|
|
|
|
for (const index of indexes) {
|
|
try {
|
|
db.exec(
|
|
`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${index.name} ON ${index.table}(${index.columns})`,
|
|
);
|
|
summary.created++;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
// A missing table or column means this install does not have that
|
|
// feature's schema; nothing to index and nothing to warn loudly about.
|
|
if (/no such table|no such column/i.test(message)) {
|
|
summary.skipped++;
|
|
continue;
|
|
}
|
|
summary.failed++;
|
|
databaseLogger.warn(`Could not create index ${index.name}: ${message}`, {
|
|
operation: "performance_index_create",
|
|
index: index.name,
|
|
table: index.table,
|
|
});
|
|
}
|
|
}
|
|
|
|
databaseLogger.info(
|
|
`Performance indexes ready in ${Date.now() - startedAt}ms`,
|
|
{
|
|
operation: "performance_index_create",
|
|
...summary,
|
|
},
|
|
);
|
|
|
|
return summary;
|
|
}
|