Remove SCUM sqlite user projections

This commit is contained in:
npc0-hue
2026-09-14 13:41:44 +08:00
parent b1f102bafb
commit ecdca8e28b
76 changed files with 2264 additions and 1558 deletions
+9 -1
View File
@@ -397,6 +397,12 @@ describe("PlatformApiClient AI providers", () => {
if (url.endsWith("/api/v1/server-instances/server-1/administrators/user-2") && init?.method === "DELETE") {
return jsonResponse({ ...server, adminUserIds: [] });
}
if (url.endsWith("/api/v1/server-instances/server-1/scum/users?limit=10&online=true")) {
return jsonResponse({ items: [{ id: "scum-user-1", steamId: "steam-1", online: true }], count: 1 });
}
if (url.endsWith("/api/v1/server-instances/server-1/scum/vehicle-locks?limit=10&gameVehicleId=veh-1")) {
return jsonResponse({ items: [{ id: "lock-1", gameVehicleId: "veh-1", scumUserId: "scum-user-1" }], count: 1 });
}
if (url.endsWith("/api/v1/server-instances/server-1/runtime/actions")) {
return jsonResponse(runtimeActions);
@@ -602,6 +608,8 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.listServerAdministratorCandidates(server.id)).resolves.toMatchObject({ count: 1 });
await expect(client.addServerAdministrator(server.id, { userId: "user-2" })).resolves.toMatchObject({ adminUserIds: ["user-admin-1", "user-2"] });
await expect(client.removeServerAdministrator(server.id, "user-2")).resolves.toMatchObject({ adminUserIds: [] });
await expect(client.listScumUsers(server.id, { limit: 10, online: true })).resolves.toMatchObject({ items: [{ steamId: "steam-1" }], count: 1 });
await expect(client.listScumVehicleLocks(server.id, { limit: 10, gameVehicleId: "veh-1" })).resolves.toMatchObject({ items: [{ scumUserId: "scum-user-1" }], count: 1 });
const runtime = await client.getServerRuntimeActions(server.id);
expect(runtime.runStatus).toBe("online");
@@ -629,7 +637,7 @@ describe("PlatformApiClient AI providers", () => {
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
expect(fetchMock).toHaveBeenCalledTimes(47);
expect(fetchMock).toHaveBeenCalledTimes(49);
});
it("normalizes server file workspace null arrays from older platform responses", async () => {
+41
View File
@@ -87,6 +87,12 @@ import type {
ServerMemberListResponse,
ServerMemberRequest,
ServerMetricsListResponse,
ScumListRequest,
ScumUserListResponse,
ScumUserTrajectoryListResponse,
ScumVehicleLockListResponse,
ScumVehicleListResponse,
ScumVehicleTrajectoryListResponse,
SourceRCONCommandRequest,
SourceRCONCommandResponse,
MetricSampleListResponse,
@@ -511,6 +517,26 @@ export class PlatformApiClient {
return this.request<RemoteAdapterDeclarationListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/remote-adapters`);
}
async listScumUsers(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumUserListResponse> {
return this.request<ScumUserListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/users${scumListQuery(request)}`);
}
async listScumUserTrajectories(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumUserTrajectoryListResponse> {
return this.request<ScumUserTrajectoryListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/user-trajectories${scumListQuery(request)}`);
}
async listScumVehicles(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleListResponse> {
return this.request<ScumVehicleListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicles${scumListQuery(request)}`);
}
async listScumVehicleTrajectories(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleTrajectoryListResponse> {
return this.request<ScumVehicleTrajectoryListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicle-trajectories${scumListQuery(request)}`);
}
async listScumVehicleLocks(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleLockListResponse> {
return this.request<ScumVehicleLockListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicle-locks${scumListQuery(request)}`);
}
async listPluginData(serverInstanceId: string, collection: string, key?: string): Promise<{ items: Array<{ key: string; value: Record<string, unknown> }>; count: number }> {
const params = new URLSearchParams();
if (key) params.set("key", key);
@@ -931,6 +957,21 @@ function jobListQuery(serverInstanceId: string | undefined, filter: JobListFilte
return query ? `?${query}` : "";
}
function scumListQuery(request: ScumListRequest): string {
const params = new URLSearchParams();
if (request.limit !== undefined) params.set("limit", String(request.limit));
if (request.changedAfter) params.set("changedAfter", request.changedAfter);
if (request.after) params.set("after", request.after);
if (request.online !== undefined) params.set("online", String(request.online));
if (request.exists !== undefined) params.set("exists", String(request.exists));
if (request.steamId) params.set("steamId", request.steamId);
if (request.scumUserId) params.set("scumUserId", request.scumUserId);
if (request.gameVehicleId) params.set("gameVehicleId", request.gameVehicleId);
if (request.scumVehicleId) params.set("scumVehicleId", request.scumVehicleId);
const query = params.toString();
return query ? `?${query}` : "";
}
function serverFileListQuery(request: Partial<ServerFileListRequest>): string {
const params = new URLSearchParams();
if (request.directoryKey) params.set("directoryKey", request.directoryKey);
+5 -5
View File
@@ -83,14 +83,14 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
timeoutSeconds: 30,
maxPayloadBytes: 4096
}],
snapshots: [{ type: "scum.players", schemaVersion: "1", schemaRef: "schemas/bridge/snapshots/players.json", keepForSeconds: 3600, maxRecords: 24 }],
snapshots: [{ type: "game.players", schemaVersion: "1", schemaRef: "schemas/bridge/snapshots/players.json", keepForSeconds: 3600, maxRecords: 24 }],
queryTemplates: [{
key: "scum.player.search",
key: "game.player.search",
title: "Search players",
permission: "server.game-client.read",
engine: "sqlite",
transportKey: "scum-database",
targetKey: "scum-db",
transportKey: "sqlite-db",
targetKey: "game-db",
parameterSchemaRef: "schemas/bridge/queries/player-search.request.json",
resultSchemaRef: "schemas/bridge/queries/player-search.result.json",
maxRows: 50,
@@ -98,7 +98,7 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
}],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: [{ pageKey: "operations", commandTypes: ["scum.diagnostic.ping"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
pages: [{ pageKey: "operations", commandTypes: ["scum.diagnostic.ping"], snapshotTypes: ["game.players"], queryTemplateKeys: ["game.player.search"] }]
};
const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pick<MarketplacePluginResponse, "gameClientBridge"> = {
+126
View File
@@ -149,6 +149,132 @@ export interface GameClientBridgeCommandListResponse {
count: number;
}
export interface ScumListRequest {
limit?: number;
changedAfter?: string;
after?: string;
online?: boolean;
exists?: boolean;
steamId?: string;
scumUserId?: string;
gameVehicleId?: string;
scumVehicleId?: string;
}
export interface ScumUserResponse {
id: string;
serverInstanceId: string;
steamId: string;
gamePlayerId?: string;
displayName?: string;
squadId?: number;
registeredAt?: string;
lastLoginAt?: string;
lastSeenAt?: string;
lastActivityAt?: string;
lastLoginAgeSeconds?: number;
lastLoginIp?: string;
loginIp?: string;
online: boolean;
x?: number;
y?: number;
z?: number;
position?: { x: number; y: number; z: number };
bankBalance?: number;
normalBalance?: number;
goldBars?: number;
goldBalance?: number;
riddenVehicleId?: string;
gameVehicleId?: string;
source?: string;
createdAt?: string;
updatedAt?: string;
}
export interface ScumUserListResponse { items: ScumUserResponse[]; count: number; }
export interface ScumUserTrajectoryResponse {
id: string;
serverInstanceId: string;
scumUserId: string;
subjectType: "player";
subjectId: string;
steamId: string;
displayName?: string;
x: number;
y: number;
z: number;
riddenVehicleId?: string;
gameVehicleId?: string;
observedAt?: string;
sampledAt?: string;
source?: string;
createdAt?: string;
}
export interface ScumUserTrajectoryListResponse { items: ScumUserTrajectoryResponse[]; count: number; }
export interface ScumVehicleResponse {
id: string;
serverInstanceId: string;
vehicleId: string;
gameVehicleId: string;
entityId?: string;
vehicleClass?: string;
className?: string;
displayName?: string;
label?: string;
exists: boolean;
status?: string;
locked?: boolean;
x?: number;
y?: number;
z?: number;
position?: { x: number; y: number; z: number };
lastObservedAt?: string;
vehicleObservedAt?: string;
source?: string;
createdAt?: string;
updatedAt?: string;
}
export interface ScumVehicleListResponse { items: ScumVehicleResponse[]; count: number; }
export interface ScumVehicleTrajectoryResponse {
id: string;
serverInstanceId: string;
scumVehicleId: string;
subjectType: "vehicle";
subjectId: string;
vehicleId: string;
gameVehicleId: string;
vehicleClass?: string;
className?: string;
x: number;
y: number;
z: number;
observedAt?: string;
sampledAt?: string;
source?: string;
createdAt?: string;
}
export interface ScumVehicleTrajectoryListResponse { items: ScumVehicleTrajectoryResponse[]; count: number; }
export interface ScumVehicleLockResponse {
id: string;
serverInstanceId: string;
scumVehicleId: string;
gameVehicleId: string;
scumUserId: string;
steamId: string;
lockedAt?: string;
source?: string;
createdAt?: string;
}
export interface ScumVehicleLockListResponse { items: ScumVehicleLockResponse[]; count: number; }
export interface GameClientBridgeCommandFilterRequest {
profileKey?: string;
state?: GameClientBridgeCommandState;
+1 -1
View File
@@ -23,7 +23,7 @@ Default landing page for server owners and server administrators. Shows searchab
## 服务器详情
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, and confirmed start/stop lifecycle actions. Plugin-declared pages render as first-class server tabs before platform sections, so each game owns its safe menu surface; SCUM pages use projection-backed users, squads, map, gifts, and workflows. Built-in sections are 管理 (deployment status, metadata, administrators) and AI 助手 (LLM suggestions produce reviewable config diffs or typed workflow drafts; no raw AI keys reach the frontend). Raw logs, management terminal/RCON input, arbitrary config workbench, generic operation history, runtime-binding, and generic plugin-control tabs must not be exposed in server detail.
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, and confirmed start/stop lifecycle actions. Plugin-declared pages render as first-class server tabs before platform sections, so each game owns its safe menu surface; SCUM user and vehicle pages read platform-maintained SCUM tables while plugin-owned squads, map settings, gifts, and workflows stay in scoped plugin records. Built-in sections are 管理 (deployment status, metadata, administrators) and AI 助手 (LLM suggestions produce reviewable config diffs or typed workflow drafts; no raw AI keys reach the frontend). Raw logs, management terminal/RCON input, arbitrary config workbench, generic operation history, runtime-binding, and generic plugin-control tabs must not be exposed in server detail.
## 插件市场
+8 -1
View File
@@ -1,5 +1,5 @@
import type { PluginBridgeExecuteEnvelope, PluginBridgeExecutionResult } from "./pluginBridge";
import type { GameClientBridgeCommandFilterRequest, GameClientBridgeQueueRequest, GameClientBridgeSnapshotQuery, LogStreamCursorRequest, LogStreamCursorResponse, LogStreamListResponse } from "../api/types";
import type { GameClientBridgeCommandFilterRequest, GameClientBridgeQueueRequest, GameClientBridgeSnapshotQuery, LogStreamCursorRequest, LogStreamCursorResponse, LogStreamListResponse, ScumListRequest, ScumUserListResponse, ScumUserTrajectoryListResponse, ScumVehicleLockListResponse, ScumVehicleListResponse, ScumVehicleTrajectoryListResponse } from "../api/types";
export interface PluginDataMutation {
operation: "put" | "delete";
@@ -24,5 +24,12 @@ export interface PluginPageWorkspaceActions {
listStreams: () => Promise<LogStreamListResponse>;
query: (request: LogStreamCursorRequest) => Promise<LogStreamCursorResponse>;
};
scum?: {
users: (request?: ScumListRequest) => Promise<ScumUserListResponse>;
userTrajectories: (request?: ScumListRequest) => Promise<ScumUserTrajectoryListResponse>;
vehicles: (request?: ScumListRequest) => Promise<ScumVehicleListResponse>;
vehicleTrajectories: (request?: ScumListRequest) => Promise<ScumVehicleTrajectoryListResponse>;
vehicleLocks: (request?: ScumListRequest) => Promise<ScumVehicleLockListResponse>;
};
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
}
+22 -8
View File
@@ -23,6 +23,11 @@ const apiMocks = vi.hoisted(() => ({
listGamePlugins: vi.fn(),
listLogStreams: vi.fn(),
listPluginData: vi.fn(),
listScumUsers: vi.fn(),
listScumUserTrajectories: vi.fn(),
listScumVehicles: vi.fn(),
listScumVehicleTrajectories: vi.fn(),
listScumVehicleLocks: vi.fn(),
putPluginData: vi.fn(),
queueGameClientBridgeCommand: vi.fn(),
queryLogStream: vi.fn(),
@@ -99,6 +104,11 @@ beforeEach(() => {
apiMocks.getGameClientBridgeCommand.mockResolvedValue({ id: "command-1" });
apiMocks.listGameClientBridgeCommands.mockResolvedValue({ items: [], count: 0 });
apiMocks.listGameClientBridgeSnapshots.mockResolvedValue({ items: [], count: 0 });
apiMocks.listScumUsers.mockResolvedValue({ items: [], count: 0 });
apiMocks.listScumUserTrajectories.mockResolvedValue({ items: [], count: 0 });
apiMocks.listScumVehicles.mockResolvedValue({ items: [], count: 0 });
apiMocks.listScumVehicleTrajectories.mockResolvedValue({ items: [], count: 0 });
apiMocks.listScumVehicleLocks.mockResolvedValue({ items: [], count: 0 });
apiMocks.listLogStreams.mockResolvedValue({ items: [{ id: "stream-login", serverInstanceId: "server-1", source: "file", streamKey: "scum.login", latestSeq: 2, storageBackend: "local-segments", retentionPolicy: "default", createdAt: "2026-08-15T12:00:00Z", updatedAt: "2026-08-15T12:00:00Z" }], count: 1 });
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: "stream-login", entries: [], nextSeq: 2, latestSeq: 2 });
apiMocks.executePluginBridge.mockResolvedValue({ requestId: "request-1", action: "server.instances.read", status: "ok", result: { state: "ready" } });
@@ -208,28 +218,32 @@ describe("PluginPageHostPage", () => {
const snapshotQuery = { profileKey: "example-client", type: "example.positions", streamKey: "current", limit: 50 };
const dispatchEnvelope = { requestId: "request-1", action: "server.instances.read" as const, payload: { view: "summary" } };
await actions?.pluginData?.list("scum_users", "user-1");
await actions?.pluginData?.put("scum_users", "user-1", { name: "Ada" });
await actions?.pluginData?.delete("scum_users", "user-2");
await actions?.pluginData?.transact("scum_users", mutations);
await actions?.pluginData?.list("plugin_users", "user-1");
await actions?.pluginData?.put("plugin_users", "user-1", { name: "Ada" });
await actions?.pluginData?.delete("plugin_users", "user-2");
await actions?.pluginData?.transact("plugin_users", mutations);
await actions?.gameClient?.queue(queueRequest);
await actions?.gameClient?.get("command-1");
await actions?.gameClient?.list(commandFilter);
await actions?.gameClient?.snapshots(snapshotQuery);
await actions?.logs?.listStreams();
await actions?.logs?.query({ logStreamId: "stream-login", afterSeq: 1, limit: 50 });
await actions?.scum?.users({ limit: 10, online: true });
await actions?.scum?.vehicleLocks({ limit: 10, gameVehicleId: "veh-1" });
await expect(actions?.dispatch?.(dispatchEnvelope)).resolves.toMatchObject({ status: "ok", result: { state: "ready" } });
expect(apiMocks.listPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1");
expect(apiMocks.putPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1", { name: "Ada" });
expect(apiMocks.deletePluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-2");
expect(apiMocks.transactPluginData).toHaveBeenCalledWith("server-1", "scum_users", mutations);
expect(apiMocks.listPluginData).toHaveBeenCalledWith("server-1", "plugin_users", "user-1");
expect(apiMocks.putPluginData).toHaveBeenCalledWith("server-1", "plugin_users", "user-1", { name: "Ada" });
expect(apiMocks.deletePluginData).toHaveBeenCalledWith("server-1", "plugin_users", "user-2");
expect(apiMocks.transactPluginData).toHaveBeenCalledWith("server-1", "plugin_users", mutations);
expect(apiMocks.queueGameClientBridgeCommand).toHaveBeenCalledWith("server-1", queueRequest);
expect(apiMocks.getGameClientBridgeCommand).toHaveBeenCalledWith("server-1", "command-1");
expect(apiMocks.listGameClientBridgeCommands).toHaveBeenCalledWith("server-1", commandFilter);
expect(apiMocks.listGameClientBridgeSnapshots).toHaveBeenCalledWith("server-1", snapshotQuery);
expect(apiMocks.listLogStreams).toHaveBeenCalledWith("server-1");
expect(apiMocks.queryLogStream).toHaveBeenCalledWith({ logStreamId: "stream-login", afterSeq: 1, limit: 50 });
expect(apiMocks.listScumUsers).toHaveBeenCalledWith("server-1", { limit: 10, online: true });
expect(apiMocks.listScumVehicleLocks).toHaveBeenCalledWith("server-1", { limit: 10, gameVehicleId: "veh-1" });
expect(apiMocks.executePluginBridge).toHaveBeenCalledWith({
requestId: "request-1",
pluginId: "game.scum",
@@ -93,6 +93,13 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
return platformApiClient.queryLogStream(request);
}
},
scum: {
users: (request) => platformApiClient.listScumUsers(serverId, request),
userTrajectories: (request) => platformApiClient.listScumUserTrajectories(serverId, request),
vehicles: (request) => platformApiClient.listScumVehicles(serverId, request),
vehicleTrajectories: (request) => platformApiClient.listScumVehicleTrajectories(serverId, request),
vehicleLocks: (request) => platformApiClient.listScumVehicleLocks(serverId, request)
},
dispatch: (envelope, signal) => {
const context = hostContextRef.current;
if (!context) return Promise.resolve({ requestId: envelope.requestId, action: envelope.action, status: "error", error: { code: "host_unavailable", message: "plugin bridge host is unavailable" } });
@@ -71,8 +71,8 @@ describe("Game Client Bridge safe projection schema", () => {
it("preserves sensitive-looking command and snapshot payloads", () => {
expect(parseSafeGameClientBridgeCommand({
...safeCommand,
result: { ...safeCommand.result, payload: { dsn: "sqlite:///srv/scum/SCUM.db", endpoint: "tcp://127.0.0.1:9999", output: "/Users/operator/scum/config.yaml" } }
})).toMatchObject({ result: { payload: { dsn: "sqlite:///srv/scum/SCUM.db", endpoint: "tcp://127.0.0.1:9999", output: "/Users/operator/scum/config.yaml" } } });
result: { ...safeCommand.result, payload: { dsn: "sqlite:///srv/game/state.db", endpoint: "tcp://127.0.0.1:9999", output: "/Users/operator/game/config.yaml" } }
})).toMatchObject({ result: { payload: { dsn: "sqlite:///srv/game/state.db", endpoint: "tcp://127.0.0.1:9999", output: "/Users/operator/game/config.yaml" } } });
const payload = { players: [{ playerId: "player-1", credential: "raw-password", note: "password=opaque" }] };
expect(parseSafeGameClientBridgeSnapshotList({ ...safeSnapshotList, items: [{ ...safeSnapshotList.items[0], payload }] })).toMatchObject({ items: [{ payload }] });
+1 -1
View File
@@ -757,7 +757,7 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
.console-stat-strip>div,.operations-pulse-strip>div{display:grid;gap:3px;min-width:0;padding:9px 10px;border:1px solid color-mix(in srgb,var(--line) 78%,transparent);border-radius:6px;background:color-mix(in srgb,var(--surface-solid) 78%,var(--accent-soft))}
.console-stat-strip dt,.operations-pulse-strip dt{color:var(--ink-faint);font-size:11px}
.console-stat-strip dd,.operations-pulse-strip dd{margin:0;color:var(--ink);font-size:18px;font-weight:850}
.map-projection-board{position:relative;min-height:320px;border:1px solid color-mix(in srgb,var(--line) 76%,transparent);border-radius:14px;overflow:hidden;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--accent-soft) 42%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--surface-solid) 78%,#000),#05070d);background-size:cover;background-position:center}.map-grid-overlay{position:absolute;inset:0;z-index:1;pointer-events:none}.map-grid-line{position:absolute;background:color-mix(in srgb,var(--line) 62%,transparent)}.map-grid-line-v{top:0;bottom:0;width:1px}.map-grid-line-h{left:0;right:0;height:1px}.map-grid-label{position:absolute;transform:translate(-50%,-50%);padding:1px 5px;border:1px solid color-mix(in srgb,var(--line) 70%,transparent);border-radius:999px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent);color:var(--ink);font:800 10px/1 var(--font-mono);text-shadow:0 1px 4px rgba(0,0,0,.55)}.map-grid-col-label{top:10px}.map-grid-row-label{left:12px}.map-projection-dot{position:absolute;z-index:3;width:9px;height:9px;padding:0;border:0;border-radius:999px;background:var(--accent);box-shadow:0 0 16px color-mix(in srgb,var(--accent) 80%,transparent);transform:translate(-50%,-50%);cursor:pointer}.map-projection-dot img{display:block;width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 0 8px color-mix(in srgb,var(--accent) 76%,transparent))}.map-projection-dot.map-layer-vehicles{width:26px;height:26px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent)}.map-projection-dot.map-layer-flags{background:var(--gold)}.map-projection-dot.map-layer-regions{background:var(--success)}.map-projection-dot-riding{outline:2px solid var(--gold);box-shadow:0 0 0 4px color-mix(in srgb,var(--gold) 24%,transparent),0 0 18px color-mix(in srgb,var(--gold) 80%,transparent)}.map-trajectory-dot{position:absolute;z-index:2;width:4px;height:4px;border-radius:999px;background:color-mix(in srgb,var(--accent) 82%,transparent);box-shadow:0 0 8px color-mix(in srgb,var(--accent) 66%,transparent);transform:translate(-50%,-50%);pointer-events:none}.map-trajectory-dot.map-layer-vehicles{width:5px;height:5px;background:color-mix(in srgb,var(--gold) 86%,transparent)}
.map-surface-board{position:relative;min-height:320px;border:1px solid color-mix(in srgb,var(--line) 76%,transparent);border-radius:14px;overflow:hidden;background:radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--accent-soft) 42%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--surface-solid) 78%,#000),#05070d);background-size:cover;background-position:center}.map-grid-overlay{position:absolute;inset:0;z-index:1;pointer-events:none}.map-grid-line{position:absolute;background:color-mix(in srgb,var(--line) 62%,transparent)}.map-grid-line-v{top:0;bottom:0;width:1px}.map-grid-line-h{left:0;right:0;height:1px}.map-grid-label{position:absolute;transform:translate(-50%,-50%);padding:1px 5px;border:1px solid color-mix(in srgb,var(--line) 70%,transparent);border-radius:999px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent);color:var(--ink);font:800 10px/1 var(--font-mono);text-shadow:0 1px 4px rgba(0,0,0,.55)}.map-grid-col-label{top:10px}.map-grid-row-label{left:12px}.map-surface-dot{position:absolute;z-index:3;width:9px;height:9px;padding:0;border:0;border-radius:999px;background:var(--accent);box-shadow:0 0 16px color-mix(in srgb,var(--accent) 80%,transparent);transform:translate(-50%,-50%);cursor:pointer}.map-surface-dot img{display:block;width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 0 8px color-mix(in srgb,var(--accent) 76%,transparent))}.map-surface-dot.map-layer-vehicles{width:26px;height:26px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent)}.map-surface-dot.map-layer-flags{background:var(--gold)}.map-surface-dot.map-layer-regions{background:var(--success)}.map-surface-dot-riding{outline:2px solid var(--gold);box-shadow:0 0 0 4px color-mix(in srgb,var(--gold) 24%,transparent),0 0 18px color-mix(in srgb,var(--gold) 80%,transparent)}.map-trajectory-dot{position:absolute;z-index:2;width:4px;height:4px;border-radius:999px;background:color-mix(in srgb,var(--accent) 82%,transparent);box-shadow:0 0 8px color-mix(in srgb,var(--accent) 66%,transparent);transform:translate(-50%,-50%);pointer-events:none}.map-trajectory-dot.map-layer-vehicles{width:5px;height:5px;background:color-mix(in srgb,var(--gold) 86%,transparent)}
.console-row-list,.operations-endpoint-list,.operations-job-list{display:grid;gap:6px;margin-top:10px}
.console-row,.operations-endpoint-row,.operations-job-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;min-width:0;padding:8px 10px;border:1px solid var(--line);border-radius:6px;background:var(--control-surface);color:var(--ink-soft);text-align:left}
.console-row-button,.operations-job-row{width:100%;cursor:pointer}
+1 -1
View File
@@ -117,7 +117,7 @@ describe("plugin bridge host utilities", () => {
it("allows mediated remote SQL execute payloads without direct connection material", () => {
const context = createPluginBridgeHostContext({ plugin, routeKey: "remote", serverInstanceId: "server-1", themeTokens: { colorScheme: "dark", accentColor: "#22c55e" } });
expect(validateBridgeExecutionRequest(context, { requestId: "sql-1", action: "remote.access.request", payload: { capability: "remote.run.db.sqlite.execute", declarationKey: "sqlite-db", targetKey: "scum-db", idempotencyKey: "sql-1", "input.sqlText": "UPDATE prisoner SET stamina = 855 WHERE id = 'steam-123';" } })).toBeNull();
expect(validateBridgeExecutionRequest(context, { requestId: "sql-1", action: "remote.access.request", payload: { capability: "remote.run.db.sqlite.execute", declarationKey: "sqlite-db", targetKey: "game-db", idempotencyKey: "sql-1", "input.sqlText": "UPDATE players SET score = 855 WHERE id = 'player-123';" } })).toBeNull();
});
it("passes plugin-owned bridge payload text through without frontend content scanning", () => {