generated from VLADIMIR/template_frontend
up
This commit is contained in:
+26
-5
@@ -44,17 +44,17 @@ service EveningDetectiveServer {
|
|||||||
rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) {
|
rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) {
|
||||||
option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" };
|
option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" };
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
tags: "Устройства";
|
tags : "Устройства";
|
||||||
summary: "Обновить присутствие подключения браузера";
|
summary : "Обновить присутствие подключения браузера";
|
||||||
description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.";
|
description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.";
|
||||||
security: {};
|
security : {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) {
|
rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) {
|
||||||
option (google.api.http) = { get: "/api/devices" };
|
option (google.api.http) = { get: "/api/devices" };
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
tags: "Устройства";
|
tags : "Устройства";
|
||||||
summary: "Получить устройства площадки";
|
summary : "Получить устройства площадки";
|
||||||
description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.";
|
description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2409,15 +2409,20 @@ message DeviceHeartbeatReq {
|
|||||||
string screenID = 6;
|
string screenID = 6;
|
||||||
string accessToken = 7;
|
string accessToken = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeviceHeartbeatRsp {
|
message DeviceHeartbeatRsp {
|
||||||
optional bool enabled = 1;
|
optional bool enabled = 1;
|
||||||
google.protobuf.Timestamp serverTime = 2;
|
google.protobuf.Timestamp serverTime = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDevicesReq { optional int32 gameID = 1; }
|
message GetDevicesReq { optional int32 gameID = 1; }
|
||||||
|
|
||||||
message GetDevicesRsp {
|
message GetDevicesRsp {
|
||||||
repeated VenueDevice devices = 1;
|
repeated VenueDevice devices = 1;
|
||||||
google.protobuf.Timestamp serverTime = 2;
|
google.protobuf.Timestamp serverTime = 2;
|
||||||
|
VenueWiFi wifi = 3; // Entire venue, independent of the game filter.
|
||||||
}
|
}
|
||||||
|
|
||||||
message VenueDevice {
|
message VenueDevice {
|
||||||
string ID = 1;
|
string ID = 1;
|
||||||
google.protobuf.Timestamp firstSeenAt = 2;
|
google.protobuf.Timestamp firstSeenAt = 2;
|
||||||
@@ -2425,6 +2430,7 @@ message VenueDevice {
|
|||||||
bool connected = 4;
|
bool connected = 4;
|
||||||
repeated DeviceConnection connections = 5;
|
repeated DeviceConnection connections = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeviceConnection {
|
message DeviceConnection {
|
||||||
string ID = 1;
|
string ID = 1;
|
||||||
google.protobuf.Timestamp firstSeenAt = 2;
|
google.protobuf.Timestamp firstSeenAt = 2;
|
||||||
@@ -2437,3 +2443,18 @@ message DeviceConnection {
|
|||||||
optional int32 screenID = 9;
|
optional int32 screenID = 9;
|
||||||
string kind = 10; // browser, organizer, team, screen
|
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.
|
||||||
|
}
|
||||||
|
|||||||
@@ -1303,6 +1303,7 @@ export type GetDevicesReq = {
|
|||||||
export type GetDevicesRsp = {
|
export type GetDevicesRsp = {
|
||||||
devices: VenueDevice[] | undefined;
|
devices: VenueDevice[] | undefined;
|
||||||
serverTime: wellKnownTimestamp | undefined;
|
serverTime: wellKnownTimestamp | undefined;
|
||||||
|
wifi: VenueWiFi | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VenueDevice = {
|
export type VenueDevice = {
|
||||||
@@ -1326,6 +1327,21 @@ export type DeviceConnection = {
|
|||||||
kind: string | undefined;
|
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 {
|
export interface EveningDetectiveServer {
|
||||||
DeviceHeartbeat(request: DeviceHeartbeatReq): Promise<DeviceHeartbeatRsp>;
|
DeviceHeartbeat(request: DeviceHeartbeatReq): Promise<DeviceHeartbeatRsp>;
|
||||||
GetDevices(request: GetDevicesReq): Promise<GetDevicesRsp>;
|
GetDevices(request: GetDevicesReq): Promise<GetDevicesRsp>;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const canViewState = computed(
|
|||||||
)
|
)
|
||||||
const isStateOnly = computed(() => canViewState.value && !canManageGame.value)
|
const isStateOnly = computed(() => canViewState.value && !canManageGame.value)
|
||||||
useDeviceContext(() => (canManageGame.value ? { organizerGameID: Number(gameId) } : {}))
|
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),
|
() => Number(route.params.id),
|
||||||
() => canViewState.value && activeTab.value === 'state',
|
() => canViewState.value && activeTab.value === 'state',
|
||||||
)
|
)
|
||||||
@@ -178,7 +178,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const canExport = computed(
|
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 isStartingExport = ref(false)
|
||||||
const isDownloadingPackage = ref(false)
|
const isDownloadingPackage = ref(false)
|
||||||
@@ -1360,6 +1363,7 @@ useSimplePolling(() => {
|
|||||||
>
|
>
|
||||||
<GameStatePanel
|
<GameStatePanel
|
||||||
:devices="devices"
|
:devices="devices"
|
||||||
|
:wifi="wifi"
|
||||||
:server-time="serverTime"
|
:server-time="serverTime"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
:has-error="hasError"
|
:has-error="hasError"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NAlert, NButton, NCollapse, NCollapseItem, NFlex, NSpin, NTag } from 'naive-ui'
|
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 UiBlock from '@/components/UiBlock.vue'
|
||||||
import { formatGameDateTime } from '@/utils/datetime'
|
import { formatGameDateTime } from '@/utils/datetime'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
devices: VenueDevice[]
|
devices: VenueDevice[]
|
||||||
|
wifi?: VenueWiFi
|
||||||
serverTime?: string
|
serverTime?: string
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
@@ -37,6 +38,35 @@ const kinds: Record<string, string> = {
|
|||||||
<p v-else-if="!hasError && !serverTime && !isLoading">Загружаем состояние…</p>
|
<p v-else-if="!hasError && !serverTime && !isLoading">Загружаем состояние…</p>
|
||||||
<p v-else-if="serverTime && !devices.length">Устройства этой игры пока не зарегистрированы</p>
|
<p v-else-if="serverTime && !devices.length">Устройства этой игры пока не зарегистрированы</p>
|
||||||
</UiBlock>
|
</UiBlock>
|
||||||
|
<UiBlock>
|
||||||
|
<h3>Wi-Fi площадки</h3>
|
||||||
|
<p>
|
||||||
|
Все Wi-Fi-клиенты Cudy, независимо от выбранной игры. MAC устройства и UUID браузера
|
||||||
|
различаются.
|
||||||
|
</p>
|
||||||
|
<p v-if="!wifi?.enabled">Сбор данных Cudy не настроен</p>
|
||||||
|
<template v-else>
|
||||||
|
<n-alert
|
||||||
|
v-if="wifi.hasError"
|
||||||
|
type="warning"
|
||||||
|
role="alert"
|
||||||
|
title="Не удалось обновить данные Cudy"
|
||||||
|
/>
|
||||||
|
<n-tag v-if="wifi.stale" type="warning">Данные устарели</n-tag>
|
||||||
|
<p v-if="wifi.collectedAt">
|
||||||
|
Последний снимок Cudy: {{ formatGameDateTime(wifi.collectedAt) }}
|
||||||
|
</p>
|
||||||
|
<template v-if="wifi.collectedAt">
|
||||||
|
<p>Wi-Fi-клиентов: {{ wifi.clients?.length || 0 }}</p>
|
||||||
|
<p v-if="!wifi.clients?.length">Wi-Fi-клиенты пока не подключены</p>
|
||||||
|
<div v-for="client in wifi.clients" :key="client.MAC" class="connection device-details">
|
||||||
|
<strong>{{ client.MAC }}</strong>
|
||||||
|
<p>IP: {{ client.IP || '—' }} · {{ client.band }}</p>
|
||||||
|
<p>Сигнал: {{ client.signal === undefined ? 'Нет данных' : `${client.signal} dBm` }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</UiBlock>
|
||||||
<UiBlock v-for="device in devices" :key="device.ID">
|
<UiBlock v-for="device in devices" :key="device.ID">
|
||||||
<div class="device-details">
|
<div class="device-details">
|
||||||
<strong>{{ device.ID }}</strong>
|
<strong>{{ device.ID }}</strong>
|
||||||
|
|||||||
@@ -38,9 +38,22 @@ const settle = async () => {
|
|||||||
await new Promise(setImmediate)
|
await new Promise(setImmediate)
|
||||||
}
|
}
|
||||||
const response = (devices: unknown[] = []) =>
|
const response = (devices: unknown[] = []) =>
|
||||||
new Response(JSON.stringify({ devices, serverTime: '2026-10-11T10:00:00Z' }), {
|
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' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
test('counts, sorting, polling, late responses and recovery', async () => {
|
test('counts, sorting, polling, late responses and recovery', async () => {
|
||||||
const active = ref(false)
|
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),
|
state.devices.value.map((d: { ID: string }) => d.ID),
|
||||||
['a', 'z', 'b'],
|
['a', 'z', 'b'],
|
||||||
)
|
)
|
||||||
|
assert.equal(state.wifi.value.clients[0].signal, -65)
|
||||||
assert.deepEqual(state.summary.value, { connected: 1, disconnected: 2, connections: 3 })
|
assert.deepEqual(state.summary.value, { connected: 1, disconnected: 2, connections: 3 })
|
||||||
globalThis.fetch = async () => {
|
globalThis.fetch = async () => {
|
||||||
throw new Error('offline')
|
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.hasError.value, true)
|
||||||
assert.equal(state.devices.value.length, 3)
|
assert.equal(state.devices.value.length, 3)
|
||||||
assert.ok(state.serverTime.value)
|
assert.ok(state.serverTime.value)
|
||||||
|
assert.equal(state.wifi.value.clients[0].signal, -65)
|
||||||
globalThis.fetch = async () => response()
|
globalThis.fetch = async () => response()
|
||||||
await state.reload()
|
await state.reload()
|
||||||
assert.equal(state.hasError.value, false)
|
assert.equal(state.hasError.value, false)
|
||||||
@@ -127,6 +142,7 @@ test('counts, sorting, polling, late responses and recovery', async () => {
|
|||||||
oldResolve(response([{ ID: 'old game' }]))
|
oldResolve(response([{ ID: 'old game' }]))
|
||||||
await settle()
|
await settle()
|
||||||
assert.equal(state.devices.value.length, 0)
|
assert.equal(state.devices.value.length, 0)
|
||||||
|
assert.equal(state.wifi.value, undefined)
|
||||||
resolve(response([{ ID: 'new game', connected: true }]))
|
resolve(response([{ ID: 'new game', connected: true }]))
|
||||||
await settle()
|
await settle()
|
||||||
assert.equal(state.devices.value[0].ID, 'new game')
|
assert.equal(state.devices.value[0].ID, 'new game')
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { computed, onScopeDispose, ref, watch } from 'vue'
|
import { computed, onScopeDispose, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { getAuthClient } from '@/api/auth_client'
|
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'
|
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||||
|
|
||||||
export function useGameState(gameID: () => number, isActive: () => boolean) {
|
export function useGameState(gameID: () => number, isActive: () => boolean) {
|
||||||
const client = getAuthClient()
|
const client = getAuthClient()
|
||||||
const devices = ref<VenueDevice[]>([])
|
const devices = ref<VenueDevice[]>([])
|
||||||
|
const wifi = ref<VenueWiFi>()
|
||||||
const serverTime = ref<string>()
|
const serverTime = ref<string>()
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const hasError = ref(false)
|
const hasError = ref(false)
|
||||||
@@ -35,6 +36,7 @@ export function useGameState(gameID: () => number, isActive: () => boolean) {
|
|||||||
(a, b) =>
|
(a, b) =>
|
||||||
Number(!!a.connected) - Number(!!b.connected) || (a.ID || '').localeCompare(b.ID || ''),
|
Number(!!a.connected) - Number(!!b.connected) || (a.ID || '').localeCompare(b.ID || ''),
|
||||||
)
|
)
|
||||||
|
wifi.value = result.wifi
|
||||||
serverTime.value = result.serverTime
|
serverTime.value = result.serverTime
|
||||||
hasError.value = false
|
hasError.value = false
|
||||||
} catch {
|
} catch {
|
||||||
@@ -54,6 +56,7 @@ export function useGameState(gameID: () => number, isActive: () => boolean) {
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
if (id !== previousID) {
|
if (id !== previousID) {
|
||||||
devices.value = []
|
devices.value = []
|
||||||
|
wifi.value = undefined
|
||||||
serverTime.value = undefined
|
serverTime.value = undefined
|
||||||
hasError.value = false
|
hasError.value = false
|
||||||
}
|
}
|
||||||
@@ -69,5 +72,5 @@ export function useGameState(gameID: () => number, isActive: () => boolean) {
|
|||||||
version++
|
version++
|
||||||
})
|
})
|
||||||
|
|
||||||
return { devices, serverTime, isLoading, hasError, summary, reload }
|
return { devices, wifi, serverTime, isLoading, hasError, summary, reload }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user