From 5980a5385e6432bd7f4b9bace451a88075c8bf9a Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 11 Oct 2026 14:59:59 +0700 Subject: [PATCH] up --- proto/main.proto | 71 ++++++++++++------- .../crabs/evening_detective_server/index.ts | 16 +++++ src/components/GamePage.vue | 8 ++- src/components/GameStatePanel.vue | 32 ++++++++- src/composables/__tests__/gameState.test.ts | 22 +++++- src/composables/useGameState.ts | 7 +- 6 files changed, 123 insertions(+), 33 deletions(-) diff --git a/proto/main.proto b/proto/main.proto index 1303a42..680e8da 100644 --- a/proto/main.proto +++ b/proto/main.proto @@ -44,17 +44,17 @@ service EveningDetectiveServer { rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) { option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Устройства"; - summary: "Обновить присутствие подключения браузера"; + tags : "Устройства"; + summary : "Обновить присутствие подключения браузера"; description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон."; - security: {}; + security : {}; }; } rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) { option (google.api.http) = { get: "/api/devices" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Устройства"; - summary: "Получить устройства площадки"; + tags : "Устройства"; + summary : "Получить устройства площадки"; description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры."; }; } @@ -2401,39 +2401,60 @@ message GameResultsPackageRsp { } message DeviceHeartbeatReq { - string deviceID = 1; - string connectionID = 2; + string deviceID = 1; + string connectionID = 2; optional int32 organizerGameID = 3; - optional int32 teamID = 4; - string password = 5; - string screenID = 6; - string accessToken = 7; + optional int32 teamID = 4; + string password = 5; + string screenID = 6; + string accessToken = 7; } + message DeviceHeartbeatRsp { - optional bool enabled = 1; + optional bool enabled = 1; google.protobuf.Timestamp serverTime = 2; } + message GetDevicesReq { optional int32 gameID = 1; } + message GetDevicesRsp { - repeated VenueDevice devices = 1; + repeated VenueDevice devices = 1; google.protobuf.Timestamp serverTime = 2; + VenueWiFi wifi = 3; // Entire venue, independent of the game filter. } + message VenueDevice { - string ID = 1; + string ID = 1; google.protobuf.Timestamp firstSeenAt = 2; - google.protobuf.Timestamp lastSeenAt = 3; - bool connected = 4; + google.protobuf.Timestamp lastSeenAt = 3; + bool connected = 4; repeated DeviceConnection connections = 5; } + message DeviceConnection { - string ID = 1; + string ID = 1; google.protobuf.Timestamp firstSeenAt = 2; - google.protobuf.Timestamp lastSeenAt = 3; - string IP = 4; - string userAgent = 5; - bool connected = 6; - optional int32 gameID = 7; - optional int32 teamID = 8; - optional int32 screenID = 9; - string kind = 10; // browser, organizer, team, screen + google.protobuf.Timestamp lastSeenAt = 3; + string IP = 4; + string userAgent = 5; + bool connected = 6; + optional int32 gameID = 7; + optional int32 teamID = 8; + optional int32 screenID = 9; + string kind = 10; // browser, organizer, team, screen +} + +message VenueWiFi { + bool enabled = 1; + google.protobuf.Timestamp collectedAt = 2; + bool stale = 3; + bool hasError = 4; + repeated WiFiClient clients = 5; +} + +message WiFiClient { + string IP = 1; + string MAC = 2; + string band = 3; + optional int32 signal = 4; // dBm; absent when the router does not report it. } diff --git a/src/api/generated/crabs/evening_detective_server/index.ts b/src/api/generated/crabs/evening_detective_server/index.ts index fd5c37a..92c2a68 100644 --- a/src/api/generated/crabs/evening_detective_server/index.ts +++ b/src/api/generated/crabs/evening_detective_server/index.ts @@ -1303,6 +1303,7 @@ export type GetDevicesReq = { export type GetDevicesRsp = { devices: VenueDevice[] | undefined; serverTime: wellKnownTimestamp | undefined; + wifi: VenueWiFi | undefined; }; export type VenueDevice = { @@ -1326,6 +1327,21 @@ export type DeviceConnection = { kind: string | undefined; }; +export type VenueWiFi = { + enabled: boolean | undefined; + collectedAt: wellKnownTimestamp | undefined; + stale: boolean | undefined; + hasError: boolean | undefined; + clients: WiFiClient[] | undefined; +}; + +export type WiFiClient = { + IP: string | undefined; + MAC: string | undefined; + band: string | undefined; + signal?: number; +}; + export interface EveningDetectiveServer { DeviceHeartbeat(request: DeviceHeartbeatReq): Promise; GetDevices(request: GetDevicesReq): Promise; diff --git a/src/components/GamePage.vue b/src/components/GamePage.vue index a4d4fcd..9d79fe7 100644 --- a/src/components/GamePage.vue +++ b/src/components/GamePage.vue @@ -136,7 +136,7 @@ const canViewState = computed( ) const isStateOnly = computed(() => canViewState.value && !canManageGame.value) useDeviceContext(() => (canManageGame.value ? { organizerGameID: Number(gameId) } : {})) -const { devices, serverTime, isLoading, hasError, summary, reload } = useGameState( +const { devices, wifi, serverTime, isLoading, hasError, summary, reload } = useGameState( () => Number(route.params.id), () => canViewState.value && activeTab.value === 'state', ) @@ -178,7 +178,10 @@ watch( ) const canExport = computed( - () => uiStore.environment === 'prod' && authStore.hasAnyRole(['admin', 'organizer', 'developer']), + () => + Number(game.value.ID) > 0 && + uiStore.environment === 'prod' && + authStore.hasAnyRole(['admin', 'organizer', 'developer']), ) const isStartingExport = ref(false) const isDownloadingPackage = ref(false) @@ -1360,6 +1363,7 @@ useSimplePolling(() => { > import { NAlert, NButton, NCollapse, NCollapseItem, NFlex, NSpin, NTag } from 'naive-ui' -import type { VenueDevice } from '@/api/generated/crabs/evening_detective_server' +import type { VenueDevice, VenueWiFi } from '@/api/generated/crabs/evening_detective_server' import UiBlock from '@/components/UiBlock.vue' import { formatGameDateTime } from '@/utils/datetime' defineProps<{ devices: VenueDevice[] + wifi?: VenueWiFi serverTime?: string isLoading: boolean hasError: boolean @@ -37,6 +38,35 @@ const kinds: Record = {

Загружаем состояние…

Устройства этой игры пока не зарегистрированы

+ +

Wi-Fi площадки

+

+ Все Wi-Fi-клиенты Cudy, независимо от выбранной игры. MAC устройства и UUID браузера + различаются. +

+

Сбор данных Cudy не настроен

+ +
{{ device.ID }} diff --git a/src/composables/__tests__/gameState.test.ts b/src/composables/__tests__/gameState.test.ts index 7d48c4f..97503bf 100644 --- a/src/composables/__tests__/gameState.test.ts +++ b/src/composables/__tests__/gameState.test.ts @@ -38,9 +38,22 @@ const settle = async () => { await new Promise(setImmediate) } const response = (devices: unknown[] = []) => - new Response(JSON.stringify({ devices, serverTime: '2026-10-11T10:00:00Z' }), { - headers: { 'Content-Type': 'application/json' }, - }) + new Response( + JSON.stringify({ + devices, + serverTime: '2026-10-11T10:00:00Z', + wifi: { + enabled: true, + collectedAt: '2026-10-11T10:00:00Z', + stale: false, + hasError: false, + clients: [{ MAC: 'AA:BB:CC:DD:EE:01', signal: -65, band: '5 GHz' }], + }, + }), + { + headers: { 'Content-Type': 'application/json' }, + }, + ) test('counts, sorting, polling, late responses and recovery', async () => { const active = ref(false) @@ -95,6 +108,7 @@ test('counts, sorting, polling, late responses and recovery', async () => { state.devices.value.map((d: { ID: string }) => d.ID), ['a', 'z', 'b'], ) + assert.equal(state.wifi.value.clients[0].signal, -65) assert.deepEqual(state.summary.value, { connected: 1, disconnected: 2, connections: 3 }) globalThis.fetch = async () => { throw new Error('offline') @@ -103,6 +117,7 @@ test('counts, sorting, polling, late responses and recovery', async () => { assert.equal(state.hasError.value, true) assert.equal(state.devices.value.length, 3) assert.ok(state.serverTime.value) + assert.equal(state.wifi.value.clients[0].signal, -65) globalThis.fetch = async () => response() await state.reload() assert.equal(state.hasError.value, false) @@ -127,6 +142,7 @@ test('counts, sorting, polling, late responses and recovery', async () => { oldResolve(response([{ ID: 'old game' }])) await settle() assert.equal(state.devices.value.length, 0) + assert.equal(state.wifi.value, undefined) resolve(response([{ ID: 'new game', connected: true }])) await settle() assert.equal(state.devices.value[0].ID, 'new game') diff --git a/src/composables/useGameState.ts b/src/composables/useGameState.ts index 8878565..bb3849a 100644 --- a/src/composables/useGameState.ts +++ b/src/composables/useGameState.ts @@ -1,12 +1,13 @@ import { computed, onScopeDispose, ref, watch } from 'vue' import { getAuthClient } from '@/api/auth_client' -import type { VenueDevice } from '@/api/generated/crabs/evening_detective_server' +import type { VenueDevice, VenueWiFi } from '@/api/generated/crabs/evening_detective_server' import { useSimplePolling } from '@/composables/useSimplePolling' export function useGameState(gameID: () => number, isActive: () => boolean) { const client = getAuthClient() const devices = ref([]) + const wifi = ref() const serverTime = ref() const isLoading = ref(false) const hasError = ref(false) @@ -35,6 +36,7 @@ export function useGameState(gameID: () => number, isActive: () => boolean) { (a, b) => Number(!!a.connected) - Number(!!b.connected) || (a.ID || '').localeCompare(b.ID || ''), ) + wifi.value = result.wifi serverTime.value = result.serverTime hasError.value = false } catch { @@ -54,6 +56,7 @@ export function useGameState(gameID: () => number, isActive: () => boolean) { isLoading.value = false if (id !== previousID) { devices.value = [] + wifi.value = undefined serverTime.value = undefined hasError.value = false } @@ -69,5 +72,5 @@ export function useGameState(gameID: () => number, isActive: () => boolean) { version++ }) - return { devices, serverTime, isLoading, hasError, summary, reload } + return { devices, wifi, serverTime, isLoading, hasError, summary, reload } }