Files
OpenFrontIO/src/client/utilities/Diagnostic.ts
T
Skigim f7598369ed refactor: consolidate platform detection across client components (#3325)
## Description:

This PR consolidates ad hoc platform/environment/viewport detection into
a single shared utility. It is scoped to this refactor only, and serves
as groundwork for the mobile-focused feature work planned for the v31
milestone.

### What changed
- Introduced a shared `Platform` utility centralising:
  - OS detection (with `userAgentData` + UA fallback)
  - Electron environment detection
- Viewport breakpoint helpers (`isMobileWidth`, `isTabletWidth`,
`isDesktopWidth`)
- Replaced duplicated inline checks across client files with the shared
API.
- Normalised Mac detection to derive from the consolidated OS logic
rather than a separate regex.

### Why
- Multiple client files each independently ran `navigator.userAgent`
regexes or copy-pasted `isElectron` logic — this unifies all of that.
- Puts a stable, tested abstraction in place before v31 mobile work
lands, so mobile feature branches have a consistent surface to build
against.

## Please complete the following:

- [x] I have added screenshots for all UI updates (N/A: refactor only,
no visible UI changes)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A: no new user-facing strings)
- [x] I have added relevant tests to the test directory (N/A: refactor
only)
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

skigim
2026-03-02 10:12:48 -08:00

135 lines
3.0 KiB
TypeScript

import { Platform } from "../Platform";
export type RendererType = "Canvas2D" | "WebGL1" | "WebGL2";
export interface BrowserInfo {
engine: string;
platform: string;
os: string;
dpr: number;
}
export interface GraphicsDiagnostics {
browser: BrowserInfo;
rendering: RenderingInfo;
power: PowerInfo;
}
export interface GPUInfo {
vendor?: string;
renderer?: string;
software?: boolean;
unavailable?: boolean;
}
export interface RenderingInfo {
type: RendererType;
antialias?: boolean;
maxTextureSize?: number;
shaderHighp?: boolean;
gpu?: GPUInfo;
}
export interface PerformanceInfo {
fps: number;
worstFrameMs: number;
jankPercent: number;
throttlingLikely: boolean;
}
export interface PowerInfo {
charging?: boolean;
level?: string;
unavailable?: boolean;
}
export async function collectGraphicsDiagnostics(
canvas: HTMLCanvasElement,
): Promise<GraphicsDiagnostics> {
/* ---------- Browser / OS ---------- */
const uaData = (navigator as any).userAgentData;
const os = Platform.os;
const browser: BrowserInfo = {
engine: uaData?.brands
? uaData.brands.map((b: any) => b.brand).join(", ")
: navigator.userAgent,
platform: navigator.platform,
os,
dpr: window.devicePixelRatio,
};
/* ---------- Rendering ---------- */
let gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
let type: RendererType = "Canvas2D";
gl =
canvas.getContext("webgl2", { antialias: true }) ??
canvas.getContext("webgl", { antialias: true });
if (gl) {
const isWebGL2 =
typeof WebGL2RenderingContext !== "undefined" &&
gl instanceof WebGL2RenderingContext;
type = isWebGL2 ? "WebGL2" : "WebGL1";
}
const rendering: RenderingInfo = { type };
if (gl) {
rendering.antialias = gl.getContextAttributes()?.antialias ?? false;
rendering.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const precision = gl.getShaderPrecisionFormat(
gl.FRAGMENT_SHADER,
gl.HIGH_FLOAT,
);
rendering.shaderHighp = precision !== null && precision.precision > 0;
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
if (debugInfo) {
const renderer = gl.getParameter(
(debugInfo as any).UNMASKED_RENDERER_WEBGL,
) as string;
const vendor = gl.getParameter(
(debugInfo as any).UNMASKED_VENDOR_WEBGL,
) as string;
rendering.gpu = {
vendor,
renderer,
software: /swiftshader|llvmpipe|software/i.test(renderer),
};
} else {
rendering.gpu = { unavailable: true };
}
}
/* ---------- Power ---------- */
let power: PowerInfo = {};
if ("getBattery" in navigator) {
try {
const battery = await (navigator as any).getBattery();
power = {
charging: battery.charging,
level: Math.round(battery.level * 100) + "%",
};
} catch {
power = { unavailable: true };
}
} else {
power = { unavailable: true };
}
return {
browser,
rendering,
power,
};
}