feat: instance-level streaming limits with admin settings panel

Add a server-side instance_settings table (single-row, CHECK(id=1))
that stores admin-configurable streaming bounds: bitrate min/max/step,
allowed resolutions, and allowed framerates.

Backend:
- New instance_settings schema + migrations (is_admin on users, default
  settings row, first-registered-user promoted to admin)
- GET/PATCH /api/settings/streaming endpoints with admin-only writes
  and full input validation including cross-field checks

Frontend:
- settingsStore fetches limits on WebSocket ready, exposes isAdmin flag
- ScreenShareSettingsPopover reads bounds from store instead of
  hardcoded constants, auto-clamps stale localStorage values
- buildScreenShareOptions() clamps bitrate to server limits at build
  time as enforcement backstop
- ServerSettings modal gains a "Streaming" tab (admin-only) with
  bitrate range sliders, resolution/framerate toggles, and save/reset
This commit is contained in:
Jannis Braun
2026-02-26 03:33:29 +01:00
parent 2184ded2c1
commit 773a03b1aa
22 changed files with 541 additions and 9 deletions
+12
View File
@@ -174,6 +174,18 @@ function createTables(db: Database.Database): void {
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
PRIMARY KEY (folder_id, server_id)
);
CREATE TABLE IF NOT EXISTS instance_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
max_bitrate_kbps INTEGER NOT NULL DEFAULT 20000,
min_bitrate_kbps INTEGER NOT NULL DEFAULT 500,
bitrate_step_kbps INTEGER NOT NULL DEFAULT 500,
allowed_resolutions TEXT NOT NULL DEFAULT '540,720,1080',
allowed_framerates TEXT NOT NULL DEFAULT '30,45,60',
max_resolution INTEGER NOT NULL DEFAULT 1080,
max_framerate INTEGER NOT NULL DEFAULT 60,
updated_at INTEGER NOT NULL DEFAULT 0
);
`);
}
+35
View File
@@ -48,6 +48,12 @@ export function runMigrations(db: Database.Database): void {
columns: [
{ name: 'owner_id', type: 'TEXT' }
]
},
{
name: 'users',
columns: [
{ name: 'is_admin', type: 'INTEGER DEFAULT 0' }
]
}
];
@@ -82,9 +88,38 @@ export function runMigrations(db: Database.Database): void {
// ─── RBAC Migration: Ensure @everyone roles exist for all servers ─────────
migrateEveryoneRoles(db);
// ─── Instance settings: ensure default row exists ──────────────────────────
migrateInstanceSettings(db);
// ─── Admin flag: ensure at least one admin exists (first registered user) ──
migrateFirstAdmin(db);
console.log('Migrations complete.');
}
/** Ensure the single-row instance_settings row exists */
function migrateInstanceSettings(db: Database.Database): void {
const row = db.prepare('SELECT id FROM instance_settings WHERE id = 1').get();
if (!row) {
db.prepare(
'INSERT OR IGNORE INTO instance_settings (id, max_bitrate_kbps, min_bitrate_kbps, bitrate_step_kbps, allowed_resolutions, allowed_framerates, max_resolution, max_framerate, updated_at) VALUES (1, 20000, 500, 500, ?, ?, 1080, 60, ?)'
).run('540,720,1080', '30,45,60', Date.now());
console.log('Migrating: Inserted default instance_settings row');
}
}
/** Ensure at least one user has is_admin = 1 (the earliest registered user) */
function migrateFirstAdmin(db: Database.Database): void {
const anyAdmin = db.prepare('SELECT id FROM users WHERE is_admin = 1 LIMIT 1').get();
if (!anyAdmin) {
const firstUser = db.prepare('SELECT id FROM users ORDER BY created_at ASC LIMIT 1').get() as { id: string } | undefined;
if (firstUser) {
db.prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(firstUser.id);
console.log(`Migrating: Set first user ${firstUser.id} as instance admin`);
}
}
}
/** For each server, ensure an @everyone role exists with id === server.id */
function migrateEveryoneRoles(db: Database.Database): void {
const servers = db.prepare('SELECT id FROM servers').all() as { id: string }[];
+13
View File
@@ -8,6 +8,7 @@ export const users = sqliteTable('users', {
avatar: text('avatar'),
status: text('status').default('offline'),
customStatus: text('custom_status'),
isAdmin: integer('is_admin').default(0),
createdAt: integer('created_at').notNull(),
});
@@ -173,3 +174,15 @@ export const serverFolderMembers = sqliteTable('server_folder_members', {
}, (table) => ({
pk: primaryKey({ columns: [table.folderId, table.serverId] }),
}));
export const instanceSettings = sqliteTable('instance_settings', {
id: integer('id').primaryKey().default(1),
maxBitrateKbps: integer('max_bitrate_kbps').notNull().default(20000),
minBitrateKbps: integer('min_bitrate_kbps').notNull().default(500),
bitrateStepKbps: integer('bitrate_step_kbps').notNull().default(500),
allowedResolutions: text('allowed_resolutions').notNull().default('540,720,1080'),
allowedFramerates: text('allowed_framerates').notNull().default('30,45,60'),
maxResolution: integer('max_resolution').notNull().default(1080),
maxFramerate: integer('max_framerate').notNull().default(60),
updatedAt: integer('updated_at').notNull(),
});
+1
View File
@@ -24,6 +24,7 @@ export async function seedDatabase(): Promise<void> {
displayName: 'Admin',
passwordHash: adminPasswordHash,
status: 'offline',
isAdmin: 1,
createdAt: Date.now(),
}).run();