-
-
{{ client.name }}
-
{{ client.ipv4Address }}
+
+
{{ client.name }}
+
{{ client.ipv4Address }}
+
+
-
@@ -149,7 +179,7 @@
-
+
+
+
+
+
+
+ {{ $t('admin.speed.moveToClass') }}
+
+
+
+
+
+
+
@@ -224,6 +290,49 @@ const unassignedClients = computed(() => {
return data.value.clients.filter((c) => c.ipv4Address && !assignedIps.has(c.ipv4Address));
});
+// Class selector dropdown state
+const menuOpenFor = ref<{ client: TcClient; fromClassIdx: number | null } | null>(null);
+const menuPosition = ref({ x: 0, y: 0 });
+
+function toggleMenu(e: MouseEvent, client: TcClient, fromClassIdx: number | null) {
+ if (menuOpenFor.value?.client.ipv4Address === client.ipv4Address) {
+ menuOpenFor.value = null;
+ return;
+ }
+ menuOpenFor.value = { client, fromClassIdx };
+ // Position near the click, clamped to viewport
+ const x = Math.min(e.clientX, window.innerWidth - 180);
+ const y = Math.min(e.clientY, window.innerHeight - 300);
+ menuPosition.value = { x, y };
+}
+
+function closeMenu() {
+ menuOpenFor.value = null;
+}
+
+function moveClientViaMenu(toClassIdx: number | null) {
+ if (!menuOpenFor.value || !data.value) return;
+ const { client, fromClassIdx } = menuOpenFor.value;
+
+ // Remove from source class
+ if (fromClassIdx !== null) {
+ const srcClass = data.value.tcState.classes[fromClassIdx];
+ if (srcClass) {
+ srcClass.clientIps = srcClass.clientIps.filter((ip) => ip !== client.ipv4Address);
+ }
+ }
+
+ // Add to target class (null = unassigned)
+ if (toClassIdx !== null) {
+ const dstClass = data.value.tcState.classes[toClassIdx];
+ if (dstClass && client.ipv4Address && !dstClass.clientIps.includes(client.ipv4Address)) {
+ dstClass.clientIps.push(client.ipv4Address);
+ }
+ }
+
+ menuOpenFor.value = null;
+}
+
// Debounce timer for class UL input
const classUlTimers = new Map
>();
@@ -276,6 +385,18 @@ function onDragStart(client: TcClient, fromClassIdx: number | null) {
dragState.value = { client, fromClassIdx };
}
+function onDropUnassigned() {
+ if (!dragState.value || !data.value) return;
+ const { client, fromClassIdx } = dragState.value;
+ if (fromClassIdx !== null) {
+ const srcClass = data.value.tcState.classes[fromClassIdx];
+ if (srcClass) {
+ srcClass.clientIps = srcClass.clientIps.filter((ip) => ip !== client.ipv4Address);
+ }
+ }
+ dragState.value = null;
+}
+
function onDrop(toClassIdx: number) {
if (!dragState.value || !data.value) return;
const { client, fromClassIdx } = dragState.value;
diff --git a/src/server/database/repositories/tcState/schema.ts b/src/server/database/repositories/tcState/schema.ts
index 8ec1157c..daf54319 100644
--- a/src/server/database/repositories/tcState/schema.ts
+++ b/src/server/database/repositories/tcState/schema.ts
@@ -5,7 +5,7 @@ export const tcState = sqliteTable('tc_state_table', {
id: int().primaryKey({ autoIncrement: false }).default(1),
totalUlRate: int('total_ul_rate').notNull().default(100),
- defaultClassId: int('default_class_id').notNull().default(21), // 2{ulRate} convention: 21 means ulRate=1
+ defaultClassId: int('default_class_id').notNull().default(211), // 2{ulRate} convention: 211 means ulRate=11
/** JSON array of { id: number, ulRate: number, clientIps: string[] } */
classes: text({ mode: 'json' }).$type().notNull().default([]),
diff --git a/src/server/database/repositories/tcState/service.ts b/src/server/database/repositories/tcState/service.ts
index 298cc327..7bad9e7b 100644
--- a/src/server/database/repositories/tcState/service.ts
+++ b/src/server/database/repositories/tcState/service.ts
@@ -32,7 +32,7 @@ export class TcStateService {
await this.#db.insert(tcState).values({
id: 1,
totalUlRate: 100,
- defaultClassId: 21, // 2{ulRate} convention: 21 means ulRate=1
+ defaultClassId: 211, // 2{ulRate} convention: 211 means ulRate=11
classes: [],
}).execute();
state = await this.#statements.get.execute();