Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539e62af7f | ||
|
|
2d5edf4651 | ||
|
|
358d6469cf | ||
|
|
b435230739 | ||
|
|
d3e7cd45a3 | ||
|
|
4799dd0251 | ||
|
|
28a433386e | ||
|
|
d2cc9780e7 | ||
|
|
a149dbdfab | ||
|
|
b40468f37c | ||
|
|
6bb5ce4db1 | ||
|
|
f6c4ecff5b | ||
|
|
ec95d18245 |
No files matched your search
@@ -12,6 +12,7 @@ coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
.next-qa/
|
||||
out/
|
||||
next-env.d.ts
|
||||
|
||||
|
||||
@@ -15,21 +15,32 @@ const withNextIntl = createNextIntlPlugin({
|
||||
requestConfig: "../../packages/translation/src/request.ts",
|
||||
});
|
||||
|
||||
const getDevelopmentServiceAliases = () => {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
const developmentServicePackages = ["@homarr/tasks", "@homarr/websocket"] as const;
|
||||
const isQaStandaloneBuild = process.env.HOMARR_QA_STANDALONE_BUILD === "true";
|
||||
|
||||
const getDevelopmentServiceAliases = (target: string) => {
|
||||
if (process.env.NODE_ENV !== "development" && !isQaStandaloneBuild) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
"@homarr/tasks": "./src/instrumentation-noop.ts",
|
||||
"@homarr/websocket": "./src/instrumentation-noop.ts",
|
||||
};
|
||||
return Object.fromEntries(developmentServicePackages.map((packageName) => [packageName, target]));
|
||||
};
|
||||
|
||||
const getDistDir = () => {
|
||||
const qaDistDir = process.env.HOMARR_QA_NEXT_DIST_DIR;
|
||||
if (!qaDistDir) return ".next";
|
||||
if (!/^\.next-qa\/(?:slot-[1-3]|release-v2-[0-9a-f]{40})$/u.test(qaDistDir)) {
|
||||
throw new Error("HOMARR_QA_NEXT_DIST_DIR must identify a QA slot or candidate-pinned release-v2 build");
|
||||
}
|
||||
return qaDistDir;
|
||||
};
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Next previews otherwise create agent instruction files in the application
|
||||
// directory during development.
|
||||
agentRules: false,
|
||||
// Release-v2 QA runs multiple isolated development servers from one worktree.
|
||||
distDir: getDistDir(),
|
||||
env: {
|
||||
HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown",
|
||||
},
|
||||
@@ -52,9 +63,23 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
turbopack: {
|
||||
root: path.resolve(import.meta.dirname, "../.."),
|
||||
// Development runs tasks and WebSocket as separate processes. These aliases
|
||||
// keep their production-only instrumentation imports out of the dev graph.
|
||||
resolveAlias: getDevelopmentServiceAliases(),
|
||||
// Development and the isolated QA standalone build do not embed tasks or WebSocket.
|
||||
// These aliases keep their production-only instrumentation imports out of those graphs.
|
||||
resolveAlias: getDevelopmentServiceAliases("./src/instrumentation-noop.ts"),
|
||||
},
|
||||
webpack(config, { dev, isServer }) {
|
||||
if ((!dev && !isQaStandaloneBuild) || !isServer) return config;
|
||||
|
||||
const instrumentationNoopPath = path.resolve(import.meta.dirname, "src/instrumentation-noop.ts");
|
||||
const aliases = getDevelopmentServiceAliases(instrumentationNoopPath);
|
||||
if (!aliases) return config;
|
||||
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
...aliases,
|
||||
"./instrumentation-node": instrumentationNoopPath,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
||||
images: {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Analytics } from "./analytics";
|
||||
|
||||
const postHogMock = vi.hoisted(() => {
|
||||
let resolveImport: (() => void) | undefined;
|
||||
const importGate = new Promise<void>((resolve) => {
|
||||
resolveImport = resolve;
|
||||
});
|
||||
|
||||
return {
|
||||
importGate,
|
||||
init: vi.fn(),
|
||||
releaseImport: () => resolveImport?.(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("posthog-js", async () => {
|
||||
await postHogMock.importGate;
|
||||
return { default: { init: postHogMock.init } };
|
||||
});
|
||||
|
||||
const host = document.createElement("div");
|
||||
const root = createRoot(host);
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
describe("Analytics", () => {
|
||||
it("deduplicates Strict Mode initialization and retries after a failure", async () => {
|
||||
const initializationError = new Error("initialization failed");
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
postHogMock.init.mockImplementationOnce(() => {
|
||||
throw initializationError;
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Analytics enabled />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
postHogMock.releaseImport();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(postHogMock.init).toHaveBeenCalledOnce();
|
||||
expect(consoleError).toHaveBeenCalledWith("PostHog initialization failed", initializationError);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Analytics enabled={false} />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Analytics enabled />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(postHogMock.init).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(consoleError).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -5,19 +5,36 @@ import { useEffect } from "react";
|
||||
const POSTHOG_KEY = "phc_vYBmGWNbRshvfeC7EHfeSmUm2pD2Neg5nGqzJuGvS8Hs";
|
||||
const POSTHOG_HOST = "https://hog.homarr.dev";
|
||||
|
||||
let postHogInitialization: Promise<void> | undefined;
|
||||
|
||||
const initializePostHog = () => {
|
||||
if (postHogInitialization) return;
|
||||
|
||||
const initializationAttempt = import("posthog-js").then(({ default: posthog }) => {
|
||||
posthog.init(POSTHOG_KEY, {
|
||||
api_host: POSTHOG_HOST,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
autocapture: false,
|
||||
persistence: "memory",
|
||||
});
|
||||
});
|
||||
|
||||
postHogInitialization = initializationAttempt;
|
||||
void initializationAttempt.catch((error: unknown) => {
|
||||
if (postHogInitialization === initializationAttempt) {
|
||||
postHogInitialization = undefined;
|
||||
}
|
||||
|
||||
console.error("PostHog initialization failed", error);
|
||||
});
|
||||
};
|
||||
|
||||
export const Analytics = ({ enabled }: { enabled: boolean }) => {
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
void import("posthog-js").then(({ default: posthog }) => {
|
||||
posthog.init(POSTHOG_KEY, {
|
||||
api_host: POSTHOG_HOST,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
autocapture: false,
|
||||
persistence: "memory",
|
||||
});
|
||||
});
|
||||
initializePostHog();
|
||||
}, [enabled]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
.mobileContent {
|
||||
display: none;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
width: 100%;
|
||||
@@ -103,8 +103,20 @@
|
||||
}
|
||||
|
||||
.mobileZone[data-zone="right"] {
|
||||
justify-content: flex-end;
|
||||
max-width: 72vw;
|
||||
justify-content: safe flex-end;
|
||||
width: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--mantine-color-dimmed) transparent;
|
||||
}
|
||||
|
||||
.mobileZone[data-zone="right"]::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: rem(6px);
|
||||
}
|
||||
|
||||
.mobileZone[data-zone="right"]::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
background: var(--mantine-color-dimmed);
|
||||
}
|
||||
|
||||
.floatingControls {
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
// next.config.ts keep production-only instrumentation imports out of the graph.
|
||||
export const instrumentationNoop = true;
|
||||
export const startupPromise = Promise.resolve();
|
||||
export const registerNodeInstrumentation = () => Promise.resolve();
|
||||
@@ -44,6 +44,12 @@
|
||||
"lint:fix": "turbo lint --continue -- --fix",
|
||||
"lint:ws": "pnpm dlx sherif@latest",
|
||||
"qa:browser-agent:ux": "bash scripts/browser-agent/ux-coherence-happy-paths.sh",
|
||||
"qa:release-v2:fixtures": "tsx scripts/release-v2-qa/fixture-server.mts",
|
||||
"qa:release-v2:report": "tsx scripts/release-v2-qa/report.mts",
|
||||
"qa:release-v2:runtime": "tsx scripts/release-v2-qa/runtime.mts",
|
||||
"qa:release-v2:seed": "tsx scripts/release-v2-qa/seed.mts",
|
||||
"qa:release-v2:verify": "tsx scripts/release-v2-qa/verify.mts",
|
||||
"qa:release-v2:zoom": "tsx scripts/release-v2-qa/zoom.mts",
|
||||
"release": "semantic-release",
|
||||
"scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts",
|
||||
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createI18nMiddleware } from "./middleware";
|
||||
|
||||
const createRewrittenRequest = (response: Response) => {
|
||||
const destination = response.headers.get("x-middleware-rewrite");
|
||||
const overriddenHeaderNames = response.headers.get("x-middleware-override-headers")?.split(",") ?? [];
|
||||
const headers = new Headers();
|
||||
|
||||
for (const name of overriddenHeaderNames) {
|
||||
const value = response.headers.get(`x-middleware-request-${name}`);
|
||||
if (value !== null) {
|
||||
headers.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (destination === null) {
|
||||
throw new Error("Expected middleware to rewrite the request");
|
||||
}
|
||||
|
||||
return new NextRequest(destination, { headers });
|
||||
};
|
||||
|
||||
describe("createI18nMiddleware", () => {
|
||||
it("canonicalizes a prefixed path when the locale header is supplied externally", () => {
|
||||
const middleware = createI18nMiddleware("en");
|
||||
const request = new NextRequest("http://localhost/en/auth/login", {
|
||||
headers: {
|
||||
cookie: "homarr.locale=fr",
|
||||
"x-next-intl-locale": "en",
|
||||
},
|
||||
});
|
||||
|
||||
const response = middleware(request);
|
||||
|
||||
expect(response.headers.get("x-middleware-next")).toBeNull();
|
||||
expect(response.headers.get("location")).toBe("http://localhost/auth/login");
|
||||
expect(response.headers.get("set-cookie")).toContain("homarr.locale=en");
|
||||
});
|
||||
|
||||
it("continues a valid internal locale handoff across middleware instances", () => {
|
||||
const firstPassMiddleware = createI18nMiddleware("en");
|
||||
const publicRequest = new NextRequest("http://localhost/auth/login");
|
||||
|
||||
const rewriteResponse = firstPassMiddleware(publicRequest);
|
||||
expect(rewriteResponse.headers.get("x-middleware-rewrite")).toBe("http://localhost/en/auth/login");
|
||||
|
||||
const localizedRequest = createRewrittenRequest(rewriteResponse);
|
||||
const secondPassMiddleware = createI18nMiddleware("en");
|
||||
const handoffResponse = secondPassMiddleware(localizedRequest);
|
||||
|
||||
expect(handoffResponse.headers.get("x-middleware-next")).toBe("1");
|
||||
expect(handoffResponse.headers.get("location")).toBeNull();
|
||||
expect(handoffResponse.headers.get("x-middleware-override-headers")).toBe("x-next-intl-locale");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "unsupported locale",
|
||||
pathname: "/en/auth/login",
|
||||
locale: "xx",
|
||||
},
|
||||
{
|
||||
name: "locale that does not match the path",
|
||||
pathname: "/fr/auth/login",
|
||||
locale: "en",
|
||||
},
|
||||
])("does not continue an internal handoff with a $name", ({ pathname, locale }) => {
|
||||
const middleware = createI18nMiddleware("en");
|
||||
const rewriteResponse = middleware(new NextRequest("http://localhost/auth/login"));
|
||||
const rewrittenRequest = createRewrittenRequest(rewriteResponse);
|
||||
rewrittenRequest.headers.set("x-next-intl-locale", locale);
|
||||
const request = new NextRequest(`http://localhost${pathname}`, {
|
||||
headers: rewrittenRequest.headers,
|
||||
});
|
||||
|
||||
const response = middleware(request);
|
||||
|
||||
expect(response.headers.get("x-middleware-next")).toBeNull();
|
||||
expect(response.headers.get("location")).toBe("http://localhost/auth/login");
|
||||
});
|
||||
|
||||
it("canonicalizes a directly requested prefixed path and synchronizes its locale", () => {
|
||||
const middleware = createI18nMiddleware("en");
|
||||
const request = new NextRequest("http://localhost/fr/auth/login");
|
||||
|
||||
const response = middleware(request);
|
||||
|
||||
expect(response.headers.get("x-middleware-next")).toBeNull();
|
||||
expect(response.headers.get("location")).toBe("http://localhost/auth/login");
|
||||
expect(response.headers.get("set-cookie")).toContain("homarr.locale=fr");
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,35 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import type { SupportedLanguage } from ".";
|
||||
import { supportedLanguages } from "./languages";
|
||||
import { createRouting } from "./routing";
|
||||
|
||||
export const createI18nMiddleware = (defaultLocale: SupportedLanguage) =>
|
||||
createMiddleware(createRouting(defaultLocale));
|
||||
// next-intl carries request headers through its rewrite. Keep the random value
|
||||
// module-private so a client-supplied locale header cannot impersonate that handoff.
|
||||
const localeHandoffHeader = "x-homarr-locale-handoff";
|
||||
const localeHandoffMarker = crypto.randomUUID();
|
||||
|
||||
export const createI18nMiddleware = (defaultLocale: SupportedLanguage) => {
|
||||
const middleware = createMiddleware(createRouting(defaultLocale));
|
||||
|
||||
return (request: NextRequest) => {
|
||||
const requestLocale = request.headers.get("x-next-intl-locale");
|
||||
const pathLocale = request.nextUrl.pathname.split("/")[1];
|
||||
const isSupportedLocale = supportedLanguages.includes(requestLocale as SupportedLanguage);
|
||||
const isInternalHandoff = request.headers.get(localeHandoffHeader) === localeHandoffMarker;
|
||||
|
||||
if (isInternalHandoff && isSupportedLocale && requestLocale === pathLocale) {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete(localeHandoffHeader);
|
||||
return NextResponse.next({ request: { headers } });
|
||||
}
|
||||
|
||||
request.headers.set(localeHandoffHeader, localeHandoffMarker);
|
||||
return middleware(request);
|
||||
};
|
||||
};
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 1px solid
|
||||
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
border: 1px solid rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
background-color: transparent;
|
||||
transition:
|
||||
border-color 120ms ease,
|
||||
@@ -14,9 +13,7 @@
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
background-color: rgb(
|
||||
from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14)
|
||||
);
|
||||
background-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -25,12 +22,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global(.mediaMissing-wrapper) :global(.mantine-SimpleGrid-root) > :global(.mantine-Paper-root) {
|
||||
.root :global(.mantine-SimpleGrid-root) > :global(.mantine-Paper-root) {
|
||||
border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:global(.mediaMissing-wrapper) :global(.mantine-Tabs-list) {
|
||||
.root :global(.mantine-Tabs-list) {
|
||||
--tab-border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
--tab-hover-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14));
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function MediaMissingWidget({
|
||||
);
|
||||
if (isAdvanced) {
|
||||
return (
|
||||
<Stack h="100%" gap={0}>
|
||||
<Stack className={classes.root} h="100%" gap={0}>
|
||||
{partialFailures}
|
||||
<SimpleGrid cols={enabledPanelCount} spacing="sm" p="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||
{showMissing && (
|
||||
@@ -197,6 +197,7 @@ export default function MediaMissingWidget({
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className={classes.root}
|
||||
value={activeTab}
|
||||
keepMounted={false}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatWeatherDate, WeatherDescription } from "./icon";
|
||||
import { getCompactWeatherLayout } from "./layout";
|
||||
|
||||
interface CompactWeatherProps {
|
||||
displayScale: number;
|
||||
height: number;
|
||||
isEditMode: boolean;
|
||||
options: WidgetProps<"weather">["options"];
|
||||
@@ -22,13 +23,13 @@ interface CompactWeatherProps {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const CompactWeather = ({ height, isEditMode, options, weather, width }: CompactWeatherProps) => {
|
||||
export const CompactWeather = ({ displayScale, height, isEditMode, options, weather, width }: CompactWeatherProps) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useI18n("widget.weather");
|
||||
const tCommon = useI18n("common");
|
||||
const layout = useMemo(
|
||||
() => getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount),
|
||||
[height, options.forecastDayCount, options.hasForecast, width],
|
||||
() => getCompactWeatherLayout(width, height, options.hasForecast, options.forecastDayCount, displayScale),
|
||||
[displayScale, height, options.forecastDayCount, options.hasForecast, width],
|
||||
);
|
||||
const today = weather.daily[0];
|
||||
const speedUnit = options.useImperialSpeed
|
||||
|
||||
@@ -53,6 +53,18 @@ describe("weather layout", () => {
|
||||
expect(getCompactWeatherLayout(640, 360, false, 7).forecastDays).toBe(0);
|
||||
});
|
||||
|
||||
it("uses the rendered dimensions when a responsive board scales the widget", () => {
|
||||
expect(getCompactWeatherLayout(320, 180, true, 7).tier).toBe("standard");
|
||||
expect(getCompactWeatherLayout(320, 180, true, 7, 0.5)).toEqual({
|
||||
tier: "micro",
|
||||
forecastDays: 0,
|
||||
showCity: false,
|
||||
showCondition: false,
|
||||
showHighLow: false,
|
||||
showSecondary: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("adapts the advanced view to mobile and desktop surfaces", () => {
|
||||
expect(getAdvancedWeatherLayout(374, 624)).toMatchObject({
|
||||
dailyColumns: 2,
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function WeatherWidget({
|
||||
width,
|
||||
height,
|
||||
displayMode,
|
||||
displayScale = 1,
|
||||
}: WidgetComponentProps<"weather">) {
|
||||
const input = {
|
||||
latitude: options.location.latitude,
|
||||
@@ -35,7 +36,14 @@ export default function WeatherWidget({
|
||||
{displayMode === "advanced" ? (
|
||||
<AdvancedWeather height={height} options={options} weather={weather} width={width} />
|
||||
) : (
|
||||
<CompactWeather height={height} isEditMode={isEditMode} options={options} weather={weather} width={width} />
|
||||
<CompactWeather
|
||||
displayScale={displayScale}
|
||||
height={height}
|
||||
isEditMode={isEditMode}
|
||||
options={options}
|
||||
weather={weather}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -21,9 +21,17 @@ export const getCompactWeatherLayout = (
|
||||
height: number,
|
||||
hasForecast: boolean,
|
||||
configuredDays: number,
|
||||
displayScale = 1,
|
||||
): CompactWeatherLayout => {
|
||||
let responsiveWidth = width;
|
||||
let responsiveHeight = height;
|
||||
if (Number.isFinite(displayScale) && displayScale > 0) {
|
||||
responsiveWidth *= displayScale;
|
||||
responsiveHeight *= displayScale;
|
||||
}
|
||||
|
||||
const breakpoint = compactWeatherBreakpoints.find(
|
||||
({ minHeight, minWidth }) => width >= minWidth && height >= minHeight,
|
||||
({ minHeight, minWidth }) => responsiveWidth >= minWidth && responsiveHeight >= minHeight,
|
||||
);
|
||||
const tier = breakpoint?.tier ?? "micro";
|
||||
|
||||
@@ -41,15 +49,15 @@ export const getCompactWeatherLayout = (
|
||||
if (tier === "compact") {
|
||||
return {
|
||||
tier: "compact",
|
||||
forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(width / 92))) : 0,
|
||||
showCity: width >= 220,
|
||||
forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(responsiveWidth / 92))) : 0,
|
||||
showCity: responsiveWidth >= 220,
|
||||
showCondition: true,
|
||||
showHighLow: true,
|
||||
showSecondary: false,
|
||||
};
|
||||
}
|
||||
|
||||
const availableForecastWidth = Math.max(0, width - 24);
|
||||
const availableForecastWidth = Math.max(0, responsiveWidth - 24);
|
||||
return {
|
||||
tier,
|
||||
forecastDays: hasForecast ? Math.min(configuredDays, Math.max(1, Math.floor(availableForecastWidth / 64))) : 0,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Release-v2 dogfood QA
|
||||
|
||||
This directory is the fixed coverage contract for the release-v2 browser dogfood campaign. The candidate is always the checkout's exact `HEAD` commit when the harness runs. `coverage-manifest.json` owns the matrix and agent assignments; `ledger.json` and `master-report.md` are generated from the tracked reports in `reports/<agent-id>/report.md`.
|
||||
|
||||
A dirty pre-commit checkout can be used for focused harness checks, but its runtime provenance still identifies the underlying `HEAD` and does not claim that uncommitted files belong to that SHA. Start fresh campaign instances after the campaign changes are committed so runtime, fixture, coverage, ledger, and report metadata all record the exact final commit.
|
||||
|
||||
## Required commands
|
||||
|
||||
Use Node 24 and the repository toolchain. These package commands are the supported entry points:
|
||||
|
||||
```bash
|
||||
pnpm qa:release-v2:fixtures
|
||||
pnpm qa:release-v2:seed -- --profile <profile> --output <directory> [--reset]
|
||||
pnpm qa:release-v2:verify
|
||||
pnpm qa:release-v2:report
|
||||
pnpm qa:release-v2:zoom -- --origin <app-origin> --factor <0.8|1|1.25|2> --output <new-absolute-directory>
|
||||
```
|
||||
|
||||
The fixture command binds to an explicit loopback host on an automatically allocated port and prints only its non-secret readiness metadata. It accepts `127.0.0.1`, `localhost`, and IPv6 loopback origins such as `[::1]`, while rejecting wildcard and non-loopback hosts. Pass `--ready-file <path>` when another process needs to consume the selected URL. Direct seeding also requires an isolated migrated SQLite `DB_URL`, a credential-free loopback HTTP origin in `QA_FIXTURE_URL`, and a password for populated profiles. Candidate provenance is derived from the actual checkout; optional `--candidate-sha` or `QA_CANDIDATE_SHA` input is accepted only when it exactly matches `HEAD`. Supply the password through `QA_PASSWORD_FILE` where possible; the file content is never written to a manifest or command output, and neither password environment variable is forwarded to the spawned Next.js process after seeding. `onboarding-fresh` does not require a password.
|
||||
|
||||
Each populated fixture manifest keeps the human-readable persona label in `personas[].name` and exposes the non-secret credentials username in `personas[].loginUsername`. Board route names are deterministic, unique, and validation-safe in `boards[].name`; shared board names exactly match the IDs in `coverage-manifest.json` (including `qa-scroll-lab`), while the human-readable title remains in `boards[].label` and the board page/meta titles. The login username is the deterministic lowercase persona handle; the password remains available only through `QA_PASSWORD_FILE`.
|
||||
|
||||
`scripts/release-v2-qa/permissions.mts` explicitly maps all 45 packet persona/board combinations to `none`, `view`, `modify`, or `full`. The seeder collapses repeated assignments to the highest access required by the campaign and writes only the resulting direct fixture-board grants; administrators and board owners rely on their existing implicit full access. The fixture manifest records both `packetBoardAccess` and the collapsed `expectedBoardAccess`. Live verification compares the exact direct rows and rejects unrelated grants, Nolan protected-board access, or Vivian edit access. Any access-contract change must update the packet mapping and its focused spec together.
|
||||
|
||||
Generated placements are validated before the seed transaction commits. Root items and containers must fit the active main/left/right lane after gutters, nested containers must fit their parent content grid without cycles, and every item must fit its selected root or container in every layout. `qa-grid-24` includes a dedicated unguttered 24-column custom layout that preserves exact width coverage `1–24`; its guttered Base layout and all narrower layouts keep lane-valid projected widths.
|
||||
|
||||
Use `report -- --init` to create missing agent reports. Use `report -- --refresh-placeholders` after changing manifest templates or rolling the checkout candidate. It refreshes only safe, untouched `not-reached` placeholders, reports refreshed and protected-skip counts, and preserves reports with execution or case results, widget results, findings, artifacts, measurements, limitations, or reproduction data byte-for-byte. Malformed reports are protected skips rather than overwrite candidates.
|
||||
|
||||
When a release-blocking defect has been fixed and downstream packets contain no feature result beyond that one blocker, explicitly roll only those reports to fresh `not-reached` templates with `report -- --rollover-resolved-blocker <finding-fingerprint>`. The rollover rejects reports with another finding, passed or failed feature cases or widgets, performance measurements, malformed metadata, or a concurrent content/mtime change. It never scans report directories outside the 45 manifest packets, so independent reproducer reports remain untouched.
|
||||
|
||||
## Managed runtime
|
||||
|
||||
The runtime command provisions Redis, the deterministic fixture server, the isolated SQLite database, seed data, and Next.js:
|
||||
|
||||
```bash
|
||||
QA_PASSWORD_FILE=/secure/path pnpm qa:release-v2:runtime -- start --slot 1 --profile main-writable
|
||||
pnpm qa:release-v2:runtime -- status --slot 1
|
||||
pnpm qa:release-v2:runtime -- stop --slot 1
|
||||
```
|
||||
|
||||
Slots `1`, `2`, and `3` have separate database files, Redis containers and ports, cookie prefixes, logs, fixture ports, and app ports. They reuse one immutable candidate-pinned production standalone build under `.next-qa/release-v2-<candidate-sha>`, so three active profiles do not each retain a multi-gigabyte development compiler. The runtime manifest is written only after the standalone app is ready and records the exact shared build and server path. Status checks the owned processes, app, fixture, and Redis. Stop is idempotent for a stopped manifest and refuses to signal a live PID that does not carry the recorded QA run ID.
|
||||
|
||||
Every start, reset, and stop holds an exclusive per-slot filesystem lease outside the resettable slot tree. A second operation on the same slot fails deterministically. Stale recovery compares both the recorded PID and its operating-system process-start marker; it never signals a lease owner or an unrelated process. An incomplete fresh lease is preserved for a short grace period instead of being guessed stale.
|
||||
|
||||
The fixture child receives an explicit environment allowlist containing only its non-secret QA run ID; ports and ready-file paths are command arguments. Password, database, authentication, encryption, API, and unrelated inherited environment values are not forwarded. The app keeps its required runtime environment but never receives `QA_PASSWORD` or `QA_PASSWORD_FILE` after seeding.
|
||||
|
||||
The selected app port remains bound by a temporary loopback reservation through migration and seeding. The reservation is released immediately before Next.js binds. If another process wins that unavoidable release-to-bind window and Next.js reports `EADDRINUSE`, startup retries with a newly reserved, dynamically selected port. No application port is hard-coded.
|
||||
|
||||
`--reset` is intentionally narrow: after validating the QA run-root marker, it stops only the selected running manifest, removes only that slot directory, and clears only the selected slot's legacy development cache. It never removes the shared candidate build while another slot may use it. The run root must be a direct child of the canonical operating-system temporary directory returned by Node, its basename must be `homarr-release-v2-qa` or start with `homarr-release-v2-qa-`, and only safe suffix characters are accepted. Temporary-directory aliases are canonicalized before containment checks. Existing run roots and slot paths must not be symlinks or contain symlink components, and destructive paths are rechecked immediately before mutation. The Linux default `/tmp/homarr-release-v2-qa` remains unchanged.
|
||||
|
||||
This host's `fs.inotify.max_user_instances=128` is too low for concurrent development watchers, and three Webpack development compilers exceed the available memory. The runtime therefore creates one candidate-pinned production standalone build with `next build --webpack` and reuses it across the three isolated slots. The shared build has no watcher or HMR surface, eliminating stale development chunks and keeping each running app lightweight. Source changes require a commit and a new candidate build before browser evidence is valid. Provision slots sequentially when the shared build does not exist yet; once it is present, all three may run concurrently. The runtime manifest records `runtimeMode: "production-standalone"`, `bundler: "webpack"`, `watcher: null`, and the exact build metadata; live verification rejects another contract. Existing development slots must be restarted before they satisfy it.
|
||||
|
||||
## Profiles
|
||||
|
||||
| Profile | `DEMO_MODE` | `DEMO_READ_ONLY` | `UNSAFE_ENABLE_MOCK_INTEGRATION` |
|
||||
| ------------------ | ----------- | ---------------- | -------------------------------- |
|
||||
| `main-writable` | `true` | `false` | `true` |
|
||||
| `main-readonly` | `true` | `true` | `true` |
|
||||
| `onboarding-fresh` | `false` | `false` | `true` |
|
||||
| `degraded` | `true` | `false` | `true` |
|
||||
|
||||
The verifier enforces this exact matrix. For a live check, pass either a run-root or one or more runtime/fixture manifests:
|
||||
|
||||
```bash
|
||||
pnpm qa:release-v2:verify -- --run-root /tmp/homarr-release-v2-qa
|
||||
pnpm qa:release-v2:verify -- --manifest /tmp/homarr-release-v2-qa/slots/1/runtime-manifest.json
|
||||
```
|
||||
|
||||
Live verification checks the checkout-derived candidate, profile flags, paths, counts, personas, board names against both the fixture and coverage manifests, board-name validity and uniqueness, layouts, lane and recursive container geometry, item placements, board permissions, fixture health, app readiness, and reset-safety invariants. The default command remains a static coverage/report check and needs no running services.
|
||||
|
||||
## Evidence contract
|
||||
|
||||
Each base case ID is expanded through its wave's `caseDimensions`. Allowed statuses are `passed`, `failed`, `blocked`, and `not-reached`; never turn an unrun check into a pass. Every failed, blocked, or not-reached case or assigned widget check is a critical gap.
|
||||
|
||||
Every widget is assigned all nine required viewports. The high-risk widget list must exercise every width `1–24` × every height `1–6` at mobile, breakpoint-edge, and desktop viewports. Other widgets must cover minimum, canonical, wide, tall, maximum, overflow, and behavior-changing thresholds. Keep structured `widgetChecks` and the matching human rows synchronized.
|
||||
|
||||
Execution metadata always carries the exact checkout candidate SHA. URL, actual port, runtime flags/profile, persona, browser session ID, timestamp, viewport, input, and zoom may remain unset only while execution is not reached. Record performance measurements and limitations structurally, and attach independent reproduction outcomes to finding fingerprints without inventing results.
|
||||
|
||||
Screenshots and traces stay untracked under `.screenshots/release-v2/<agent-id>/`. Reports must link absolute local artifact paths. Never copy passwords, credentials, cookies, tokens, secret-bearing response bodies, or environment dumps into evidence.
|
||||
|
||||
The decision is `GO` only when there are no P0/P1 findings and no critical gaps; otherwise it is `NO-GO`. The report command itself treats missing, null, malformed, wrong-candidate, incomplete, assignment-inconsistent, or case-rollup-inconsistent metadata as an explicit critical metadata gap. It does not depend on a separate verification run to force `NO-GO`.
|
||||
|
||||
Aggregation refuses symlinked packet reports, `ledger.json`, and `master-report.md`. Primary report artifacts plus finding and performance evidence must be existing regular files under the packet's assigned `.screenshots/release-v2/<agent-id>/` directory. Independent reproduction evidence may use an existing regular file from any agent directory under the canonical `.screenshots/release-v2/` root. Every artifact path must be absolute and symlink-free; paths that escape the applicable root are rejected.
|
||||
|
||||
## Native browser zoom
|
||||
|
||||
The zoom helper creates a disposable Manifest V3 extension containing only `manifest.json` and `background.js`. Its host permission and runtime URL check are restricted to the exact supplied HTTP(S) origin. The factor whitelist is `0.8`, `1`, `1.25`, and `2`; factor `1` is supported as a deterministic extension baseline, though an unmodified browser session needs no extension for the same 100% baseline. The output must be a new direct child of the canonical operating-system temporary directory named `homarr-release-v2-qa-zoom-<unique>`. The helper canonicalizes temporary-directory aliases, never removes or resets an existing path, and never accepts credentials in the origin.
|
||||
|
||||
Generate one extension per factor, then launch it in a fresh session and profile:
|
||||
|
||||
```bash
|
||||
pnpm qa:release-v2:zoom -- \
|
||||
--origin http://127.0.0.1:<actual-port> \
|
||||
--factor 1.25 \
|
||||
--output /tmp/homarr-release-v2-qa-zoom-125-<unique>
|
||||
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session release-v2-zoom-125-<unique> \
|
||||
--profile /tmp/homarr-release-v2-qa-zoom-profile-125-<unique> \
|
||||
--extension /tmp/homarr-release-v2-qa-zoom-125-<unique> \
|
||||
--headed false \
|
||||
--args '--no-sandbox,--headless=new' \
|
||||
open http://127.0.0.1:<actual-port>
|
||||
```
|
||||
|
||||
Keep the session and profile unique for every factor. Restart the session when changing factors. Native browser zoom must preserve `window.visualViewport.scale === 1`; record the actual CSS viewport and `devicePixelRatio` alongside that invariant. A changed `visualViewport.scale` indicates page scaling rather than native browser zoom and must not be reported as zoom coverage.
|
||||
@@ -0,0 +1,922 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"browser": "Chromium",
|
||||
"artifactRoot": ".screenshots/release-v2",
|
||||
"statuses": ["passed", "failed", "blocked", "not-reached"],
|
||||
"severities": ["P0", "P1", "P2", "P3"],
|
||||
"pullRequests": [
|
||||
{ "number": 6356, "label": "Custom Widgets" },
|
||||
{ "number": 6503, "label": "dnd-kit grid" },
|
||||
{ "number": 6502, "label": "widget modernization" },
|
||||
{ "number": 6450, "label": "responsive layouts" },
|
||||
{ "number": 6482, "label": "Assistant" },
|
||||
{ "number": 6555, "label": "widget performance" },
|
||||
{ "number": 6569, "label": "onboarding" },
|
||||
{ "number": 6545, "label": "release-v2 rollup" }
|
||||
],
|
||||
"viewports": [
|
||||
{ "id": "mobile-320", "width": 320, "height": 568 },
|
||||
{ "id": "mobile-375", "width": 375, "height": 812 },
|
||||
{ "id": "boundary-767", "width": 767, "height": 900 },
|
||||
{ "id": "boundary-768", "width": 768, "height": 900 },
|
||||
{ "id": "tablet-1024", "width": 1024, "height": 768 },
|
||||
{ "id": "boundary-1279", "width": 1279, "height": 900 },
|
||||
{ "id": "desktop-1280", "width": 1280, "height": 900 },
|
||||
{ "id": "desktop-1440", "width": 1440, "height": 900 },
|
||||
{ "id": "desktop-1920", "width": 1920, "height": 1080 }
|
||||
],
|
||||
"zooms": [80, 100, 125, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"profiles": [
|
||||
{ "id": "main-writable", "description": "Authenticated writable release candidate" },
|
||||
{ "id": "main-readonly", "description": "Authenticated read-only release candidate" },
|
||||
{ "id": "onboarding-fresh", "description": "Fresh installation at onboarding" },
|
||||
{ "id": "degraded", "description": "Fixture-backed error, empty, and latency states" }
|
||||
],
|
||||
"personas": [
|
||||
"Avery Admin",
|
||||
"Rowan Owner",
|
||||
"Eden Editor",
|
||||
"Vivian Viewer",
|
||||
"Nolan Outsider",
|
||||
"Morgan Mobile",
|
||||
"Casey Chaos",
|
||||
"Ingrid Infra",
|
||||
"Maya Media",
|
||||
"Brooke Minimalist",
|
||||
"Cora Creator",
|
||||
"Ash Assistant",
|
||||
"Kira Keyboard",
|
||||
"Nora Newcomer"
|
||||
],
|
||||
"boards": [
|
||||
{ "id": "qa-grid-24", "handle": "grid-24" },
|
||||
{ "id": "qa-scroll-lab", "handle": "scroll" },
|
||||
{ "id": "qa-dense-collisions", "handle": "dense-collisions" },
|
||||
{ "id": "qa-nested-containers", "handle": "nested-containers" },
|
||||
{ "id": "qa-layout-boundaries", "handle": "layout-boundaries" },
|
||||
{ "id": "qa-icons-bookmarks", "handle": "icons-bookmarks" },
|
||||
{ "id": "qa-widgets-01", "handle": "widgets-01" },
|
||||
{ "id": "qa-widgets-02", "handle": "widgets-02" },
|
||||
{ "id": "qa-widgets-03", "handle": "widgets-03" },
|
||||
{ "id": "qa-widgets-04", "handle": "widgets-04" },
|
||||
{ "id": "qa-widgets-05", "handle": "widgets-05" },
|
||||
{ "id": "qa-widgets-06", "handle": "widgets-06" },
|
||||
{ "id": "qa-widgets-07", "handle": "widgets-07" },
|
||||
{ "id": "qa-widgets-08", "handle": "widgets-08" },
|
||||
{ "id": "qa-widgets-09", "handle": "widgets-09" },
|
||||
{ "id": "qa-widgets-10", "handle": "widgets-10" },
|
||||
{ "id": "qa-widgets-11", "handle": "widgets-11" },
|
||||
{ "id": "qa-widgets-12", "handle": "widgets-12" },
|
||||
{ "id": "qa-custom-widget-assistant", "handle": "custom-widget-assistant" },
|
||||
{ "id": "qa-permissions-public", "handle": "permissions-public" },
|
||||
{ "id": "qa-download-upload", "handle": "download-upload" }
|
||||
],
|
||||
"wavePacketCounts": { "preflight": 3, "board": 9, "widgets": 12, "core-v2": 8, "whole-product": 9, "performance": 4 },
|
||||
"caseDimensions": {
|
||||
"preflight": [
|
||||
{
|
||||
"id": "ENVIRONMENT",
|
||||
"expected": "Verify candidate identity, Chromium session, runtime health, fixture reachability, and clean console/network baselines."
|
||||
},
|
||||
{
|
||||
"id": "ACCESS",
|
||||
"expected": "Verify assigned profiles, personas, boards, permissions, and read-only boundaries before mutations."
|
||||
},
|
||||
{
|
||||
"id": "EVIDENCE",
|
||||
"expected": "Capture timestamped viewport/input/zoom evidence and confirm artifacts contain no secrets."
|
||||
}
|
||||
],
|
||||
"board": [
|
||||
{
|
||||
"id": "THRESHOLDS",
|
||||
"expected": "Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress."
|
||||
},
|
||||
{
|
||||
"id": "MUTATION",
|
||||
"expected": "Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet."
|
||||
},
|
||||
{
|
||||
"id": "PERSISTENCE",
|
||||
"expected": "Reload, switch layouts or sessions, and verify saved and reset state remains correct."
|
||||
},
|
||||
{
|
||||
"id": "ACCESS-RECOVERY",
|
||||
"expected": "Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies."
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "RENDER-SIZE",
|
||||
"expected": "Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported."
|
||||
},
|
||||
{
|
||||
"id": "STATES-RECOVERY",
|
||||
"expected": "Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind."
|
||||
},
|
||||
{
|
||||
"id": "OPTIONS-PERSISTENCE",
|
||||
"expected": "Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly."
|
||||
},
|
||||
{
|
||||
"id": "ACCESS-READONLY",
|
||||
"expected": "Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind."
|
||||
},
|
||||
{
|
||||
"id": "EVIDENCE",
|
||||
"expected": "Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row."
|
||||
}
|
||||
],
|
||||
"core-v2": [
|
||||
{
|
||||
"id": "HAPPY-PATH",
|
||||
"expected": "Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies."
|
||||
},
|
||||
{
|
||||
"id": "MUTATION-PERSISTENCE",
|
||||
"expected": "Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions."
|
||||
},
|
||||
{
|
||||
"id": "ACCESSIBILITY-ACCESS",
|
||||
"expected": "Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement."
|
||||
},
|
||||
{
|
||||
"id": "DEGRADED-RECOVERY",
|
||||
"expected": "Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior."
|
||||
}
|
||||
],
|
||||
"whole-product": [
|
||||
{
|
||||
"id": "JOURNEY",
|
||||
"expected": "Complete the persona journey across all assigned boards and profiles without bypassing product navigation."
|
||||
},
|
||||
{
|
||||
"id": "THRESHOLDS",
|
||||
"expected": "Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus."
|
||||
},
|
||||
{
|
||||
"id": "PERSISTENCE-ACCESS",
|
||||
"expected": "Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact."
|
||||
},
|
||||
{
|
||||
"id": "FAILURE-EVIDENCE",
|
||||
"expected": "Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence."
|
||||
}
|
||||
],
|
||||
"performance": [
|
||||
{
|
||||
"id": "BASELINE",
|
||||
"expected": "Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata."
|
||||
},
|
||||
{
|
||||
"id": "THRESHOLD",
|
||||
"expected": "Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold."
|
||||
},
|
||||
{
|
||||
"id": "STRESS",
|
||||
"expected": "Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies."
|
||||
},
|
||||
{
|
||||
"id": "RECOVERY-EVIDENCE",
|
||||
"expected": "Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts."
|
||||
}
|
||||
]
|
||||
},
|
||||
"packets": [
|
||||
{
|
||||
"id": "preflight-01",
|
||||
"wave": "preflight",
|
||||
"title": "Candidate identity and services",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Avery Admin"],
|
||||
"boards": [],
|
||||
"profiles": ["main-writable", "main-readonly", "onboarding-fresh", "degraded"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["PF-001"]
|
||||
},
|
||||
{
|
||||
"id": "preflight-02",
|
||||
"wave": "preflight",
|
||||
"title": "Fixture and persona access",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Avery Admin", "Rowan Owner", "Vivian Viewer", "Nolan Outsider"],
|
||||
"boards": ["qa-grid-24", "qa-permissions-public"],
|
||||
"profiles": ["main-writable", "main-readonly"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["PF-002"]
|
||||
},
|
||||
{
|
||||
"id": "preflight-03",
|
||||
"wave": "preflight",
|
||||
"title": "Browser evidence hygiene",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Casey Chaos"],
|
||||
"boards": [],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-320", "desktop-1920"],
|
||||
"zooms": [80, 100, 125, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["PF-003"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "board-01",
|
||||
"wave": "board",
|
||||
"title": "24-column placement and resize",
|
||||
"prRefs": [6503],
|
||||
"personas": ["Rowan Owner"],
|
||||
"boards": ["qa-grid-24"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["desktop-1440", "desktop-1920"],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["BD-001"]
|
||||
},
|
||||
{
|
||||
"id": "board-02",
|
||||
"wave": "board",
|
||||
"title": "Scrollable canvas and collapse states",
|
||||
"prRefs": [6503],
|
||||
"personas": ["Eden Editor"],
|
||||
"boards": ["qa-scroll-lab"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-375", "desktop-1280"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch"],
|
||||
"cases": ["BD-002"]
|
||||
},
|
||||
{
|
||||
"id": "board-03",
|
||||
"wave": "board",
|
||||
"title": "Dense collision handling",
|
||||
"prRefs": [6503],
|
||||
"personas": ["Casey Chaos"],
|
||||
"boards": ["qa-dense-collisions"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["tablet-1024", "desktop-1440"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["BD-003"]
|
||||
},
|
||||
{
|
||||
"id": "board-04",
|
||||
"wave": "board",
|
||||
"title": "Nested containers",
|
||||
"prRefs": [6503],
|
||||
"personas": ["Rowan Owner"],
|
||||
"boards": ["qa-nested-containers"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch"],
|
||||
"cases": ["BD-004"]
|
||||
},
|
||||
{
|
||||
"id": "board-05",
|
||||
"wave": "board",
|
||||
"title": "Responsive layout boundaries",
|
||||
"prRefs": [6450],
|
||||
"personas": ["Morgan Mobile"],
|
||||
"boards": ["qa-layout-boundaries"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["boundary-767", "boundary-768", "boundary-1279", "desktop-1280"],
|
||||
"zooms": [100],
|
||||
"inputs": ["touch", "keyboard"],
|
||||
"cases": ["BD-005"]
|
||||
},
|
||||
{
|
||||
"id": "board-06",
|
||||
"wave": "board",
|
||||
"title": "Icons, bookmarks, and compact layout",
|
||||
"prRefs": [6502],
|
||||
"personas": ["Brooke Minimalist"],
|
||||
"boards": ["qa-icons-bookmarks"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-375", "desktop-1440"],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "touch"],
|
||||
"cases": ["BD-006"]
|
||||
},
|
||||
{
|
||||
"id": "board-07",
|
||||
"wave": "board",
|
||||
"title": "Permission-aware editing",
|
||||
"prRefs": [6503, 6545],
|
||||
"personas": ["Rowan Owner", "Eden Editor", "Vivian Viewer", "Nolan Outsider"],
|
||||
"boards": ["qa-permissions-public"],
|
||||
"profiles": ["main-writable", "main-readonly"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["BD-007"]
|
||||
},
|
||||
{
|
||||
"id": "board-08",
|
||||
"wave": "board",
|
||||
"title": "Board import and export",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Avery Admin"],
|
||||
"boards": ["qa-download-upload"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["desktop-1280"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["BD-008"]
|
||||
},
|
||||
{
|
||||
"id": "board-09",
|
||||
"wave": "board",
|
||||
"title": "Keyboard-only grid operation",
|
||||
"prRefs": [6503, 6450],
|
||||
"personas": ["Kira Keyboard"],
|
||||
"boards": ["qa-grid-24"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["keyboard"],
|
||||
"cases": ["BD-009"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "widgets-01",
|
||||
"wave": "widgets",
|
||||
"title": "Time and environment",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Brooke Minimalist"],
|
||||
"boards": ["qa-widgets-01"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["clock", "weather", "airQuality", "countdown", "timer"],
|
||||
"cases": ["WG-001"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-02",
|
||||
"wave": "widgets",
|
||||
"title": "Apps, embeds, video, game status, and stocks",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Cora Creator"],
|
||||
"boards": ["qa-widgets-02"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["app", "iframe", "video", "minecraftServerStatus", "stockPrice"],
|
||||
"cases": ["WG-002"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-03",
|
||||
"wave": "widgets",
|
||||
"title": "Notes, bookmarks, feeds, and timetable",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Cora Creator"],
|
||||
"boards": ["qa-widgets-03"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["notebook", "anchorNote", "bookmarks", "rssFeed", "timetable"],
|
||||
"cases": ["WG-003"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-04",
|
||||
"wave": "widgets",
|
||||
"title": "Downloads, containers, indexers, and DNS",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-04"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["downloads", "dockerContainers", "indexerManager", "dnsHoleSummary", "dnsHoleControls"],
|
||||
"cases": ["WG-004"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-05",
|
||||
"wave": "widgets",
|
||||
"title": "Smart home, health, and system telemetry",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-05"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": [
|
||||
"smartHome-entityState",
|
||||
"smartHome-executeAutomation",
|
||||
"healthMonitoring",
|
||||
"systemResources",
|
||||
"systemDisks"
|
||||
],
|
||||
"cases": ["WG-005"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-06",
|
||||
"wave": "widgets",
|
||||
"title": "Network availability and operations",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-06"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["firewall", "notifications", "networkControllerSummary", "networkControllerStatus", "uptimeKuma"],
|
||||
"cases": ["WG-006"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-07",
|
||||
"wave": "widgets",
|
||||
"title": "Beszel and update monitoring",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-07"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["beszelSystemTable", "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", "wud"],
|
||||
"cases": ["WG-007"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-08",
|
||||
"wave": "widgets",
|
||||
"title": "Power, VPN, speed, routing, and analytics",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-08"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["ups", "vpn", "speedtestTracker", "traefik", "umami"],
|
||||
"cases": ["WG-008"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-09",
|
||||
"wave": "widgets",
|
||||
"title": "Media overview and requests",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Maya Media"],
|
||||
"boards": ["qa-widgets-09"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": [
|
||||
"calendar",
|
||||
"mediaServer",
|
||||
"mediaRequests-requestList",
|
||||
"mediaRequests-requestStats",
|
||||
"mediaMissing"
|
||||
],
|
||||
"cases": ["WG-009"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-10",
|
||||
"wave": "widgets",
|
||||
"title": "Media activity, Immich, and audio",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Maya Media"],
|
||||
"boards": ["qa-widgets-10"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["mediaReleases", "mediaTranscoding", "immich-serverStats", "immich-albumCarousel", "audioStats"],
|
||||
"cases": ["WG-010"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-11",
|
||||
"wave": "widgets",
|
||||
"title": "Documents, patching, media services, and releases",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-11"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["paperlessNgx", "patchmon", "bazarr", "tracearr", "releases"],
|
||||
"cases": ["WG-011"]
|
||||
},
|
||||
{
|
||||
"id": "widgets-12",
|
||||
"wave": "widgets",
|
||||
"title": "Coolify, ArchiveTeam, Custom API, and Assistant",
|
||||
"prRefs": [6502, 6482, 6555],
|
||||
"personas": ["Ash Assistant", "Cora Creator"],
|
||||
"boards": ["qa-widgets-12", "qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": [
|
||||
"mobile-320",
|
||||
"mobile-375",
|
||||
"boundary-767",
|
||||
"boundary-768",
|
||||
"tablet-1024",
|
||||
"boundary-1279",
|
||||
"desktop-1280",
|
||||
"desktop-1440",
|
||||
"desktop-1920"
|
||||
],
|
||||
"zooms": [80, 100, 125, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"widgetKinds": ["coolify", "archiveTeamWarrior", "customApi", "assistant"],
|
||||
"cases": ["WG-012"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "core-v2-01",
|
||||
"wave": "core-v2",
|
||||
"title": "Custom widget authoring",
|
||||
"prRefs": [6356],
|
||||
"personas": ["Cora Creator"],
|
||||
"boards": ["qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["CV-001"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-02",
|
||||
"wave": "core-v2",
|
||||
"title": "Assistant tool flow",
|
||||
"prRefs": [6482],
|
||||
"personas": ["Ash Assistant"],
|
||||
"boards": ["qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["mobile-375", "desktop-1440"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-002"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-03",
|
||||
"wave": "core-v2",
|
||||
"title": "Onboarding happy path",
|
||||
"prRefs": [6569],
|
||||
"personas": ["Nora Newcomer"],
|
||||
"boards": [],
|
||||
"profiles": ["onboarding-fresh"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-003"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-04",
|
||||
"wave": "core-v2",
|
||||
"title": "Authentication and session transitions",
|
||||
"prRefs": [6545, 6569],
|
||||
"personas": ["Avery Admin", "Nolan Outsider"],
|
||||
"boards": ["qa-permissions-public"],
|
||||
"profiles": ["main-writable", "main-readonly", "onboarding-fresh"],
|
||||
"viewports": ["mobile-375", "desktop-1280"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-004"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-05",
|
||||
"wave": "core-v2",
|
||||
"title": "Integration management",
|
||||
"prRefs": [6502, 6545],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-07"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["CV-005"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-06",
|
||||
"wave": "core-v2",
|
||||
"title": "Search, menus, and dialogs",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Kira Keyboard"],
|
||||
"boards": ["qa-grid-24"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-006"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-07",
|
||||
"wave": "core-v2",
|
||||
"title": "Read-only enforcement",
|
||||
"prRefs": [6356, 6482, 6545],
|
||||
"personas": ["Vivian Viewer"],
|
||||
"boards": ["qa-custom-widget-assistant", "qa-permissions-public"],
|
||||
"profiles": ["main-readonly"],
|
||||
"viewports": ["mobile-375", "desktop-1280"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-007"]
|
||||
},
|
||||
{
|
||||
"id": "core-v2-08",
|
||||
"wave": "core-v2",
|
||||
"title": "Failure and recovery states",
|
||||
"prRefs": [6356, 6482, 6502, 6545],
|
||||
"personas": ["Casey Chaos"],
|
||||
"boards": ["qa-custom-widget-assistant", "qa-widgets-12"],
|
||||
"profiles": ["degraded"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["CV-008"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "whole-product-01",
|
||||
"wave": "whole-product",
|
||||
"title": "Admin day-one journey",
|
||||
"prRefs": [6545, 6569],
|
||||
"personas": ["Avery Admin", "Nora Newcomer"],
|
||||
"boards": ["qa-grid-24"],
|
||||
"profiles": ["onboarding-fresh", "main-writable"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["WP-001"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-02",
|
||||
"wave": "whole-product",
|
||||
"title": "Owner customization journey",
|
||||
"prRefs": [6356, 6503, 6502],
|
||||
"personas": ["Rowan Owner"],
|
||||
"boards": ["qa-grid-24", "qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["desktop-1920"],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["WP-002"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-03",
|
||||
"wave": "whole-product",
|
||||
"title": "Editor daily journey",
|
||||
"prRefs": [6503, 6502, 6450],
|
||||
"personas": ["Eden Editor"],
|
||||
"boards": ["qa-scroll-lab", "qa-layout-boundaries"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["tablet-1024", "desktop-1280"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["WP-003"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-04",
|
||||
"wave": "whole-product",
|
||||
"title": "Viewer and outsider boundaries",
|
||||
"prRefs": [6545],
|
||||
"personas": ["Vivian Viewer", "Nolan Outsider"],
|
||||
"boards": ["qa-permissions-public"],
|
||||
"profiles": ["main-writable", "main-readonly"],
|
||||
"viewports": ["mobile-375", "desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["WP-004"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-05",
|
||||
"wave": "whole-product",
|
||||
"title": "Mobile journey",
|
||||
"prRefs": [6450, 6503, 6502],
|
||||
"personas": ["Morgan Mobile"],
|
||||
"boards": ["qa-layout-boundaries", "qa-widgets-01"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-320", "mobile-375", "boundary-767", "boundary-768"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["touch", "keyboard"],
|
||||
"cases": ["WP-005"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-06",
|
||||
"wave": "whole-product",
|
||||
"title": "Media operator journey",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Maya Media"],
|
||||
"boards": ["qa-widgets-04", "qa-widgets-05"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["WP-006"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-07",
|
||||
"wave": "whole-product",
|
||||
"title": "Infrastructure operator journey",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra"],
|
||||
"boards": ["qa-widgets-07", "qa-widgets-08", "qa-widgets-09", "qa-widgets-10", "qa-widgets-11"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["desktop-1920"],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"cases": ["WP-007"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-08",
|
||||
"wave": "whole-product",
|
||||
"title": "Creator and Assistant journey",
|
||||
"prRefs": [6356, 6482],
|
||||
"personas": ["Cora Creator", "Ash Assistant"],
|
||||
"boards": ["qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["mobile-375", "desktop-1440"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["WP-008"]
|
||||
},
|
||||
{
|
||||
"id": "whole-product-09",
|
||||
"wave": "whole-product",
|
||||
"title": "Accessibility and destructive-action pass",
|
||||
"prRefs": [6356, 6503, 6502, 6450, 6482, 6569, 6545],
|
||||
"personas": ["Kira Keyboard", "Casey Chaos"],
|
||||
"boards": ["qa-grid-24", "qa-download-upload", "qa-custom-widget-assistant"],
|
||||
"profiles": ["main-writable", "main-readonly", "degraded"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100, 200],
|
||||
"inputs": ["keyboard"],
|
||||
"cases": ["WP-009"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "performance-01",
|
||||
"wave": "performance",
|
||||
"title": "Cold and warm board load",
|
||||
"prRefs": [6555, 6545],
|
||||
"personas": ["Avery Admin"],
|
||||
"boards": ["qa-grid-24", "qa-widgets-12"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["mobile-375", "desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse"],
|
||||
"cases": ["PE-001"]
|
||||
},
|
||||
{
|
||||
"id": "performance-02",
|
||||
"wave": "performance",
|
||||
"title": "Grid interaction responsiveness",
|
||||
"prRefs": [6503, 6555],
|
||||
"personas": ["Rowan Owner"],
|
||||
"boards": ["qa-dense-collisions", "qa-nested-containers"],
|
||||
"profiles": ["main-writable"],
|
||||
"viewports": ["tablet-1024", "desktop-1920"],
|
||||
"zooms": [80, 100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["PE-002"]
|
||||
},
|
||||
{
|
||||
"id": "performance-03",
|
||||
"wave": "performance",
|
||||
"title": "Widget network and render budget",
|
||||
"prRefs": [6502, 6555],
|
||||
"personas": ["Ingrid Infra", "Maya Media"],
|
||||
"boards": ["qa-widgets-04", "qa-widgets-07", "qa-widgets-12"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["mobile-320", "desktop-1440"],
|
||||
"zooms": [100],
|
||||
"inputs": ["mouse", "touch"],
|
||||
"cases": ["PE-003"]
|
||||
},
|
||||
{
|
||||
"id": "performance-04",
|
||||
"wave": "performance",
|
||||
"title": "Long-session stability",
|
||||
"prRefs": [6356, 6503, 6502, 6482, 6555, 6545],
|
||||
"personas": ["Casey Chaos"],
|
||||
"boards": ["qa-grid-24", "qa-custom-widget-assistant", "qa-widgets-12"],
|
||||
"profiles": ["main-writable", "degraded"],
|
||||
"viewports": ["mobile-375", "desktop-1920"],
|
||||
"zooms": [100, 125],
|
||||
"inputs": ["mouse", "touch", "keyboard"],
|
||||
"cases": ["PE-004"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"updatedAt": "2026-09-01T12:42:59.288Z",
|
||||
"packetStatuses": {
|
||||
"preflight-01": {
|
||||
"status": "passed",
|
||||
"cases": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
}
|
||||
},
|
||||
"preflight-02": {
|
||||
"status": "passed",
|
||||
"cases": {
|
||||
"PF-002-ENVIRONMENT": "passed",
|
||||
"PF-002-ACCESS": "passed",
|
||||
"PF-002-EVIDENCE": "passed"
|
||||
}
|
||||
},
|
||||
"preflight-03": {
|
||||
"status": "passed",
|
||||
"cases": {
|
||||
"PF-003-ENVIRONMENT": "not-reached",
|
||||
"PF-003-ACCESS": "not-reached",
|
||||
"PF-003-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-01": {
|
||||
"status": "passed",
|
||||
"cases": {
|
||||
"BD-001-THRESHOLDS": "passed",
|
||||
"BD-001-MUTATION": "passed",
|
||||
"BD-001-PERSISTENCE": "passed",
|
||||
"BD-001-ACCESS-RECOVERY": "passed"
|
||||
}
|
||||
},
|
||||
"board-02": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-002-THRESHOLDS": "not-reached",
|
||||
"BD-002-MUTATION": "not-reached",
|
||||
"BD-002-PERSISTENCE": "not-reached",
|
||||
"BD-002-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-03": {
|
||||
"status": "failed",
|
||||
"cases": {
|
||||
"BD-003-THRESHOLDS": "passed",
|
||||
"BD-003-MUTATION": "failed",
|
||||
"BD-003-PERSISTENCE": "passed",
|
||||
"BD-003-ACCESS-RECOVERY": "passed"
|
||||
}
|
||||
},
|
||||
"board-04": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-004-THRESHOLDS": "not-reached",
|
||||
"BD-004-MUTATION": "not-reached",
|
||||
"BD-004-PERSISTENCE": "not-reached",
|
||||
"BD-004-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-05": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-005-THRESHOLDS": "not-reached",
|
||||
"BD-005-MUTATION": "not-reached",
|
||||
"BD-005-PERSISTENCE": "not-reached",
|
||||
"BD-005-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-06": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-006-THRESHOLDS": "not-reached",
|
||||
"BD-006-MUTATION": "not-reached",
|
||||
"BD-006-PERSISTENCE": "not-reached",
|
||||
"BD-006-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-07": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-007-THRESHOLDS": "not-reached",
|
||||
"BD-007-MUTATION": "not-reached",
|
||||
"BD-007-PERSISTENCE": "not-reached",
|
||||
"BD-007-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-08": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-008-THRESHOLDS": "not-reached",
|
||||
"BD-008-MUTATION": "not-reached",
|
||||
"BD-008-PERSISTENCE": "not-reached",
|
||||
"BD-008-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"board-09": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"BD-009-THRESHOLDS": "not-reached",
|
||||
"BD-009-MUTATION": "not-reached",
|
||||
"BD-009-PERSISTENCE": "not-reached",
|
||||
"BD-009-ACCESS-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-01": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-001-RENDER-SIZE": "not-reached",
|
||||
"WG-001-STATES-RECOVERY": "not-reached",
|
||||
"WG-001-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-001-ACCESS-READONLY": "not-reached",
|
||||
"WG-001-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-02": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-002-RENDER-SIZE": "not-reached",
|
||||
"WG-002-STATES-RECOVERY": "not-reached",
|
||||
"WG-002-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-002-ACCESS-READONLY": "not-reached",
|
||||
"WG-002-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-03": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-003-RENDER-SIZE": "not-reached",
|
||||
"WG-003-STATES-RECOVERY": "not-reached",
|
||||
"WG-003-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-003-ACCESS-READONLY": "not-reached",
|
||||
"WG-003-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-04": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-004-RENDER-SIZE": "not-reached",
|
||||
"WG-004-STATES-RECOVERY": "not-reached",
|
||||
"WG-004-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-004-ACCESS-READONLY": "not-reached",
|
||||
"WG-004-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-05": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-005-RENDER-SIZE": "not-reached",
|
||||
"WG-005-STATES-RECOVERY": "not-reached",
|
||||
"WG-005-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-005-ACCESS-READONLY": "not-reached",
|
||||
"WG-005-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-06": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-006-RENDER-SIZE": "not-reached",
|
||||
"WG-006-STATES-RECOVERY": "not-reached",
|
||||
"WG-006-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-006-ACCESS-READONLY": "not-reached",
|
||||
"WG-006-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-07": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-007-RENDER-SIZE": "not-reached",
|
||||
"WG-007-STATES-RECOVERY": "not-reached",
|
||||
"WG-007-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-007-ACCESS-READONLY": "not-reached",
|
||||
"WG-007-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-08": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-008-RENDER-SIZE": "not-reached",
|
||||
"WG-008-STATES-RECOVERY": "not-reached",
|
||||
"WG-008-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-008-ACCESS-READONLY": "not-reached",
|
||||
"WG-008-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-09": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-009-RENDER-SIZE": "not-reached",
|
||||
"WG-009-STATES-RECOVERY": "not-reached",
|
||||
"WG-009-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-009-ACCESS-READONLY": "not-reached",
|
||||
"WG-009-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-10": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-010-RENDER-SIZE": "not-reached",
|
||||
"WG-010-STATES-RECOVERY": "not-reached",
|
||||
"WG-010-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-010-ACCESS-READONLY": "not-reached",
|
||||
"WG-010-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-11": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-011-RENDER-SIZE": "not-reached",
|
||||
"WG-011-STATES-RECOVERY": "not-reached",
|
||||
"WG-011-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-011-ACCESS-READONLY": "not-reached",
|
||||
"WG-011-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"widgets-12": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WG-012-RENDER-SIZE": "not-reached",
|
||||
"WG-012-STATES-RECOVERY": "not-reached",
|
||||
"WG-012-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-012-ACCESS-READONLY": "not-reached",
|
||||
"WG-012-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-01": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-001-HAPPY-PATH": "not-reached",
|
||||
"CV-001-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-001-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-001-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-02": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-002-HAPPY-PATH": "not-reached",
|
||||
"CV-002-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-002-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-002-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-03": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-003-HAPPY-PATH": "not-reached",
|
||||
"CV-003-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-003-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-003-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-04": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-004-HAPPY-PATH": "not-reached",
|
||||
"CV-004-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-004-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-004-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-05": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-005-HAPPY-PATH": "not-reached",
|
||||
"CV-005-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-005-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-005-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-06": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-006-HAPPY-PATH": "not-reached",
|
||||
"CV-006-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-006-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-006-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-07": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-007-HAPPY-PATH": "not-reached",
|
||||
"CV-007-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-007-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-007-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"core-v2-08": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"CV-008-HAPPY-PATH": "not-reached",
|
||||
"CV-008-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-008-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-008-DEGRADED-RECOVERY": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-01": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-001-JOURNEY": "not-reached",
|
||||
"WP-001-THRESHOLDS": "not-reached",
|
||||
"WP-001-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-001-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-02": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-002-JOURNEY": "not-reached",
|
||||
"WP-002-THRESHOLDS": "not-reached",
|
||||
"WP-002-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-002-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-03": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-003-JOURNEY": "not-reached",
|
||||
"WP-003-THRESHOLDS": "not-reached",
|
||||
"WP-003-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-003-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-04": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-004-JOURNEY": "not-reached",
|
||||
"WP-004-THRESHOLDS": "not-reached",
|
||||
"WP-004-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-004-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-05": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-005-JOURNEY": "not-reached",
|
||||
"WP-005-THRESHOLDS": "not-reached",
|
||||
"WP-005-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-005-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-06": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-006-JOURNEY": "not-reached",
|
||||
"WP-006-THRESHOLDS": "not-reached",
|
||||
"WP-006-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-006-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-07": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-007-JOURNEY": "not-reached",
|
||||
"WP-007-THRESHOLDS": "not-reached",
|
||||
"WP-007-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-007-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-08": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-008-JOURNEY": "not-reached",
|
||||
"WP-008-THRESHOLDS": "not-reached",
|
||||
"WP-008-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-008-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"whole-product-09": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"WP-009-JOURNEY": "not-reached",
|
||||
"WP-009-THRESHOLDS": "not-reached",
|
||||
"WP-009-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-009-FAILURE-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"performance-01": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"PE-001-BASELINE": "not-reached",
|
||||
"PE-001-THRESHOLD": "not-reached",
|
||||
"PE-001-STRESS": "not-reached",
|
||||
"PE-001-RECOVERY-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"performance-02": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"PE-002-BASELINE": "not-reached",
|
||||
"PE-002-THRESHOLD": "not-reached",
|
||||
"PE-002-STRESS": "not-reached",
|
||||
"PE-002-RECOVERY-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"performance-03": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"PE-003-BASELINE": "not-reached",
|
||||
"PE-003-THRESHOLD": "not-reached",
|
||||
"PE-003-STRESS": "not-reached",
|
||||
"PE-003-RECOVERY-EVIDENCE": "not-reached"
|
||||
}
|
||||
},
|
||||
"performance-04": {
|
||||
"status": "not-reached",
|
||||
"cases": {
|
||||
"PE-004-BASELINE": "not-reached",
|
||||
"PE-004-THRESHOLD": "not-reached",
|
||||
"PE-004-STRESS": "not-reached",
|
||||
"PE-004-RECOVERY-EVIDENCE": "not-reached"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,205 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-01",
|
||||
"status": "passed",
|
||||
"caseStatuses": {
|
||||
"BD-001-THRESHOLDS": "passed",
|
||||
"BD-001-MUTATION": "passed",
|
||||
"BD-001-PERSISTENCE": "passed",
|
||||
"BD-001-ACCESS-RECOVERY": "passed"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "358d6469cf50ae9513cbce0f9238cd6c03aa4182",
|
||||
"url": "http://127.0.0.1:45657/boards/qa-grid-24",
|
||||
"actualPort": 45657,
|
||||
"runtimeProfile": "main-writable",
|
||||
"runtimeFlags": [
|
||||
"DEMO_MODE=true",
|
||||
"DEMO_READ_ONLY=false",
|
||||
"UNSAFE_ENABLE_MOCK_INTEGRATION=true"
|
||||
],
|
||||
"persona": "Rowan Owner",
|
||||
"sessionId": "bd01-358d-080; bd01-358d-125; bd01-358d-mut-100; bd01-358d-fresh-100; bd01-358d-fresh2-100; bd01-358d-cleanfresh-100",
|
||||
"timestamp": "2026-09-01T13:45:13+02:00",
|
||||
"viewport": "desktop-1440",
|
||||
"viewports": ["desktop-1440", "desktop-1920"],
|
||||
"input": "mouse",
|
||||
"additionalInputs": ["keyboard"],
|
||||
"zoom": 100,
|
||||
"zooms": [80, 100, 125]
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-bottom.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top-after.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-bottom.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-baseline-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-edit-baseline-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-move-down-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-after-down-final-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-after-escape-final-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-resize-down-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-resize-down-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-resize-restored-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-collision-right-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-collision-right-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-collision-all-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-collision-restored-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-before-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-after-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-after-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-invalid-before-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-invalid-after-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-invalid-after-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-handles-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-before-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-after-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-after-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-restored-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-restored-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-restored-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-restored-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-dirty-before-nav-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-dirty-before-nav-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-unsaved-dialog-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-cancel-after-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-cancel-after-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-discard-settings-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-discard-back-clean-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-discard-reload-check-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-fresh-baseline-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-fresh-baseline-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-before-save-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-before-save-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-save-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-save-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-reload-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-reload-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-away-back-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-away-back-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-saved-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-saved-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-before-restore-save-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-restore-save-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-restore-save-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-restore-reload-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-restore-reload-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-clean-1440-100.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-clean-1440-100.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-080-network.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-125-network.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-cleanfresh-network.txt"
|
||||
],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [
|
||||
{"surface":"desktop-1440","zoom":80,"cssViewport":"1800x1125","dpr":0.8,"visualViewportScale":1,"root":"x16..right1611.93 width1595.94","page":"scrollWidth=1781 clientWidth=1781 scrollHeight=4807 clientHeight=1125","scrollBottom":3682.5,"renderedWidths":"1-22; seeded 23/24 gutter-clamped"},
|
||||
{"surface":"desktop-1440","zoom":100,"cssViewport":"1440x900","dpr":1,"visualViewportScale":1,"root":"x16..right1300.59 width1284.59","page":"scrollWidth=1440 clientWidth=1440 scrollHeight=3887 clientHeight=900","scrollBottom":2987,"renderedWidths":"1-22; seeded 23/24 gutter-clamped"},
|
||||
{"surface":"desktop-1440","zoom":125,"cssViewport":"1152x720","dpr":1.25,"visualViewportScale":1,"root":"x16..right994.73 width978.73","page":"scrollWidth=1140 clientWidth=1140 scrollHeight=4558 clientHeight=720","scrollBottom":3837.6,"renderedWidths":"1-16; seeded 17-24 gutter-clamped"},
|
||||
{"surface":"desktop-1920","zoom":80,"cssViewport":"2400x1350","dpr":0.8,"visualViewportScale":1,"root":"x16..right2159.36 width2143.36","page":"scrollWidth=2381 clientWidth=2381 scrollHeight=6425 clientHeight=1350","scrollBottom":5075,"renderedWidths":"1-22; seeded 23/24 gutter-clamped"},
|
||||
{"surface":"desktop-1920","zoom":100,"cssViewport":"1920x1080","dpr":1,"visualViewportScale":1,"root":"x16..right1738.53 width1722.5","page":"scrollWidth=1920 clientWidth=1920 scrollHeight=5181 clientHeight=1080","scrollBottom":4101,"renderedWidths":"1-22; seeded 23/24 gutter-clamped"},
|
||||
{"surface":"desktop-1920","zoom":125,"cssViewport":"1536x864","dpr":1.25,"visualViewportScale":1,"root":"x16..right1377.24 width1361.24","page":"scrollWidth=1524 clientWidth=1524 scrollHeight=4114 clientHeight=864","scrollBottom":3249.6,"renderedWidths":"1-22; seeded 23/24 gutter-clamped"}
|
||||
],
|
||||
"limitations": [
|
||||
"Metrics distinguish browser outer viewport from CSS viewport for extension zoom; visualViewport.scale remained 1 on every surface.",
|
||||
"The seeded 1..24 width series and the visible 24-column width-coverage option were exercised as rendered UI evidence; the board was not left in a changed settings layout.",
|
||||
"Diagnostics retained status-only network/asset listings with query values redacted. Console and page-error commands returned no output; headers, bodies, cookies, tokens, and credentials were not retained.",
|
||||
"The browser CLI retained stale registry metadata for an exited recorder daemon after close; no corresponding Chrome process remained."
|
||||
]
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Pre-fix black-box run against candidate 358d6469cf50ae9513cbce0f9238cd6c03aa4182. Six viewport/zoom surfaces each rendered 24 seeded groups and returned to scroll top after bottom capture. The six top and bottom JSON pairs plus top-after JSON files are the full threshold matrix. The assigned board showed left x=16 gutters and a bounded right edge; scrollWidth equaled clientWidth on all six surfaces. Width intents 1..24 were represented by the seeded matrix; overwide entries were bounded to the available last column. Mutation covered physical pointer move into an empty gap, physical resize, active invalid-drop Escape rollback, keyboard move/resize, collision displacement, and restoration. A row-2 move survived Save, reload, away/back, and a fresh profile; the move was reversed through the UI, saved, reloaded, and confirmed clean in a fresh profile. Dirty navigation Cancel retained work; Discard navigated to settings and the discarded move was absent after reload and fresh-session checks. No unresolved finding remains."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-01: 24-column placement and resize
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | passed |
|
||||
| Wave | board |
|
||||
| PR refs | candidate `358d6469cf50ae9513cbce0f9238cd6c03aa4182` |
|
||||
| Personas | Rowan Owner |
|
||||
| Boards | qa-grid-24 |
|
||||
| Profiles | main-writable; `/tmp/homarr-release-v2-qa-zoom-358d-s1b-080`; `/tmp/homarr-release-v2-qa-zoom-358d-s1b-125` |
|
||||
| Chromium viewports | desktop-1440, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01) |
|
||||
| Candidate SHA | `358d6469cf50ae9513cbce0f9238cd6c03aa4182` |
|
||||
| URL / actual port | http://127.0.0.1:45657/boards/qa-grid-24 / 45657 |
|
||||
| Runtime profile / flags | main-writable / `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, `UNSAFE_ENABLE_MOCK_INTEGRATION=true` |
|
||||
| Executed persona | Rowan Owner |
|
||||
| Browser session ID | `bd01-358d-080`; `bd01-358d-125`; `bd01-358d-mut-100`; `bd01-358d-fresh2-100`; `bd01-358d-cleanfresh-100` |
|
||||
| Timestamp | 2026-09-01T13:45:13+02:00 |
|
||||
| Executed viewport / input / zoom | desktop-1440 (1440x900), desktop-1920 (1920x1080) / mouse, keyboard / 80%, 100%, 125% |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-001-THRESHOLDS | passed | [1440/100 top geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top.json), [1440/125 geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top.json), [1920/125 geometry](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top.json) | All six viewport/zoom surfaces rendered 24 seeded groups. Width intents 1..24 were present; measured rendered widths were 1..22 on five surfaces and 1..16 at desktop-1440/125, with overwide entries bounded to the available right edge. Left/right gutters were present, `scrollWidth == clientWidth` on every surface, and bottom/top scroll captures completed. | 24-column placement/scaling remains usable at each assigned viewport and zoom, with gutter bounds and no unintended page overflow. |
|
||||
| BD-001-MUTATION | passed | [pointer gap move](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-after-1440-100.json), [Escape rollback](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-invalid-after-1440-100.json), [pointer resize](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-pointer-resize-after-1440-100.json), [keyboard collision](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-mutation-keyboard-collision-all-1440-100.json) | Physical drag moved cell 01 from column 1 to empty column 22; an active invalid drag plus Escape returned to its prior position. Physical SE resize changed height 1 to 2 and was reversed. Keyboard move, resize, collision displacement, and restoration passed without overlap or page overflow. | Pointer DnD, resize, keyboard move/resize, collision handling, and cancel/rollback stay within bounds. |
|
||||
| BD-001-PERSISTENCE | passed | [saved row-2 state](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-save-1440-100.json), [reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-after-reload-1440-100.json), [away/back](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-away-back-1440-100.json), [fresh saved session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-saved-1440-100.json), [fresh clean baseline](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-persistence-fresh-clean-1440-100.json) | A harmless move to row 2 survived Save, reload, away/back, and a fresh profile. The move was reversed through the UI, saved, reloaded, and confirmed at row 1 with 24 items in a fresh profile. | Saved layout persists across reload and fresh session; restored baseline remains clean. |
|
||||
| BD-001-ACCESS-RECOVERY | passed | [dirty state](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-dirty-before-nav-1440-100.json), [unsaved dialog](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-unsaved-dialog-1440-100.png), [Cancel result](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-cancel-after-1440-100.json), [Discard reload check](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-access-discard-reload-check-1440-100.json) | Dirty navigation prompted. Cancel retained the row-2 edit. Discard navigated to Board settings; browser Back briefly showed transient history state, but reload and fresh-session checks were row 1 and no discarded move persisted. | Dirty navigation prompts, Cancel retains work, and Discard does not silently persist the discarded move. |
|
||||
|
||||
## Full viewport and zoom matrix
|
||||
|
||||
| Assigned surface | Numeric zoom | CSS viewport / DPR / scale | Root bounds and gutters | Page scroll metrics | Widths / evidence |
|
||||
| --- | ---: | --- | --- | --- | --- |
|
||||
| desktop-1440 | 80 | 1800x1125 / 0.8 / 1 | x=16, right=1611.93, width=1595.94; right gutter to client 1781 | scrollWidth=1781, clientWidth=1781, scrollHeight=4807, bottom scrollY=3682.5 | 1..22 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-top.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-080-top-after.json) |
|
||||
| desktop-1440 | 100 | 1440x900 / 1 / 1 | x=16, right=1300.59, width=1284.59; right gutter to client 1440 | scrollWidth=1440, clientWidth=1440, scrollHeight=3887, bottom scrollY=2987 | 1..22 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-100-top-after.json) |
|
||||
| desktop-1440 | 125 | 1152x720 / 1.25 / 1 | x=16, right=994.73, width=978.73; right gutter to client 1140 | scrollWidth=1140, clientWidth=1140, scrollHeight=4558, bottom scrollY=3837.6 | 1..16 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1440-125-top-after.json) |
|
||||
| desktop-1920 | 80 | 2400x1350 / 0.8 / 1 | x=16, right=2159.36, width=2143.36; right gutter to client 2381 | scrollWidth=2381, clientWidth=2381, scrollHeight=6425, bottom scrollY=5075 | 1..22 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-top.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-080-top-after.json) |
|
||||
| desktop-1920 | 100 | 1920x1080 / 1 / 1 | x=16, right=1738.53, width=1722.5; right gutter to client 1920 | scrollWidth=1920, clientWidth=1920, scrollHeight=5181, bottom scrollY=4101 | 1..22 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-100-top-after.json) |
|
||||
| desktop-1920 | 125 | 1536x864 / 1.25 / 1 | x=16, right=1377.24, width=1361.24; right gutter to client 1524 | scrollWidth=1524, clientWidth=1524, scrollHeight=4114, bottom scrollY=3249.6 | 1..22 rendered; [top](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top.json), [bottom](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-bottom.json), [top-after](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-thresholds-1920-125-top-after.json) |
|
||||
|
||||
The seeded matrix contains width intents 1 through 24. The visible Board settings UI also exposed a `24-column width coverage` layout option. On the assigned gutter-bounded surfaces, entries through the available last column had measurable widths and overwide entries were clamped to that bound; no horizontal page overflow occurred. The full top/bottom/top-after capture set above is the human-readable matrix, while the JSON geometry files preserve scalar values.
|
||||
|
||||
## Mutation details
|
||||
|
||||
The physical pointer path moved the 1x1 first item from column 1 to an empty column 22, then the inverse gesture restored it. An active invalid drag followed by Escape retained the pre-drag column. The physical southeast resize changed height 1 to 2 and was reversed. Keyboard ArrowDown moved the item to row 2; Shift+ArrowDown resized it to height 2; ArrowRight into an occupied cell displaced the neighbor without overlap; the layout was restored before persistence. Keyboard Escape stopped keyboard editing, while the active pointer Escape path provided rollback evidence.
|
||||
|
||||
## Persistence and access recovery
|
||||
|
||||
A harmless row-2 move was saved and observed after reload, Board settings away/back, and a fresh authenticated profile. The move was then reversed with the UI, saved, reloaded, and confirmed as row 1 in a separate fresh profile with 24 items. A dirty navigation attempt displayed the Unsaved changes dialog. Cancel retained the dirty row-2 edit. Discard navigated to Board settings; browser history briefly retained transient in-memory row 2, but reload and fresh-session checks returned row 1, confirming the discarded move was not persisted.
|
||||
|
||||
## Diagnostics and limitations
|
||||
|
||||
Status-only network/asset listings were captured for the 80%, 125%, and fresh native sessions: [80% network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-080-network.txt), [125% network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-125-network.txt), and [fresh native network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-01/bd01-358d-diagnostics-cleanfresh-network.txt). Terminal status lines were 200; a few image entries had no terminal status in the CLI listing. Console and page-error commands returned no output. No headers, bodies, cookies, tokens, credentials, or query values were retained.
|
||||
|
||||
No widget-specific checks or performance timings were requested. No rail-specific target was claimed because the assigned seeded board exposed the gutter-bounded canvas rather than a separate rail target. No unresolved finding remains.
|
||||
|
||||
## Cleanup
|
||||
|
||||
The saved temporary row-2 move was reversed and saved. Reload and a separate fresh profile confirmed the clean row-1 baseline. All exact Chromium sessions were closed; the browser CLI retained stale registry metadata for the exited recorder/main daemon only, with no corresponding Chrome process. No resources were created and no source, database, manifest, ledger, or master files were inspected or changed.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-02",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-002-THRESHOLDS": "not-reached",
|
||||
"BD-002-MUTATION": "not-reached",
|
||||
"BD-002-PERSISTENCE": "not-reached",
|
||||
"BD-002-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-02: Scrollable canvas and collapse states
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6503 |
|
||||
| Personas | Eden Editor |
|
||||
| Boards | qa-scroll-lab |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-375, desktop-1280 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-002-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/BD-002-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-002-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/BD-002-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-002-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/BD-002-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-002-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-02/BD-002-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,133 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-03",
|
||||
"status": "failed",
|
||||
"caseStatuses": {
|
||||
"BD-003-THRESHOLDS": "passed",
|
||||
"BD-003-MUTATION": "failed",
|
||||
"BD-003-PERSISTENCE": "passed",
|
||||
"BD-003-ACCESS-RECOVERY": "passed"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "358d6469cf50ae9513cbce0f9238cd6c03aa4182",
|
||||
"url": "http://127.0.0.1:34355/boards/qa-dense-collisions",
|
||||
"actualPort": 34355,
|
||||
"runtimeProfile": "main-writable",
|
||||
"runtimeFlags": [
|
||||
"DEMO_MODE=true",
|
||||
"DEMO_READ_ONLY=false",
|
||||
"UNSAFE_ENABLE_MOCK_INTEGRATION=true"
|
||||
],
|
||||
"persona": "Casey Chaos",
|
||||
"sessionId": "bd03-358d-main",
|
||||
"timestamp": "2026-09-01T12:39:31+02:00",
|
||||
"viewport": "tablet-1024",
|
||||
"input": "mouse",
|
||||
"zoom": 100
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "BD-003-F-001",
|
||||
"severity": "P2",
|
||||
"status": "blocked",
|
||||
"replayStatus": "awaiting independent clean-clone replay",
|
||||
"fingerprint": "board-grid-remove-item-does-not-remove-disposable-item",
|
||||
"caseIds": [
|
||||
"BD-003-MUTATION"
|
||||
],
|
||||
"title": "Remove item does not remove a disposable board item",
|
||||
"steps": [
|
||||
"Login as Casey Chaos and enter Edit board.",
|
||||
"Open Settings for a Date and time item containing the date text.",
|
||||
"Activate Remove item by locator, semantic role, physical pointer, and keyboard Enter.",
|
||||
"Observe that the menu remains open and the item remains in the editor; Save board and reload."
|
||||
],
|
||||
"observed": "The selected disposable item was not removed. Fresh session and reload retained both disposable items.",
|
||||
"expected": "Remove item should remove the selected item and the saved board should no longer contain it.",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh2-session.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-final-reload-1440.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-native-initial.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-native-edit-unobstructed.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-zoom125.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1440-native.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1440-zoom125.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-collision-reflow.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-saved-reflow.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-reload-persisted.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh-session.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh2-session.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-final-reload-1440.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-after-collision-drag.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-after-gap-drag.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-pointer-drag-gap-result.png"
|
||||
],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widget": "Date and time",
|
||||
"status": "passed",
|
||||
"notes": "Rendered in the dense board and was created through the Add board content UI; one move/resize modal save triggered collision reflow."
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": [
|
||||
"No touch command is available for the installed Chromium agent-browser provider; one fresh capability retry was limited to the CLI touch surface, so native touch semantics are not claimed.",
|
||||
"Low-level mouse DnD/resize attempts in collision and gap destinations produced no committed geometry change; the UI Move / resize item modal did commit a collision move and reflow.",
|
||||
"The UI Remove item action remained ineffective after a fresh-session retry. The final saved board therefore retains two disposable items and the collision-reflowed layout; original fixture geometry could not be restored through the UI."
|
||||
]
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Casey Chaos (username casey-chaos) had the corrected MODIFY/Edit board permission. Native 100% and extension-backed 125% renders were exercised at 1024x768 and 1440x900; the full matrix is recorded below. Fresh2 was a persistence check, not an independent defect replay. Finding BD-003-F-001 awaits independent clean-clone replay. All browser sessions were closed. No repository tests, source inspection, API calls, or database access were used."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-03: Dense collision handling
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | failed — mutation cleanup finding BD-003-F-001 |
|
||||
| Wave | board |
|
||||
| PR refs | #6503 |
|
||||
| Personas | Casey Chaos (`casey-chaos`) |
|
||||
| Boards | `qa-dense-collisions` |
|
||||
| Profile | main-writable clone, `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, `UNSAFE_ENABLE_MOCK_INTEGRATION=true` |
|
||||
| Candidate SHA | `358d6469cf50ae9513cbce0f9238cd6c03aa4182` |
|
||||
| URL / actual port | `http://127.0.0.1:34355/boards/qa-dense-collisions` / `34355` |
|
||||
| Browser sessions | `bd03-358d-main`, `bd03-358d-fresh2`, `bd03-358d-zoom125` |
|
||||
| Viewports | `1024x768`, `1440x900` |
|
||||
| Zoom | native `100%`; extension `125%` (`/tmp/homarr-release-v2-qa-zoom-358d-s3-125`) |
|
||||
| Input | mouse, keyboard; touch-equivalent capability checked but unavailable in Chromium CLI |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03) |
|
||||
| Timestamp | `2026-09-01T12:39:31+02:00` |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-003-THRESHOLDS | passed | [1024 native](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-native-initial.png), [1440 native](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1440-native.png), [1024 at 125%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-zoom125.png), [1440 at 125%](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1440-zoom125.png) | Casey had Edit board; dense items rendered at all four viewport/zoom cells. No horizontal item overflow was observed; the dense canvas correctly continued vertically below the 1024px viewport. | Dense items render within bounds at both viewport sizes and zoom levels. |
|
||||
| BD-003-MUTATION | failed | [edit](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-native-edit-unobstructed.png), [collision reflow](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-collision-reflow.png), [pointer collision](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-after-collision-drag.png), [pointer gap](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-after-gap-drag.png) | Two disposable Date/time items were created. Physical mouse DnD/resize attempts in collision/gap paths did not commit geometry; Move / resize item with X offset `6`→`8` did commit and reflow. Keyboard cancellation was attempted; no verified keyboard geometry change. Remove item remained ineffective, so the required delete/cleanup path failed. | Exercise collision/gap/bounds/invalid-drop/cancel/rollback with pointer and non-pointer input, then complete safe create/move/resize/reorder/delete cleanup. |
|
||||
| BD-003-PERSISTENCE | passed | [saved](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-saved-reflow.png), [reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-reload-persisted.png), [fresh session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh2-session.png), [final reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-final-reload-1440.png) | Saved collision reflow survived reload, away/back navigation, and a second authenticated session. The two disposable items also survived because UI deletion did not work. | Saved changes survive reload, navigation, and a fresh session; cleanup should leave the original fixture. |
|
||||
| BD-003-ACCESS-RECOVERY | passed | [native edit](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-1024-native-edit-unobstructed.png), [fresh session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh2-session.png) | Casey authenticated successfully in the isolated clone and consistently received Edit board / Save board controls. A fresh named session authenticated and recovered the saved board. | Corrected Casey MODIFY access exposes Edit board and saved state is recoverable in a second session. |
|
||||
|
||||
## Findings
|
||||
|
||||
### BD-003-F-001 — Remove item does not remove a disposable board item
|
||||
|
||||
In Edit board, selecting a Date/time item containing the date text and activating Remove item by locator, semantic role, physical pointer, and keyboard Enter left the menu open and the item present. Save/reload and a fresh session retained both disposable items. The finding awaits independent clean-clone replay; `bd03-358d-fresh2` was a persistence check, not an independent defect replay. See [fresh-session evidence](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-fresh2-session.png) and [final reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-03/bd03-358d-final-reload-1440.png).
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
No performance timings were collected. The installed Chromium agent-browser provider exposes no native touch command; the touch-equivalent capability retry therefore cannot claim native touch semantics. Low-level mouse collision/gap DnD and resize attempts were made and left the layout unchanged, while the visible Move / resize item dialog successfully committed one collision move and automatic reflow.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
None recorded. The fresh2 session was used for persistence, not as an independent defect replay; BD-003-F-001 awaits an independent clean-clone replay.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Two disposable Date/time items were created through the UI. UI removal was attempted in the fresh session through semantic, locator, physical-pointer, and keyboard activation, but neither item was removed. The final reload evidence therefore records the retained disposable items and reflowed layout as an explicit cleanup limitation; original fixture geometry was not restored through the UI. Console and page-error commands returned no output. Status-only network inspection showed successful 200 responses for the app document/assets/data observed and no displayed app request failures. All named browser sessions were closed. No source, database, fixture manifest, API, test, build, or aggregator inspection was performed.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-04",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-004-THRESHOLDS": "not-reached",
|
||||
"BD-004-MUTATION": "not-reached",
|
||||
"BD-004-PERSISTENCE": "not-reached",
|
||||
"BD-004-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-04: Nested containers
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6503 |
|
||||
| Personas | Rowan Owner |
|
||||
| Boards | qa-nested-containers |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-004-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-004-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-004-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-004-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-04/BD-004-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-05",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-005-THRESHOLDS": "not-reached",
|
||||
"BD-005-MUTATION": "not-reached",
|
||||
"BD-005-PERSISTENCE": "not-reached",
|
||||
"BD-005-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-05: Responsive layout boundaries
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6450 |
|
||||
| Personas | Morgan Mobile |
|
||||
| Boards | qa-layout-boundaries |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | boundary-767, boundary-768, boundary-1279, desktop-1280 |
|
||||
| Zoom | 100% |
|
||||
| Input | touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-005-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-005-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-005-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-005-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-05/BD-005-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-06",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-006-THRESHOLDS": "not-reached",
|
||||
"BD-006-MUTATION": "not-reached",
|
||||
"BD-006-PERSISTENCE": "not-reached",
|
||||
"BD-006-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-06: Icons, bookmarks, and compact layout
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6502 |
|
||||
| Personas | Brooke Minimalist |
|
||||
| Boards | qa-icons-bookmarks |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-375, desktop-1440 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, touch |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-006-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-006-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-006-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-006-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-06/BD-006-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-07",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-007-THRESHOLDS": "not-reached",
|
||||
"BD-007-MUTATION": "not-reached",
|
||||
"BD-007-PERSISTENCE": "not-reached",
|
||||
"BD-007-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-07: Permission-aware editing
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6503, #6545 |
|
||||
| Personas | Rowan Owner, Eden Editor, Vivian Viewer, Nolan Outsider |
|
||||
| Boards | qa-permissions-public |
|
||||
| Profiles | main-writable, main-readonly |
|
||||
| Chromium viewports | desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-007-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/BD-007-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-007-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/BD-007-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-007-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/BD-007-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-007-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-07/BD-007-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-08",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-008-THRESHOLDS": "not-reached",
|
||||
"BD-008-MUTATION": "not-reached",
|
||||
"BD-008-PERSISTENCE": "not-reached",
|
||||
"BD-008-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-08: Board import and export
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6545 |
|
||||
| Personas | Avery Admin |
|
||||
| Boards | qa-download-upload |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | desktop-1280 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-008-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/BD-008-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-008-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/BD-008-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-008-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/BD-008-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-008-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-08/BD-008-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "board-09",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"BD-009-THRESHOLDS": "not-reached",
|
||||
"BD-009-MUTATION": "not-reached",
|
||||
"BD-009-PERSISTENCE": "not-reached",
|
||||
"BD-009-ACCESS-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# board-09: Keyboard-only grid operation
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | board |
|
||||
| PR refs | #6503, #6450 |
|
||||
| Personas | Kira Keyboard |
|
||||
| Boards | qa-grid-24 |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BD-009-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-THRESHOLDS.png) | Not executed | Exercise every assigned viewport, zoom, and input, including breakpoint and dense-layout stress. |
|
||||
| BD-009-MUTATION | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-MUTATION.png) | Not executed | Exercise create, move, resize, reorder, collapse, and delete paths that apply to this packet. |
|
||||
| BD-009-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-PERSISTENCE.png) | Not executed | Reload, switch layouts or sessions, and verify saved and reset state remains correct. |
|
||||
| BD-009-ACCESS-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/board-09/BD-009-ACCESS-RECOVERY.png) | Not executed | Verify Owner, Editor, Viewer, outsider, read-only, keyboard, cancellation, error, and recovery behavior that applies. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-01",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-001-HAPPY-PATH": "not-reached",
|
||||
"CV-001-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-001-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-001-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-01: Custom widget authoring
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6356 |
|
||||
| Personas | Cora Creator |
|
||||
| Boards | qa-custom-widget-assistant |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-001-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/CV-001-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-001-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/CV-001-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-001-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/CV-001-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-001-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-01/CV-001-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-02",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-002-HAPPY-PATH": "not-reached",
|
||||
"CV-002-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-002-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-002-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-02: Assistant tool flow
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6482 |
|
||||
| Personas | Ash Assistant |
|
||||
| Boards | qa-custom-widget-assistant |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-375, desktop-1440 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-002-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/CV-002-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-002-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/CV-002-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-002-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/CV-002-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-002-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-02/CV-002-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-03",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-003-HAPPY-PATH": "not-reached",
|
||||
"CV-003-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-003-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-003-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-03: Onboarding happy path
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6569 |
|
||||
| Personas | Nora Newcomer |
|
||||
| Boards | None |
|
||||
| Profiles | onboarding-fresh |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-003-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/CV-003-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-003-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/CV-003-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-003-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/CV-003-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-003-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-03/CV-003-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-04",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-004-HAPPY-PATH": "not-reached",
|
||||
"CV-004-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-004-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-004-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-04: Authentication and session transitions
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6545, #6569 |
|
||||
| Personas | Avery Admin, Nolan Outsider |
|
||||
| Boards | qa-permissions-public |
|
||||
| Profiles | main-writable, main-readonly, onboarding-fresh |
|
||||
| Chromium viewports | mobile-375, desktop-1280 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-004-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/CV-004-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-004-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/CV-004-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-004-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/CV-004-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-004-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-04/CV-004-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-05",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-005-HAPPY-PATH": "not-reached",
|
||||
"CV-005-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-005-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-005-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-05: Integration management
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6502, #6545 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-07 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | desktop-1440 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-005-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/CV-005-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-005-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/CV-005-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-005-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/CV-005-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-005-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-05/CV-005-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-06",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-006-HAPPY-PATH": "not-reached",
|
||||
"CV-006-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-006-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-006-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-06: Search, menus, and dialogs
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6545 |
|
||||
| Personas | Kira Keyboard |
|
||||
| Boards | qa-grid-24 |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-006-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06/CV-006-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-006-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06/CV-006-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-006-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06/CV-006-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-006-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-06/CV-006-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-07",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-007-HAPPY-PATH": "not-reached",
|
||||
"CV-007-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-007-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-007-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-07: Read-only enforcement
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6356, #6482, #6545 |
|
||||
| Personas | Vivian Viewer |
|
||||
| Boards | qa-custom-widget-assistant, qa-permissions-public |
|
||||
| Profiles | main-readonly |
|
||||
| Chromium viewports | mobile-375, desktop-1280 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-007-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/CV-007-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-007-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/CV-007-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-007-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/CV-007-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-007-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-07/CV-007-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "core-v2-08",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"CV-008-HAPPY-PATH": "not-reached",
|
||||
"CV-008-MUTATION-PERSISTENCE": "not-reached",
|
||||
"CV-008-ACCESSIBILITY-ACCESS": "not-reached",
|
||||
"CV-008-DEGRADED-RECOVERY": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# core-v2-08: Failure and recovery states
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | core-v2 |
|
||||
| PR refs | #6356, #6482, #6502, #6545 |
|
||||
| Personas | Casey Chaos |
|
||||
| Boards | qa-custom-widget-assistant, qa-widgets-12 |
|
||||
| Profiles | degraded |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| CV-008-HAPPY-PATH | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/CV-008-HAPPY-PATH.png) | Not executed | Complete the primary flow using every assigned viewport, zoom, input, persona, profile, and board that applies. |
|
||||
| CV-008-MUTATION-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/CV-008-MUTATION-PERSISTENCE.png) | Not executed | Exercise applicable create, edit, save, reload, reset, cancel, and destructive actions. |
|
||||
| CV-008-ACCESSIBILITY-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/CV-008-ACCESSIBILITY-ACCESS.png) | Not executed | Verify keyboard/focus behavior, labels, permissions, outsider boundaries, and read-only enforcement. |
|
||||
| CV-008-DEGRADED-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/core-v2-08/CV-008-DEGRADED-RECOVERY.png) | Not executed | Verify empty, loading, failure, retry, recovery, console, network, evidence, and cleanup behavior. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "performance-01",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"PE-001-BASELINE": "not-reached",
|
||||
"PE-001-THRESHOLD": "not-reached",
|
||||
"PE-001-STRESS": "not-reached",
|
||||
"PE-001-RECOVERY-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# performance-01: Cold and warm board load
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | performance |
|
||||
| PR refs | #6555, #6545 |
|
||||
| Personas | Avery Admin |
|
||||
| Boards | qa-grid-24, qa-widgets-12 |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-375, desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PE-001-BASELINE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/PE-001-BASELINE.png) | Not executed | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. |
|
||||
| PE-001-THRESHOLD | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/PE-001-THRESHOLD.png) | Not executed | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. |
|
||||
| PE-001-STRESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/PE-001-STRESS.png) | Not executed | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. |
|
||||
| PE-001-RECOVERY-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-01/PE-001-RECOVERY-EVIDENCE.png) | Not executed | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "performance-02",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"PE-002-BASELINE": "not-reached",
|
||||
"PE-002-THRESHOLD": "not-reached",
|
||||
"PE-002-STRESS": "not-reached",
|
||||
"PE-002-RECOVERY-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# performance-02: Grid interaction responsiveness
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | performance |
|
||||
| PR refs | #6503, #6555 |
|
||||
| Personas | Rowan Owner |
|
||||
| Boards | qa-dense-collisions, qa-nested-containers |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | tablet-1024, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PE-002-BASELINE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/PE-002-BASELINE.png) | Not executed | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. |
|
||||
| PE-002-THRESHOLD | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/PE-002-THRESHOLD.png) | Not executed | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. |
|
||||
| PE-002-STRESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/PE-002-STRESS.png) | Not executed | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. |
|
||||
| PE-002-RECOVERY-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-02/PE-002-RECOVERY-EVIDENCE.png) | Not executed | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "performance-03",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"PE-003-BASELINE": "not-reached",
|
||||
"PE-003-THRESHOLD": "not-reached",
|
||||
"PE-003-STRESS": "not-reached",
|
||||
"PE-003-RECOVERY-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# performance-03: Widget network and render budget
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | performance |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra, Maya Media |
|
||||
| Boards | qa-widgets-04, qa-widgets-07, qa-widgets-12 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PE-003-BASELINE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/PE-003-BASELINE.png) | Not executed | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. |
|
||||
| PE-003-THRESHOLD | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/PE-003-THRESHOLD.png) | Not executed | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. |
|
||||
| PE-003-STRESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/PE-003-STRESS.png) | Not executed | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. |
|
||||
| PE-003-RECOVERY-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-03/PE-003-RECOVERY-EVIDENCE.png) | Not executed | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "performance-04",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"PE-004-BASELINE": "not-reached",
|
||||
"PE-004-THRESHOLD": "not-reached",
|
||||
"PE-004-STRESS": "not-reached",
|
||||
"PE-004-RECOVERY-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# performance-04: Long-session stability
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | performance |
|
||||
| PR refs | #6356, #6503, #6502, #6482, #6555, #6545 |
|
||||
| Personas | Casey Chaos |
|
||||
| Boards | qa-grid-24, qa-custom-widget-assistant, qa-widgets-12 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-375, desktop-1920 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PE-004-BASELINE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/PE-004-BASELINE.png) | Not executed | Capture repeatable cold and warm timing, request, render, memory, and console baselines with candidate metadata. |
|
||||
| PE-004-THRESHOLD | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/PE-004-THRESHOLD.png) | Not executed | Measure the packet at every assigned viewport, zoom, input, board, and profile against the documented threshold. |
|
||||
| PE-004-STRESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/PE-004-STRESS.png) | Not executed | Exercise dense data, repeated interaction, long-session, latency, and failure stress that applies. |
|
||||
| PE-004-RECOVERY-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/performance-04/PE-004-RECOVERY-EVIDENCE.png) | Not executed | Verify recovery after stress and link trace, screenshot, network, memory, and cleanup artifacts. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,277 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "preflight-01",
|
||||
"status": "passed",
|
||||
"caseStatuses": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
},
|
||||
"execution": {
|
||||
"url": "http://127.0.0.1:34499",
|
||||
"actualPort": 34499,
|
||||
"runtimeProfile": "main-writable",
|
||||
"runtimeFlags": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"fixtureUrl": "http://127.0.0.1:37357",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:37357/health",
|
||||
"candidateSha": "d3e7cd45a303bcd2c06757ead05306d334836074",
|
||||
"persona": "Avery Admin",
|
||||
"sessionId": "pf01-main-writable-d3e7",
|
||||
"sessionIds": ["pf01-main-writable-d3e7", "pf01-main-writable-d3e7-fresh"],
|
||||
"timestamp": "2026-09-01T10:37:30+02:00",
|
||||
"completedAt": "2026-09-01T10:37:30+02:00",
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"input": "mouse",
|
||||
"inputs": ["mouse", "keyboard"],
|
||||
"zoom": 100,
|
||||
"profiles": [
|
||||
{
|
||||
"runtimeProfile": "main-writable",
|
||||
"appUrl": "http://127.0.0.1:34499",
|
||||
"fixtureUrl": "http://127.0.0.1:37357",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:37357/health",
|
||||
"flags": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"sessionId": "pf01-main-writable-d3e7",
|
||||
"sessionIds": ["pf01-main-writable-d3e7", "pf01-main-writable-d3e7-fresh"],
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"input": ["mouse", "keyboard"],
|
||||
"zoom": 100,
|
||||
"result": "passed",
|
||||
"visibleBoundaryNote": "Avery reached the seeded home and healthy Weather details. Reload, Board settings away/back, fresh-session access, and Ctrl+K/Escape passed without product mutation.",
|
||||
"caseCoverage": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtimeProfile": "main-readonly",
|
||||
"appUrl": "http://127.0.0.1:44129",
|
||||
"fixtureUrl": "http://127.0.0.1:46609",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:46609/health",
|
||||
"flags": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"sessionId": "pf01-main-readonly-d3e7",
|
||||
"sessionIds": ["pf01-main-readonly-d3e7", "pf01-main-readonly-d3e7-fresh"],
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"input": ["mouse", "keyboard"],
|
||||
"zoom": 100,
|
||||
"result": "passed",
|
||||
"visibleBoundaryNote": "The seeded home, Weather details, Edit board boundary, and Add widget picker rendered. The picker was dismissed; no save or product mutation was attempted.",
|
||||
"caseCoverage": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtimeProfile": "onboarding-fresh",
|
||||
"appUrl": "http://127.0.0.1:40295",
|
||||
"fixtureUrl": "http://127.0.0.1:33189",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:33189/health",
|
||||
"flags": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"sessionId": "pf01-onboarding-d3e7",
|
||||
"sessionIds": ["pf01-onboarding-d3e7", "onb-d3e7-fresh"],
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"input": ["mouse", "keyboard"],
|
||||
"zoom": 100,
|
||||
"result": "passed",
|
||||
"visibleBoundaryNote": "Anonymous root access redirected once and stably to /init. Get started and Restore backup rendered; no seeded user or QA board was visible, and onboarding completion was not attempted.",
|
||||
"caseCoverage": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtimeProfile": "degraded",
|
||||
"appUrl": "http://127.0.0.1:44059",
|
||||
"fixtureUrl": "http://127.0.0.1:43513",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:43513/health",
|
||||
"flags": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"sessionId": "degr-d3e7",
|
||||
"sessionIds": ["degr-d3e7", "degr-fresh"],
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"input": ["mouse", "keyboard"],
|
||||
"zoom": 100,
|
||||
"result": "passed",
|
||||
"visibleBoundaryNote": "Avery reached the seeded home and healthy Weather details. Fixture status modes and a fixture-only slow abort were exercised; route removal restored the fixture and app.",
|
||||
"caseCoverage": {
|
||||
"PF-001-ENVIRONMENT": "passed",
|
||||
"PF-001-ACCESS": "passed",
|
||||
"PF-001-EVIDENCE": "passed"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onboardingFresh": {
|
||||
"status": "passed",
|
||||
"appUrl": "http://127.0.0.1:40295",
|
||||
"fixtureUrl": "http://127.0.0.1:33189",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:33189/health",
|
||||
"flags": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"sessionIds": ["pf01-onboarding-d3e7", "onb-d3e7-fresh"],
|
||||
"viewport": "desktop-1440",
|
||||
"viewportPx": "1440x900",
|
||||
"routes": ["/ -> /init", "/auth/login -> /auth/login", "back from /auth/login -> /init", "fresh / -> /init"],
|
||||
"fixtureHealth": "HTTP 200; response body omitted",
|
||||
"noSeededUserOrQABoardObserved": true,
|
||||
"completionAttempted": false,
|
||||
"reason": "The anonymous slot reached the expected first-board boundary. Completion and user creation were intentionally outside this preflight run."
|
||||
},
|
||||
"degraded": {
|
||||
"status": "passed",
|
||||
"appUrl": "http://127.0.0.1:44059",
|
||||
"fixtureUrl": "http://127.0.0.1:43513",
|
||||
"fixtureHealthUrl": "http://127.0.0.1:43513/health",
|
||||
"fixtureStatuses": {
|
||||
"health": 200,
|
||||
"slow": 200,
|
||||
"empty": 204,
|
||||
"malformed": 200,
|
||||
"error": 503
|
||||
},
|
||||
"slowAbort": "Fixture-only slow route aborted with no response status; route removed; slow endpoint recovered to HTTP 200; app reload and Weather recovered.",
|
||||
"completionAttempted": false
|
||||
}
|
||||
},
|
||||
"profileCoverage": {
|
||||
"main-writable": "passed",
|
||||
"main-readonly": "passed",
|
||||
"onboarding-fresh": "passed",
|
||||
"degraded": "passed"
|
||||
},
|
||||
"findings": [],
|
||||
"independentReproductions": [],
|
||||
"artifacts": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-away-settings.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-back-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-fresh.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-search-open.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-search-escape.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-fixture-health-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-diagnostics.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-away-settings.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-back-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-edit-boundary.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-fresh.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-search-open.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-search-escape.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-fixture-health-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-diagnostics.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-init.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-restore-expanded.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-auth-login.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-auth-login-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-back-init.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-fresh.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-final-init.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-fixture-health-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-diagnostics.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-away-settings.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-back-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-recovery.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-recovery-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-fresh.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-search-open.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-search-escape.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-fixture-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-diagnostics.txt"
|
||||
],
|
||||
"widgetChecks": [],
|
||||
"diagnostics": {
|
||||
"consoleErrors": "No page errors were captured in the clean app sessions or after degraded fixture recovery.",
|
||||
"consoleWarnings": "No actionable browser console error was observed. Console and error diagnostics were reduced to sanitized counts.",
|
||||
"network": "All four app profiles reached their assigned entry and fixture health endpoint. Degraded fixture status-only evidence recorded health 200, slow 200, empty 204, malformed 200, and error 503. A fixture-only slow abort produced no response status; removing the route restored slow 200. App recovery recorded no app 4xx/5xx or static-missing signal.",
|
||||
"artifactHygiene": "All current-candidate text artifacts contain status/path-classification counts only. No password, cookie, bearer value, token, request header, request/response body, query value, or secret was retained. Fixture response bodies were discarded."
|
||||
},
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": [
|
||||
"No performance timing was requested or recorded.",
|
||||
"Visual checks were performed at 1440x900, zoom 100%, with mouse and keyboard input.",
|
||||
"Onboarding completion and user creation were intentionally not attempted.",
|
||||
"No product state was changed; the only network interception was a local fixture slow-path abort that was removed before final app recovery."
|
||||
]
|
||||
},
|
||||
"notes": "Candidate d3e7cd45a303bcd2c06757ead05306d334836074 passed PF-001-ENVIRONMENT, PF-001-ACCESS, and PF-001-EVIDENCE across main-writable, main-readonly, onboarding-fresh, and degraded profiles at 1440x900, zoom 100%, with mouse and keyboard. Avery reached the seeded home and healthy Weather details on all authenticated profiles; reload, away/back, fresh sessions, and Ctrl+K/Escape passed. The anonymous profile reached /init without a loop and showed no seeded persona or board. Deterministic degraded fixture modes and scoped slow abort/recovery behaved as expected."
|
||||
}
|
||||
-->
|
||||
|
||||
# preflight-01: Candidate identity and services
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | passed |
|
||||
| Wave | preflight |
|
||||
| PR refs | #6545 |
|
||||
| Candidate SHA | d3e7cd45a303bcd2c06757ead05306d334836074 |
|
||||
| Profiles | main-writable, main-readonly, onboarding-fresh, degraded |
|
||||
| Primary execution | `http://127.0.0.1:34499` (main-writable) |
|
||||
| Primary fixture health | `http://127.0.0.1:37357/health` |
|
||||
| Runtime flags | `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, `UNSAFE_ENABLE_MOCK_INTEGRATION=true` |
|
||||
| Persona | Avery Admin |
|
||||
| Primary sessions | `pf01-main-writable-d3e7`; fresh `pf01-main-writable-d3e7-fresh` |
|
||||
| Viewport / input | 1440x900, zoom 100%, mouse and keyboard |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01) |
|
||||
| Completed | 2026-09-01T10:37:30+02:00 |
|
||||
| Auth | Auth-vault profiles used for seeded profiles; no password value was read or recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PF-001-ENVIRONMENT | passed | [profile matrix](#profile-matrix) and [fixture artifacts](#artifacts) | All four assigned app/fixture pairs rendered in named Chromium at the required desktop viewport. App entries and fixture health endpoints were reachable; the anonymous root reached `/init` once without a locale loop. | Candidate identity, runtime flags, browser setup, fixture reachability, and a clean app baseline. |
|
||||
| PF-001-ACCESS | passed | [profile matrix](#profile-matrix) and [Weather/reload artifacts](#artifacts) | Avery reached seeded home and healthy Weather details on main-writable, main-readonly, and degraded. The anonymous profile reached the expected first-board boundary with no seeded persona or QA board. | Assigned persona, seeded access, onboarding boundary, reload behavior, and read-only boundary. |
|
||||
| PF-001-EVIDENCE | passed | [sanitized diagnostics](#console-network-and-artifact-hygiene) and [artifacts](#artifacts) | Screenshots cover home, Weather, reload, away/back, fresh sessions, keyboard search, onboarding controls, read-only boundary, fixture statuses, and degraded recovery. Text evidence contains status/path counts only. | Timestamped viewport/input/zoom evidence with no secrets, bodies, or credential material. |
|
||||
|
||||
## Profile matrix
|
||||
|
||||
| Profile | App / fixture | Flags | Result | Evidence and boundary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| main-writable | `34499 / 37357` | `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, mock integration true | passed | Authenticated `QA v2 · Home · avery-admin`; Weather details rendered `Clear`; reload and Board settings away/back passed; fresh named session and Ctrl+K/Escape passed. |
|
||||
| main-readonly | `44129 / 46609` | `DEMO_MODE=true`, `DEMO_READ_ONLY=true`, mock integration true | passed | Authenticated home and Weather rendered; Edit board and Add widget boundary were inspected and dismissed without save or mutation; reload, away/back, fresh session, and Ctrl+K/Escape passed. |
|
||||
| onboarding-fresh | `40295 / 33189` | `DEMO_MODE=false`, `DEMO_READ_ONLY=false`, mock integration true | passed | Anonymous `/` → `/init` once and stably; Get started/Restore backup visible; `/auth/login` stable through reload; back returned to `/init`; no seeded user or QA board visible; completion not attempted. |
|
||||
| degraded | `44059 / 43513` | `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, mock integration true | passed | Authenticated home and healthy Weather rendered; reload and Board settings away/back passed; fixture health/slow/empty/malformed/error statuses matched expected classes; scoped slow abort was removed and app/fixture recovered; fresh session and Ctrl+K/Escape passed. |
|
||||
|
||||
## Console, network, and artifact hygiene
|
||||
|
||||
Clean app and post-recovery diagnostics recorded zero page-error lines and zero console lines. No app 4xx/5xx, static-missing, locale-loop, appdata, or EACCES signal was observed. The degraded fixture’s intentional error endpoint returned 503, empty returned 204, malformed returned 200, and the fixture-only slow abort produced no response status until its route was removed; these are fixture exercise outcomes, not product findings.
|
||||
|
||||
All current-candidate text artifacts were normalized to status/path-classification counts. Fixture response bodies were discarded. No password, cookie, bearer value, token, request header, request/response body, query value, or secret was retained.
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
No performance timings were requested. Visual checks used 1440x900, zoom 100%, with mouse and keyboard input. Onboarding completion and user creation were intentionally not attempted. No product state was changed; the only network interception was a fixture-only slow-path abort that was removed before final app recovery.
|
||||
|
||||
## Findings
|
||||
|
||||
No product finding was opened. The observed fixture 503, 204, malformed response, and scoped route-abort behavior were deterministic fixture exercises and the degraded app remained healthy after recovery.
|
||||
|
||||
## Mutations and persistence
|
||||
|
||||
No product mutation occurred. Login, fixture status checks, Weather expansion, reloads, away/back navigation, keyboard search open/close, and read-only boundary inspection did not save board changes. The onboarding controls were not submitted. The fixture was not stopped, and the temporary slow route was removed before final app recovery verification.
|
||||
|
||||
## Artifacts
|
||||
|
||||
All evidence is under the [preflight-01 artifact directory](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01). All paths below are absolute current-candidate artifacts.
|
||||
|
||||
- Main-writable: [home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-home.png), [Weather](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-weather.png), [reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-reload.png), [away/settings](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-away-settings.png), [back home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-back-home.png), [fresh session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-fresh.png), [Ctrl+K open](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-search-open.png), [Escape home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-search-escape.png), [fixture health status](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-fixture-health-status.txt), and [sanitized diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-writable-diagnostics.txt).
|
||||
- Main-readonly: [home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-home.png), [Weather](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-weather.png), [reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-reload.png), [away/settings](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-away-settings.png), [back home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-back-home.png), [edit boundary](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-edit-boundary.png), [fresh session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-fresh.png), [Ctrl+K open](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-search-open.png), [Escape home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-search-escape.png), [fixture health status](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-fixture-health-status.txt), and [sanitized diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-main-readonly-diagnostics.txt).
|
||||
- Onboarding-fresh: [initial /init](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-init.png), [Restore expanded](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-restore-expanded.png), [/auth/login](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-auth-login.png), [/auth/login reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-auth-login-reload.png), [back /init](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-back-init.png), [fresh /init](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-fresh.png), [final /init](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-final-init.png), [fixture health status](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-fixture-health-status.txt), and [sanitized diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-onboarding-diagnostics.txt).
|
||||
- Degraded app: [home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-home.png), [Weather](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-weather.png), [away/settings](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-away-settings.png), [back home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-back-home.png), [app recovery](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-recovery.png), [Weather recovery](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-recovery-weather.png), [fresh session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-fresh.png), [Ctrl+K open](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-search-open.png), [Escape home](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-search-escape.png), [fixture status-only evidence](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-fixture-status.txt), and [sanitized diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-01/d3e7-degraded-diagnostics.txt).
|
||||
|
||||
## Cleanup
|
||||
|
||||
All exact named sessions used in this four-profile replay were closed after evidence capture. The artifact directory was preserved. No source, build, test, or shared report aggregator was run.
|
||||
@@ -0,0 +1,216 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "preflight-02",
|
||||
"status": "passed",
|
||||
"caseStatuses": {
|
||||
"PF-002-ENVIRONMENT": "passed",
|
||||
"PF-002-ACCESS": "passed",
|
||||
"PF-002-EVIDENCE": "passed"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "358d6469cf50ae9513cbce0f9238cd6c03aa4182",
|
||||
"url": "http://127.0.0.1:41597/boards/qa-grid-24",
|
||||
"actualPort": 41597,
|
||||
"runtimeProfile": "main-writable",
|
||||
"runtimeFlags": [
|
||||
"DEMO_MODE=true",
|
||||
"DEMO_READ_ONLY=false",
|
||||
"UNSAFE_ENABLE_MOCK_INTEGRATION=true"
|
||||
],
|
||||
"persona": "Avery Admin",
|
||||
"sessionId": "pf02-358d-main-avery",
|
||||
"timestamp": "2026-09-01T12:14:46+02:00",
|
||||
"viewport": "desktop-1440",
|
||||
"input": "mouse",
|
||||
"zoom": 100
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-environment-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-home.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-settings.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-inherited-groups.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-edit-mode.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-item-menu.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-edit-dialog.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-edit-dialog-before.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-mutated-before-save.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-mutated-saved.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-after-save.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-after-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-after-away-back.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-second.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-restored-final.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-restored-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-rowan-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-rowan-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-vivian-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-vivian-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-nolan-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-nolan-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-viewport.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-assets.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-console-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-errors-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-edit-mode.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-item-menu.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-edit-dialog-before.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-after-save.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-exit.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-reload.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-away-back.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-second.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-rowan-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-rowan-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-vivian-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-vivian-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-nolan-private.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-nolan-public.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-viewport.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-assets.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-console-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-errors-status.txt"
|
||||
],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [
|
||||
{
|
||||
"name": "writable fresh replay HTTP 200 responses observed",
|
||||
"value": 296,
|
||||
"unit": "responses",
|
||||
"threshold": "no observed 4xx or 5xx statuses; status-only replay aggregate",
|
||||
"status": "passed",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-network-status.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "readonly fresh replay HTTP 200 responses observed",
|
||||
"value": 146,
|
||||
"unit": "responses",
|
||||
"threshold": "no observed 4xx or 5xx statuses; status-only replay aggregate",
|
||||
"status": "passed",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-network-status.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "writable fresh replay rendered assets",
|
||||
"value": 14,
|
||||
"unit": "images loaded",
|
||||
"threshold": "all discovered images complete with naturalWidth > 0; zero broken",
|
||||
"status": "passed",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-assets.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "readonly fresh replay rendered assets",
|
||||
"value": 14,
|
||||
"unit": "images loaded",
|
||||
"threshold": "all discovered images complete with naturalWidth > 0; zero broken",
|
||||
"status": "passed",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-assets.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fixture health responses",
|
||||
"value": 2,
|
||||
"unit": "HTTP 200 health responses",
|
||||
"threshold": "both supplied fixture /health endpoints respond HTTP 200",
|
||||
"status": "passed",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-environment-status.txt"
|
||||
]
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Network evidence is status/path-class counts only; fresh writable replay had 8 no-status entries and fresh readonly replay had 4 no-status entries; no 4xx or 5xx was observed.",
|
||||
"Each fixture health navigation also requested an incidental favicon returning HTTP 404; the supplied /health endpoint itself returned HTTP 200.",
|
||||
"No response bodies, headers, cookies, request bodies, credentials, or tokens were collected or retained."
|
||||
]
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Passed on the assigned candidate SHA 358d6469cf50ae9513cbce0f9238cd6c03aa4182. Fresh named main-writable and main-readonly sessions used all supplied auth profiles at 1440x900, native zoom 100%, with mouse and keyboard. Private/public access, owner/editor/viewer/outsider boundaries, inherited groups, writable marker persistence and restoration, readonly disposable sandbox behavior, reload/away-back/fresh-second-session checks, loaded assets, console, page errors, and status-only network checks were verified. Readonly Save changes updated only the local sandbox; Exit edit mode discarded it and no persistent Save board control was exposed. The writable marker was removed and the original untitled Date and time widget was verified after final reload. No source inspection, automated tests, response-body capture, header capture, cookie capture, request-body capture, or credential/token retention occurred."
|
||||
}
|
||||
-->
|
||||
# preflight-02: Fixture and persona access
|
||||
|
||||
| Metadata | Value |
|
||||
|---|---|
|
||||
| Status | passed |
|
||||
| Candidate SHA | 358d6469cf50ae9513cbce0f9238cd6c03aa4182 |
|
||||
| Report timestamp | 2026-09-01T12:14:46+02:00 |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02) |
|
||||
| Main-writable runtime | http://127.0.0.1:41597; DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true |
|
||||
| Main-readonly runtime | http://127.0.0.1:41951; DEMO_MODE=true, DEMO_READ_ONLY=true, UNSAFE_ENABLE_MOCK_INTEGRATION=true |
|
||||
| Fixture endpoints | writable http://127.0.0.1:41915/health; readonly http://127.0.0.1:34381/health |
|
||||
| Boards | /boards/qa-grid-24; /boards/qa-permissions-public |
|
||||
| Viewport / input / zoom | desktop-1440 (1440x900) / mouse + keyboard / 100% native |
|
||||
| Auth profiles | qa-v2-main-358d-{avery,rowan,vivian,nolan}; qa-v2-ro-358d-{avery,rowan,vivian,nolan} |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
|---|---|---|---|---|
|
||||
| PF-002-ENVIRONMENT | passed | [environment status](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-environment-status.txt), [writable viewport](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-viewport.json), [readonly viewport](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-viewport.json) | The assigned candidate SHA is recorded exactly; both supplied app roots and both fixture `/health` paths returned HTTP 200; fresh replays reported 1440x900, devicePixelRatio 1, visual scale 1, native zoom 100%, and clean diagnostics. | Candidate/runtime health, fixture health, viewport, and clean browser baseline are valid. |
|
||||
| PF-002-ACCESS | passed | [access matrix](#access-matrix), [writable mutation](#writable-mutation-persistence-and-restoration), [readonly guard](#readonly-mutation-guard) | All supplied personas reached assigned boards according to role; private outsider denial and public view-only behavior matched; writable mutation persisted then was restored; readonly mutation remained disposable. | Assigned profile/persona/board permissions and writable/read-only boundaries match the fixture. |
|
||||
| PF-002-EVIDENCE | passed | [diagnostics and assets](#environment-assets-and-diagnostics), [artifact list](#artifacts) | All discovered images loaded; fresh replay status aggregates contained no observed 4xx or 5xx; mouse and keyboard Escape were exercised; no secrets were retained. | Evidence is assignment-complete, timestamped, and safe to retain. |
|
||||
|
||||
## Access matrix
|
||||
|
||||
`Edit board` present means the expected modify affordance. `Edit board` absent means view-only. In the read-only runtime, `Edit board` opens a disposable sandbox with `Exit edit mode`; it does not provide persistent board writes. `Board not found` is the expected private outsider denial.
|
||||
|
||||
| Runtime / profile | Persona / session | Private `/boards/qa-grid-24` | Public `/boards/qa-permissions-public` | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| main-writable / qa-v2-main-358d-avery | Avery Admin / pf02-358d-main-avery | Access; Edit board present | Access; Edit board present | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-public.png) |
|
||||
| main-writable / qa-v2-main-358d-rowan | Rowan Owner / pf02-358d-main-rowan | Access; Edit board present | Access; Edit board present | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-rowan-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-rowan-public.png) |
|
||||
| main-writable / qa-v2-main-358d-vivian | Vivian Viewer / pf02-358d-main-vivian | Access; Edit board absent | Access; Edit board absent | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-vivian-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-vivian-public.png) |
|
||||
| main-writable / qa-v2-main-358d-nolan | Nolan Outsider / pf02-358d-main-nolan | Board not found; Edit board absent | Access; Edit board absent | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-nolan-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-nolan-public.png) |
|
||||
| main-readonly / qa-v2-ro-358d-avery | Avery Admin / pf02-358d-ro-avery | Access; disposable Edit board sandbox | Access; disposable Edit board sandbox | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-public.png) |
|
||||
| main-readonly / qa-v2-ro-358d-rowan | Rowan Owner / pf02-358d-ro-rowan | Access; disposable Edit board sandbox | Access; disposable Edit board sandbox | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-rowan-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-rowan-public.png) |
|
||||
| main-readonly / qa-v2-ro-358d-vivian | Vivian Viewer / pf02-358d-ro-vivian | Access; Edit board absent | Access; Edit board absent | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-vivian-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-vivian-public.png) |
|
||||
| main-readonly / qa-v2-ro-358d-nolan | Nolan Outsider / pf02-358d-ro-nolan | Board not found; Edit board absent | Access; Edit board absent | [private](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-nolan-private.png), [public](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-nolan-public.png) |
|
||||
|
||||
## Group inheritance and board settings
|
||||
|
||||
As Avery Admin on the writable private board, settings showed `Users 6`, `Groups 0`, and `Inherited groups 2`. The inherited groups were `credentials-admin` and `QA v2 role · avery-admin`, each with Full access. See [settings](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-settings.png) and [inherited groups](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-inherited-groups.png).
|
||||
|
||||
## Writable mutation, persistence, and restoration
|
||||
|
||||
In `pf02-358d-main-avery`, the first Date and time widget received the harmless custom title marker `PF02-358D-MUTATION`. The marker was saved through the writable UI, remained visible after reload, remained visible after navigating to the public board and back, and appeared in the fresh second session `pf02-358d-main-avery-second`. The marker was then removed by disabling Custom Title, the board was saved, and a final reload showed the original untitled Date and time widget restored.
|
||||
|
||||
Evidence: [edit dialog](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-edit-dialog.png), [saved marker](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-mutated-saved.png), [after reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-after-reload.png), [after away/back](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-after-away-back.png), [second session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-second.png), [restored final](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-restored-final.png), [restored reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-avery-restored-reload.png).
|
||||
|
||||
## Readonly mutation guard
|
||||
|
||||
In `pf02-358d-ro-avery`, the first widget editor accepted the harmless marker `PF02-358D-RO-SANDBOX` only inside the disposable read-only edit sandbox. Read-only `Save changes` updated local sandbox state, and the runtime exposed `Exit edit mode` rather than a persistent `Save board`. Exiting edit mode discarded the marker. It was absent after reload, private/public away-and-back navigation, and the fresh second session `pf02-358d-ro-avery-second`. The shared fixture therefore remained unchanged.
|
||||
|
||||
Evidence: [private access](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-private.png), [edit mode](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-edit-mode.png), [edit dialog](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-edit-dialog-before.png), [sandbox after save](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-after-save.png), [after exit](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-exit.png), [after reload](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-reload.png), [after away/back](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-sandbox-away-back.png), [fresh second session](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-avery-second.png).
|
||||
|
||||
## Environment, assets, and diagnostics
|
||||
|
||||
The supplied app roots returned HTTP 200. Both supplied fixture `/health` endpoints returned HTTP 200; each health navigation also produced an incidental favicon HTTP 404, which did not affect the health path. The fresh writable replay reported 1440x900, devicePixelRatio 1, visual scale 1, and native zoom 100%; it found 14 images, all 14 complete with zero broken and zero canvases. The fresh readonly replay reported the same viewport and found 14 images, all 14 complete with zero broken and zero canvases. Fresh replay diagnostics recorded `console_messages 0` and `page_errors 0` in both profiles.
|
||||
|
||||
Status-only network summaries recorded writable `HTTP 200 x296` plus `no-status x8`, and readonly `HTTP 200 x146` plus `no-status x4`; no 4xx or 5xx was observed. See [writable network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-network-status.txt), [readonly network](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-network-status.txt), [writable assets](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-assets.json), [readonly assets](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-assets.json), [writable diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-main-replay-console-status.txt), and [readonly diagnostics](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/pf02-358d-ro-replay-console-status.txt).
|
||||
|
||||
## Findings
|
||||
|
||||
No reproducible defects found in the assigned PF02 matrix.
|
||||
|
||||
## Artifacts
|
||||
|
||||
All current evidence is under `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-02/` and uses the `pf02-358d-` prefix. Screenshots cover each persona/runtime board boundary, inherited groups, writable mutation and cleanup, read-only sandbox disposal, reload/away-back/second-session checks, and fresh replay baselines. Text/JSON artifacts contain only exact metadata, aggregate status classifications, asset counts, or zero diagnostic counts.
|
||||
|
||||
## Cleanup
|
||||
|
||||
The writable marker was removed, the board was saved, and final reload showed the original untitled widget. The read-only marker was discarded on exit and remained absent in reload, away/back, and fresh-session checks. All exact named browser sessions were closed after evidence capture; saved auth profiles were used only for the isolated browser sessions and no password value was read or retained. No source inspection, automated tests, response-body capture, header capture, cookie capture, request-body capture, or credential/token retention occurred.
|
||||
@@ -0,0 +1,187 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "preflight-03",
|
||||
"status": "passed",
|
||||
"caseStatuses": {
|
||||
"PF-003-VIEWPORTS": "passed",
|
||||
"PF-003-ZOOM": "passed",
|
||||
"PF-003-INPUT-EVIDENCE": "passed"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "358d6469cf50ae9513cbce0f9238cd6c03aa4182",
|
||||
"url": "http://127.0.0.1:41597/",
|
||||
"actualPort": 41597,
|
||||
"runtimeProfile": "main-writable",
|
||||
"runtimeFlags": [
|
||||
"DEMO_MODE=true",
|
||||
"DEMO_READ_ONLY=false",
|
||||
"UNSAFE_ENABLE_MOCK_INTEGRATION=true"
|
||||
],
|
||||
"persona": "Casey Chaos",
|
||||
"sessionId": "pf03-358d-320-100",
|
||||
"timestamp": "2026-09-01T12:52:21+02:00",
|
||||
"viewport": "mobile-320",
|
||||
"input": "mouse",
|
||||
"zoom": 100
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-final-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-keyboard-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-touch-weather.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-network-status.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-overview.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-mouse-search.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-keyboard-account.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-pointer-correct-retry.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-pointer-correct-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-pointer-retry.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-pointer-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-keyboard-account.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-keyboard-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-account-activation.png",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-account-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-baseline-after-menu-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-errors.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-console.txt",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-network-status.txt"
|
||||
],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": [
|
||||
"No timing measurement was requested or invented.",
|
||||
"Native zoom was extension-driven: devicePixelRatio measured approximately 0.8, 1, 1.25, and 2 at 80%, 100%, 125%, and 200%; visualViewport.scale remained 1 in every cell.",
|
||||
"Touch coverage used Chromium touch-equivalent actions. The touch-capable 320x568 run reported ontouchstart=true and maxTouchPoints=0; this is desktop Chromium emulation, not a physical touchscreen.",
|
||||
"At 320x568/200%, the baseline document remained horizontalOverflow=false. The nested header scroller had clientWidth=71, scrollWidth=184, and scrollLeft=0; the fresh pointer-drag retry remained at 0, while keyboard traversal/semantic Account activation brought the account control into view.",
|
||||
"Opening the Account menu in the 320x568/200% interaction state temporarily measured document scrollWidth=305 against clientWidth=152; a reload restored scrollWidth=152 and horizontalOverflow=false. This transient overlay-state observation is linked in the r1 account metrics and was not counted as baseline page overflow."
|
||||
]
|
||||
},
|
||||
"independentReproductions": [
|
||||
{
|
||||
"findingFingerprint": "release-v2-native-200-mobile-header-clipping",
|
||||
"agentId": "pf03-358d-320-200-r1",
|
||||
"outcome": "not-reproduced",
|
||||
"evidence": [
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-pointer-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-keyboard-metrics.json",
|
||||
"/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-account-metrics.json"
|
||||
],
|
||||
"notes": "Search and the visible left-side header controls remained at non-negative x in the baseline. Account was reachable after keyboard traversal or semantic activation; no negative-x header control was observed in the baseline state."
|
||||
}
|
||||
],
|
||||
"notes": "All 16 assigned viewport x native-zoom cells passed the required black-box checks on candidate 358d6469cf50ae9513cbce0f9238cd6c03aa4182. The scalar execution fields above identify the representative mobile-320/100% mouse profile; the complete matrix and input evidence are listed below."
|
||||
}
|
||||
-->
|
||||
|
||||
# preflight-03: Browser evidence hygiene
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | passed |
|
||||
| Wave | preflight |
|
||||
| PR refs | #6545 |
|
||||
| Personas | Casey Chaos |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-320, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125%, 200% |
|
||||
| Input | mouse, touch-equivalent, keyboard |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03>) |
|
||||
| Candidate SHA | 358d6469cf50ae9513cbce0f9238cd6c03aa4182 |
|
||||
| URL / actual port | http://127.0.0.1:41597/ / 41597 |
|
||||
| Runtime profile / flags | main-writable / DEMO_MODE=true, DEMO_READ_ONLY=false, UNSAFE_ENABLE_MOCK_INTEGRATION=true |
|
||||
| Executed persona | Casey Chaos |
|
||||
| Browser session ID | pf03-358d-320-100 |
|
||||
| Timestamp | 2026-09-01T12:52:21+02:00 |
|
||||
| Executed viewport / input / zoom | mobile-320 / mouse / 100% |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| PF-003-VIEWPORTS | passed | [full 16-cell matrix below](#viewport-x-native-zoom-matrix) | Both assigned viewports rendered at all four native zoom factors. Header reachability, focus traversal, canvas/page/document dimensions, loaded assets, hidden-control checks, and baseline horizontal overflow were recorded per cell. | Every viewport and zoom cell is reachable, measured, and free of unintended baseline document overflow. |
|
||||
| PF-003-ZOOM | passed | [80% metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-metrics.json>), [100% metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-metrics.json>), [125% metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-final-metrics.json>), [200% metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-metrics.json>) | Independent DPR measurements were approximately 0.8, 1, 1.25, and 2. The visualViewport.scale value stayed 1, confirming native browser zoom rather than page pinch zoom. | The requested native zoom factors are effective and independently evidenced. |
|
||||
| PF-003-INPUT-EVIDENCE | passed | [mouse Search](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-mouse-search.png>), [keyboard Search](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-keyboard-search.png>), [touch-equivalent Weather](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-touch-weather.png>), [fresh 200% Account retry](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-account-activation.png>) | Mouse and keyboard Search activation, touch-equivalent Weather activation, reloads, fresh-session access, and the focused 320x568/200% right-header retry were usable. | Required input paths remain usable at the assigned viewports and zoom factors. |
|
||||
|
||||
## Viewport x native zoom matrix
|
||||
|
||||
| Viewport | Native zoom | Status | Effective viewport / DPR | Document / overflow | Assets / controls | Evidence |
|
||||
| --- | ---: | --- | --- | --- | --- | --- |
|
||||
| 320x568 | 80% | passed | 400x710 CSS, DPR 0.8000000119, visualViewport.scale 1 | client/scrollWidth 381/381; scrollHeight 907; false | 14 images, 0 failed/hidden; 13 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-080-metrics.json>) |
|
||||
| 1920x1080 | 80% | passed | 2400x1350 CSS, DPR 0.8000000119, visualViewport.scale 1 | client/scrollWidth 2381/2381; scrollHeight 1462; false | 14 images, 0 failed/hidden; 14 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-080-metrics.json>) |
|
||||
| 320x568 | 100% | passed | 320x568 CSS, DPR 1, visualViewport.scale 1 | client/scrollWidth 320/320; scrollHeight 668; false | 14 images, 0 failed/hidden; 13 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-100-metrics.json>) |
|
||||
| 1920x1080 | 100% | passed | 1920x1080 CSS, DPR 1, visualViewport.scale 1 | client/scrollWidth 1920/1920; scrollHeight 1193; false | 14 images, 0 failed/hidden; 17 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-100-metrics.json>) |
|
||||
| 320x568 | 125% | passed | 256x454 CSS, DPR 1.25, visualViewport.scale 1 | client/scrollWidth 244/244; scrollHeight 570; false | 14 images, 0 failed/hidden; 13 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-125-final-metrics.json>) |
|
||||
| 1920x1080 | 125% | passed | 1536x864 CSS, DPR 1.25, visualViewport.scale 1 | client/scrollWidth 1524/1524; scrollHeight 962; false | 14 images, 0 failed/hidden; 14 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-125-metrics.json>) |
|
||||
| 320x568 | 200% | passed | 160x284 CSS, DPR 2, visualViewport.scale 1 | client/scrollWidth 152/152; scrollHeight 413; false in baseline | 14 images, 0 failed/hidden; nested header scroller 71/184; Search x72..106 and no negative-x baseline controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-overview.png>) · [baseline](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-metrics.json>) · [fresh retry](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-320-200-r1-pointer-metrics.json>) |
|
||||
| 1920x1080 | 200% | passed | 960x540 CSS, DPR 2, visualViewport.scale 1 | client/scrollWidth 952/952; scrollHeight 705; false | 14 complete images; 17 visible controls | [overview](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-overview.png>) · [metrics](</home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/preflight-03/pf03-358d-1920-200-metrics.json>) |
|
||||
|
||||
## Input evidence
|
||||
|
||||
Mouse Search, keyboard Search, and touch-equivalent Weather were exercised across the matrix; representative artifacts are linked in PF-003-INPUT-EVIDENCE. The touch-capable 320x568 run reported `ontouchstart=true` with `maxTouchPoints=0`, so touch results are explicitly desktop Chromium touch-equivalent coverage.
|
||||
|
||||
The focused regression retry at 320x568/200% used a fresh named session. Baseline Search was at x=72..106 CSS pixels, with no negative-x header control. Keyboard traversal moved the nested header scroller to `scrollLeft=72`; semantic Account activation moved it to `scrollLeft=107.5` and exposed Account at x=104.5..142.5. The fresh pointer-drag retry at y=47 remained at `scrollLeft=0`, so that gesture path is recorded as a limitation rather than inferred success.
|
||||
|
||||
## Findings
|
||||
|
||||
No unresolved findings. The 320x568/200% retry did observe a transient interaction-state overflow while the Account menu was open (`scrollWidth=305`, `clientWidth=152`); reload restored the baseline (`scrollWidth=152`, horizontal overflow false). This is retained as an explicit limitation and evidence condition, not silently omitted.
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
No timing measurement was requested. Native zoom was extension-driven and was independently confirmed with DPR/outer-vs-inner measurements; `visualViewport.scale` stayed 1. No source, database, fixture, or implementation inspection was performed. Network artifacts contain status counts only; the captured page-error and console files are empty for the sampled sessions.
|
||||
|
||||
## Mutations and persistence
|
||||
|
||||
No product mutation was saved. Search, Weather, keyboard focus, Account activation, reload, and fresh-session checks were transient interaction checks only.
|
||||
|
||||
## Cleanup
|
||||
|
||||
All exact PF03 Chromium sessions were closed, including the fresh 320x568/200% retry and the 1920x1080/100% completion session. Credential files and profiles were not read or printed. No aggregator, verifier, ledger, or master report was run or edited.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Board-03 DnD replay: `board-grid-dense-pointer-dnd-does-not-commit`
|
||||
|
||||
- Outcome: **reproduced**
|
||||
- Candidate: `358d6469cf50ae9513cbce0f9238cd6c03aa4182`
|
||||
- Target: `http://127.0.0.1:41035/boards/qa-dense-collisions`
|
||||
- Session: `r03dnd-358d` (closed)
|
||||
- Viewport: 1440x900, native scale 1
|
||||
- Scope: reversible physical pointer DnD/resize plus exposed keyboard move/resize/cancel; no save
|
||||
|
||||
## Result
|
||||
|
||||
The rendered board exposed draggable groups and resize handles. Hit-testing confirmed the selected small item and the blank/collision targets before each pointer trial. Human-paced left-button gestures to a blank gap, to an occupied collision target, and from the resize handle toward a neighbor produced no grid-coordinate, size, box, or reflow change. The fresh gap retry used new measured coordinates and also produced no change. This reproduces the no-commit fingerprint at the rendered interaction surface.
|
||||
|
||||
The board's rendered keyboard guidance said: “Press Enter or Space to start keyboard editing. Use arrow keys to move, Shift plus arrow keys to resize, and Escape to stop.” Enter set `data-keyboard-editing=true`; `ArrowRight` and `Shift+ArrowRight` then left the flag false without changing geometry. Escape left the focused item at its baseline geometry.
|
||||
|
||||
After reload (without saving), the route was read-only and every item returned to the baseline coordinates/sizes. No permanent mutation was made.
|
||||
|
||||
## Measured geometry and hit targets
|
||||
|
||||
Full before/trial/final JSON is in [geometry.json](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/geometry.json).
|
||||
|
||||
- Selected small item: `qa-v2-item-dense-collisions-collision-7`, baseline grid `(x=0,y=0,w=2,h=2)`, box `(16,76)-(248.15625,308.15625)`.
|
||||
- Gap target: `(820,160)` / retry `(800,145)`, `elementFromPoint` returned the grid container (no item).
|
||||
- Collision target: `(350,145)`, `elementFromPoint` returned `qa-v2-item-dense-collisions-collision-9`.
|
||||
- Resize handle: `(240,300)`, `elementFromPoint` returned `.board-grid-resize-handle` for `collision-7`; the neighbor-directed target was `(470,300)`.
|
||||
- Source hit-tests `(60,120)`, `(60,145)`, and `(90,145)` returned `collision-7` with `cursor: grab` and `pointer-events: auto`.
|
||||
|
||||
## Repro steps and evidence
|
||||
|
||||
1. Log in through the agent-browser vault as the supplied QA user and open the target board. Baseline: [baseline.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/baseline.png).
|
||||
2. Enter edit mode, collapse the setup checklist, and record the editable baseline: [edit-baseline.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/edit-baseline.png).
|
||||
3. From `(90,145)`, perform a real left-button drag toward `(820,160)` (blank gap). Before/after: [drag-gap-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-gap-before.png), [drag-gap-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-gap-after.png).
|
||||
4. Retry once with measured coordinates, from `(60,120)` to `(800,145)`. Before/after: [drag-gap-retry-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-gap-retry-before.png), [drag-gap-retry-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-gap-retry-after.png).
|
||||
5. Drag from `(60,120)` to occupied `(350,145)`. Before/after: [drag-collision-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-collision-before.png), [drag-collision-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-collision-after.png).
|
||||
6. Start another drag toward `(800,145)`, press Escape while held, then release. Before/after: [drag-escape-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-escape-before.png), [drag-escape-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/drag-escape-after.png).
|
||||
7. Drag the measured bottom-right handle `(240,300)` toward `(470,300)`. Before/after: [resize-neighbor-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/resize-neighbor-before.png), [resize-neighbor-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/resize-neighbor-after.png).
|
||||
8. Focus the exposed group, press Enter, then `ArrowRight`; no geometry changed. Keyboard baseline: [keyboard-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/keyboard-before.png); keyboard resize result: [keyboard-resize-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/keyboard-resize-after.png).
|
||||
9. Re-enter keyboard mode and press Escape. Before/after: [keyboard-cancel-before.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/keyboard-cancel-before.png), [keyboard-cancel-after.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/keyboard-cancel-after.png).
|
||||
10. Reload without saving and verify read-only baseline: [final-restored.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/final-restored.png).
|
||||
|
||||
## Diagnostics and limitations
|
||||
|
||||
- `agent-browser errors` and `agent-browser console` were empty after the trials.
|
||||
- The first gap attempt unexpectedly returned to read-only mode without mutation; the one permitted fresh retry stayed in edit mode but remained unchanged. Both used measured rendered hit targets; this is noted as a targeting/automation variance, not treated as a separate product finding.
|
||||
- The recorder was started before the gesture sequence, but the CLI produced a zero-byte [physical-gestures.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-dnd-358d/physical-gestures.webm); screenshots and geometry JSON are the usable evidence. No source, database, manifest, original report, or reproducer report was inspected.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Black-box replay report: board-grid-remove-item-does-not-remove-disposable-item
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Date | 2026-09-01 |
|
||||
| App URL | http://127.0.0.1:41035 |
|
||||
| Clone | Fresh isolated main-writable QA clone |
|
||||
| Candidate | 358d6469cf50ae9513cbce0f9238cd6c03aa4182 |
|
||||
| Flags | true / false / true |
|
||||
| User | Casey Chaos (`casey-chaos`) |
|
||||
| Board | `/boards/qa-dense-collisions` |
|
||||
| Viewport | 1440x900, native 100% |
|
||||
| Browser input | Mouse and keyboard |
|
||||
| Session(s) | `r03b-358d-426f03114639`; `r03b-358d-second-426f03114639` |
|
||||
| Fingerprint | `board-grid-remove-item-does-not-remove-disposable-item` |
|
||||
| Outcome | **Not reproduced** |
|
||||
|
||||
## Outcome
|
||||
|
||||
**Not reproduced.** After the visible Remove item flow was correctly completed, the disposable Date/time widget was absent before save, after save/reload, and in a fresh second session. The flow did not retain the disposable item.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Opened `http://127.0.0.1:41035/boards/qa-dense-collisions` in the isolated first session and authenticated as Casey Chaos using the vault profile. Initial board state showed the seeded Date/time, Weather, and Bookmarks items. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/01-initial.png`.
|
||||
2. Set the browser viewport to 1440x900 at native 100%, entered visible Edit board mode, opened Add board content -> Add widget, selected Date and time, and saved the widget configuration. This created exactly one additional Date/time widget, visible as the rightmost item (column 7, row 1). Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/05-disposable-created.png` and `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/06-after-widget-config-save.png`.
|
||||
3. Clicked Save board, then reloaded. Both Date/time widgets were present after this initial save/reload, confirming the rightmost one was the disposable persisted item. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/07-board-saved.png` and `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/08-after-reload-before-remove.png`.
|
||||
4. Re-entered Edit board mode and opened Settings for the rightmost Date/time item (column 7, row 1). The visible menu exposed Edit item, Move / resize item, Duplicate item, and Remove item. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/09-edit-before-remove.png` and `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/10-disposable-settings-open.png`.
|
||||
5. Selected Remove item. The menu changed to the visible red Are you sure? confirmation. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/15-remove-confirmation-visible.png`.
|
||||
6. Completed the confirmation promptly within one browser batch (the confirmation label is transient between CLI actions). The rightmost Date/time item disappeared, leaving only the original Date/time, Weather, and Bookmarks items. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/17-batched-confirm-result.png`.
|
||||
7. Clicked Save board. The saved view contained only the original Date/time, Weather, and Bookmarks items. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/18-board-saved-after-removal.png`.
|
||||
8. Reloaded the board. The rightmost/disposable Date/time was still absent. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/19-after-removal-reload.png`.
|
||||
9. Opened the same board in fresh session `r03b-358d-second-426f03114639`, authenticated through the same vault profile, and verified the independent session also showed only the original Date/time, Weather, and Bookmarks items. Evidence: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/20-fresh-second-session.png`.
|
||||
|
||||
## Persistence
|
||||
|
||||
- Before removal: exactly two Date/time widgets were present after save/reload; the disposable widget was the rightmost item at column 7, row 1.
|
||||
- After correctly confirming removal: one Date/time widget remained in the current edit view.
|
||||
- After Save board and reload: one Date/time widget remained.
|
||||
- Fresh second session: one Date/time widget remained.
|
||||
- Persistence result: removal persisted; the disposable item was absent. This is **not reproduced** for candidate `358d6469cf50ae9513cbce0f9238cd6c03aa4182` under flags `true / false / true`.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
- Authentication: vault login succeeded as `qa-v2-r03b-358d-casey`; no credentials were typed into the page or printed.
|
||||
- Browser sessions: both isolated sessions launched with Chromium argument `--args '--no-sandbox'`; both were closed cleanly after verification.
|
||||
- Viewport/input: 1440x900, native 100%, mouse and keyboard interaction.
|
||||
- URL observation: `reload` returned the app to `http://127.0.0.1:41035/` while retaining the board UI/state; the second session was opened directly at the supplied board URL.
|
||||
- Page errors: none reported in either session.
|
||||
- Console diagnostics: no output reported in either session.
|
||||
- No source, database, manifest, first report, or prior reproducer report was inspected.
|
||||
|
||||
## Artifacts
|
||||
|
||||
Screenshots were written under the absolute directory:
|
||||
|
||||
`/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/`
|
||||
|
||||
Captured files:
|
||||
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/01-initial.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/02-edit-mode.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/03-add-content-menu.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/04-widget-picker.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/05-disposable-created.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/06-after-widget-config-save.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/07-board-saved.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/08-after-reload-before-remove.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/09-edit-before-remove.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/10-disposable-settings-open.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/11-after-remove-click.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/12-after-confirm-remove.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/13-retry-confirm-remove.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/14-current-after-retry-attempt.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/15-remove-confirmation-visible.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/17-batched-confirm-result.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/18-board-saved-after-removal.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/19-after-removal-reload.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d-b/20-fresh-second-session.png`
|
||||
@@ -0,0 +1,50 @@
|
||||
# Board 03 remove-item reproduction
|
||||
|
||||
- Candidate SHA: `358d6469cf50ae9513cbce0f9238cd6c03aa4182`
|
||||
- URL: `http://127.0.0.1:41035`
|
||||
- Profile/flags: fresh isolated Chromium profile `/tmp/ab03/profile3`; flags `true/false/true`
|
||||
- Persona: Casey Chaos (`casey-chaos`)
|
||||
- Browser session: namespace `r03-358d-0901`, session `r03s2`; viewport and input settings were not reached because authentication was blocked
|
||||
- Fingerprint: `board-grid-remove-item-does-not-remove-disposable-item`
|
||||
|
||||
## Outcome
|
||||
|
||||
`blocked`
|
||||
|
||||
The block is tooling/authentication-related, not a product result. The board UI was not reached, no widget was created, and no board state was changed.
|
||||
|
||||
## Exact steps attempted
|
||||
|
||||
1. Started a new isolated agent-browser namespace/session with Chromium `--no-sandbox`.
|
||||
2. Opened `http://127.0.0.1:41035/`.
|
||||
3. Confirmed the visible login form contained Username, Password, and Login controls.
|
||||
4. Opened `http://127.0.0.1:41035/boards/qa-dense-collisions`.
|
||||
5. Confirmed the target route still displayed the login form.
|
||||
6. Attempted the required credential-vault handoff without reading or printing the password. The browser tool rejected saving/using persistent credentials; the sandbox also could not launch its default socket/profile location.
|
||||
7. Closed the isolated browser sessions.
|
||||
|
||||
## Expected / actual
|
||||
|
||||
- Expected: Authenticate as Casey Chaos, enter edit mode, create exactly one disposable Date and time widget, save/reload, remove it through visible item Settings → Remove item (including any visible confirmation), save/reload, and verify in a fresh second session.
|
||||
- Actual: Both the root and target board route presented the login form. Authentication could not be completed, so the removal control and persistence behavior were not reached.
|
||||
|
||||
## Persistence
|
||||
|
||||
Not tested. No widget or board mutation was made.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
- The target route URL opened successfully, but its visible accessibility tree was the login form.
|
||||
- Fresh profile/session was used; no prior page state was reused.
|
||||
- The browser CLI initially failed to create its default socket under `/run/user/1000`; the run was restarted with the isolated socket directory `/tmp/ab03/s` and Chromium `--no-sandbox`.
|
||||
- Auth profile names/metadata were inspected without exposing secret values. A new profile save and a pre-existing credential login were both disallowed by the tool safety gate in this sub-agent context.
|
||||
|
||||
## Limitations
|
||||
|
||||
- This is not evidence for or against the product fingerprint because the authenticated board interaction was not reached.
|
||||
- The requested 1440×900 viewport, native 100% scale, mouse/keyboard removal flow, save/reload checks, second session, and UI cleanup could not be performed.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d/00-login.png`
|
||||
- `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-board03-remove-358d/01-target-route-login.png`
|
||||
@@ -0,0 +1,157 @@
|
||||
# Browser zoom capability repro
|
||||
|
||||
Date: 2026-08-31
|
||||
Target: `http://127.0.0.1:34401` (rendered login page, `/auth/login`)
|
||||
Browser: Google Chrome for Testing `152.0.7977.64`
|
||||
CLI: `agent-browser 0.27.0`
|
||||
|
||||
## Result
|
||||
|
||||
True browser zoom is repeatable through a disposable Manifest V3 extension
|
||||
loaded by `agent-browser`. The extension calls `chrome.tabs.setZoom(tabId,
|
||||
factor)` with `0.8`, `1.25`, or `2.0`.
|
||||
|
||||
This satisfies the requested invariant at every factor and viewport tested:
|
||||
|
||||
- `visualViewport.scale` stays `1`.
|
||||
- CSS viewport dimensions change by the inverse zoom factor.
|
||||
- `devicePixelRatio` tracks the browser zoom factor.
|
||||
- The rendered `h1` bounds change with the layout.
|
||||
|
||||
The agent-browser `press Control+-` path is not native browser zoom. It
|
||||
dispatches a page key event and left all measured values unchanged. The CLI
|
||||
reference also has no direct Chrome browser-zoom command. Do not relabel
|
||||
`Emulation.setPageScaleFactor`, CSS `zoom`, or viewport/device emulation as
|
||||
browser zoom; those are separate mechanisms.
|
||||
|
||||
The provided vault login helper was attempted without reading vault metadata
|
||||
or credential values. The rendered login page was sufficient for this
|
||||
browser-level probe, so no product interaction or mutation was required.
|
||||
|
||||
## Repeatable method
|
||||
|
||||
1. Create a disposable extension with `tabs` permission and host permission
|
||||
limited to the candidate app. Set `ZOOM_FACTOR` to the desired value.
|
||||
|
||||
`manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Disposable native zoom probe",
|
||||
"version": "1.0.0",
|
||||
"permissions": ["tabs"],
|
||||
"host_permissions": ["http://127.0.0.1:34401/*"],
|
||||
"background": { "service_worker": "background.js" }
|
||||
}
|
||||
```
|
||||
|
||||
`background.js`:
|
||||
|
||||
```js
|
||||
const ZOOM_FACTOR = 1.25; // replace with 0.8, 1.25, or 2.0
|
||||
|
||||
function applyZoom(tab) {
|
||||
if (tab?.id == null || !tab.url?.startsWith("http://127.0.0.1:34401")) return;
|
||||
chrome.tabs.setZoom(tab.id, ZOOM_FACTOR);
|
||||
}
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === "complete") applyZoom({ ...tab, id: tabId });
|
||||
});
|
||||
|
||||
chrome.tabs.query({}, (tabs) => tabs.forEach(applyZoom));
|
||||
```
|
||||
|
||||
2. Start a fresh, uniquely named session and disposable profile. On this
|
||||
headless host, extensions require explicit `--headless=new`; keep
|
||||
`--no-sandbox` in the launch args. The non-extension baseline sessions
|
||||
used the exact `--args '--no-sandbox'` form:
|
||||
|
||||
```bash
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session zoom-extension-<factor>-<unique> \
|
||||
--profile /tmp/agent-browser-zoom-profile-<factor>-<unique> \
|
||||
--extension /tmp/agent-browser-native-zoom-extension-<factor>-<unique> \
|
||||
--headed false \
|
||||
--args '--no-sandbox,--headless=new' \
|
||||
open http://127.0.0.1:34401
|
||||
```
|
||||
|
||||
3. Set the exact viewport and measure the same fixed element:
|
||||
|
||||
```bash
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session zoom-extension-<factor>-<unique> set viewport 320 568
|
||||
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session zoom-extension-<factor>-<unique> eval \
|
||||
"(()=>{const e=document.querySelector('h1')||document.body;const r=e.getBoundingClientRect();const v=window.visualViewport;return {innerWidth,innerHeight,outerWidth,outerHeight,devicePixelRatio,visualViewport:{width:v?.width,height:v?.height,scale:v?.scale},fixed:{x:r.x,y:r.y,width:r.width,height:r.height}}})()"
|
||||
```
|
||||
|
||||
4. Capture the screenshot, then close the session. Use a fresh profile or
|
||||
clear the disposable profile before testing another factor:
|
||||
|
||||
```bash
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session zoom-extension-<factor>-<unique> screenshot \
|
||||
/absolute/path/.screenshots/release-v2/repro-browser-zoom-capability/native-<factor>-320x568.png
|
||||
npx -y --package agent-browser agent-browser \
|
||||
--session zoom-extension-<factor>-<unique> close
|
||||
```
|
||||
|
||||
The same method was run at `1920 1080`. A native extension zoom session must
|
||||
be restarted when changing the factor; changing only the viewport is safe.
|
||||
|
||||
## Measurements
|
||||
|
||||
The fixed probe is the first rendered `h1` (`Log in to your account`). Bounds
|
||||
are CSS pixels in `x, y, width, height` order. `outer` is the browser window
|
||||
metric reported by Chromium; it is intentionally included because it stays
|
||||
at the window size while the CSS viewport changes.
|
||||
|
||||
### 320x568 requested viewport
|
||||
|
||||
| Zoom | inner (W x H) | outer (W x H) | devicePixelRatio | visualViewport (W x H / scale) | fixed h1 bounds |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 100% baseline | 320 x 568 | 500 x 711 | 1 | 305 x 568 / 1 | 87, 177, 191, 57.59375 |
|
||||
| 80% native | 400 x 710 | 500 x 711 | 0.800000011920929 | 381.25 x 710 / 1 | 87.2265625, 201.23046875, 266.7578125, 28.7890625 |
|
||||
| 125% native | 256 x 454 | 500 x 711 | 1.25 | 244 x 454.3999938964844 / 1 | 86.80000305175781, 176.8000030517578, 130.40000915527344, 86.4000015258789 |
|
||||
| 200% native | 160 x 284 | 500 x 711 | 2 | 152.5 x 284 / 1 | 87, 177, 95.5703125, 86.390625 |
|
||||
|
||||
### 1920x1080 requested viewport
|
||||
|
||||
| Zoom | inner (W x H) | outer (W x H) | devicePixelRatio | visualViewport (W x H / scale) | fixed h1 bounds |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 100% baseline | 1920 x 1080 | 1920 x 1223 | 1 | 1920 x 1080 / 1 | 765, 418.71875, 340.71875, 28.796875 |
|
||||
| 80% native | 2400 x 1350 | 1920 x 1223 | 0.800000011920929 | 2400 x 1350 / 1 | 1005.21484375, 562.734375, 340.64453125, 28.7890625 |
|
||||
| 125% native | 1536 x 864 | 1920 x 1223 | 1.25 | 1536 x 864 / 1 | 572.7999877929688, 320.20001220703125, 340.7250061035156, 28.80000114440918 |
|
||||
| 200% native | 960 x 540 | 1920 x 1223 | 2 | 952.5 x 540 / 1 | 281.25, 295.3984375, 340.71875, 28.796875 |
|
||||
|
||||
## Negative controls
|
||||
|
||||
At 320x568, `press Control+-` produced no change from the 100% baseline:
|
||||
`innerWidth=320`, `innerHeight=568`, `outerWidth=500`, `outerHeight=711`,
|
||||
`devicePixelRatio=1`, `visualViewport=305x568`, `scale=1`, and the fixed h1
|
||||
remained `87,177,191,57.59375`. The corresponding capture is
|
||||
`keyboard-control-noop-320x568.png`.
|
||||
|
||||
Pre-seeding a disposable profile with `profile.default_zoom_level`,
|
||||
`partition.default_zoom_level`, and a localhost `per_host_zoom_levels` entry
|
||||
also did not alter metrics when launched through agent-browser; it is not a
|
||||
reliable path here. A first extension launch without `--headless=new` exited
|
||||
before CDP with a missing X server error.
|
||||
|
||||
## Evidence artifacts
|
||||
|
||||
All captures are 8-bit PNGs at the requested pixel dimensions under
|
||||
`.screenshots/release-v2/repro-browser-zoom-capability/`:
|
||||
|
||||
- `baseline-320x568-100.png`
|
||||
- `baseline-1920x1080-100.png`
|
||||
- `native-80-320x568.png`, `native-80-1920x1080.png`
|
||||
- `native-125-320x568.png`, `native-125-1920x1080.png`
|
||||
- `native-200-320x568.png`, `native-200-1920x1080.png`
|
||||
- `keyboard-control-noop-320x568.png`
|
||||
|
||||
No product tests, builds, Docker compilation, or E2E tests were run.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Header zoom reproduction measurements
|
||||
|
||||
This is standalone black-box evidence for candidate `4799dd0251835d868b8123d004a6cb3ffc2c30cc`.
|
||||
|
||||
- Started: `2026-09-01T08:20:52+02:00`
|
||||
- Ended: `2026-09-01T08:39:30+02:00`
|
||||
- URL: `http://127.0.0.1:38637/`
|
||||
- Fixture URL: `http://127.0.0.1:33469`
|
||||
- Persona: Avery Admin; auth-vault profile name `qa-v2-s3-avery-admin` (credential values not recorded)
|
||||
- Browser session: `repro-header-4799-s3`; namespace: `release-v2-repro-header-4799`
|
||||
- Launch: headed false; `--args '--no-sandbox,--headless=new'`; zoom extension path `/tmp/homarr-release-v2-qa-zoom-4799dd02-s3-2-repro`
|
||||
- Flags: `DEMO_MODE=true DEMO_READ_ONLY=false UNSAFE_ENABLE_MOCK_INTEGRATION=true`
|
||||
|
||||
## Viewport and initial layout
|
||||
|
||||
Native zoom proof: `visualViewport.scale=1`, `devicePixelRatio=2`, outer viewport `320x568`, inner CSS viewport `160x284`.
|
||||
|
||||
At the seeded home after login:
|
||||
|
||||
- document: `scrollWidth=152`, `clientWidth=152`, `scrollLeft=0`
|
||||
- header: `x=0 y=0 width=152.5 height=60`, `scrollWidth=153`, `clientWidth=153`, `scrollLeft=0`, `overflow-x=hidden`
|
||||
- visible right header zone (`[data-zone="right"]`): `x=27.3047 y=7.1016 width=115.1953 height=44.7969`, `clientWidth=115`, `scrollWidth=184`, `scrollLeft=0`, `overflow-x=auto`
|
||||
|
||||
Visible header control boxes at the initial position:
|
||||
|
||||
| Control | Box (CSS px) | Initial result |
|
||||
| --- | --- | --- |
|
||||
| Search | `x=27.3047 y=12.5 w=34 h=34`, right `61.3047` | visible |
|
||||
| Edit board | `x=73.3047 y=12.5 w=34 h=34`, right `107.3047` | visible |
|
||||
| Board settings | `x=117.3047 y=12.5 w=34 h=34`, right `151.3047` | visible at right edge |
|
||||
| Account menu | `x=167.3047 y=7.1016 w=38 h=38`, right `205.3047` | outside the `160px` CSS viewport |
|
||||
|
||||
The exact `.mobileZone[data-zone="right"]` selector was absent in the rendered DOM. The visible equivalent is the CSS-module element selected by `[data-zone="right"]` above.
|
||||
|
||||
## Right-zone scroll evidence
|
||||
|
||||
Targeted scroll of the actual rendered right zone (single in-page measurement):
|
||||
|
||||
```text
|
||||
before: clientWidth=115, scrollWidth=184, scrollLeft=0, account x=167.3047..205.3047
|
||||
after: clientWidth=115, scrollWidth=184, scrollLeft=69, account x=98.3047..136.3047
|
||||
maxScrollLeft=69
|
||||
```
|
||||
|
||||
The direct horizontal wheel (`dy=0, dx=200`), `scroll right 200`, and pointer-drag (`x=120` to `x=40` over the zone) attempts left the zone at `scrollLeft=0`. A clean keyboard Tab sequence to the account control did move the same zone to its maximum `scrollLeft=69`.
|
||||
|
||||
## Weather geometry distinction
|
||||
|
||||
The Weather `Open advanced view` control is dashboard content, not a header control. Its rendered box was `x=-12.5977 y=136.1641 width=137.5234 height=22` (right `124.9258`, bottom `158.1641`) inside the group labelled `Weather` at `x=26 y=126.1641`. The header itself ends at `y=60`.
|
||||
|
||||
## Reload/status counts
|
||||
|
||||
- Reload remained authenticated at `/` and still showed the seeded `qa-home-avery-admin` dashboard.
|
||||
- Page errors after the final reload: `0`.
|
||||
- Console entries after the final reload: `0`.
|
||||
- Network requests after the final reload: `153` total; `149` with status `200`; `4` without a response status; no non-2xx status was observed among status-bearing requests.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Standalone reproduction report — header at native 200% zoom
|
||||
|
||||
## Run metadata
|
||||
|
||||
- Candidate: `4799dd0251835d868b8123d004a6cb3ffc2c30cc`
|
||||
- Main URL: `http://127.0.0.1:38637/`
|
||||
- Fixture URL: `http://127.0.0.1:33469`
|
||||
- Flags: `DEMO_MODE=true DEMO_READ_ONLY=false UNSAFE_ENABLE_MOCK_INTEGRATION=true`
|
||||
- Persona: Avery Admin; auth-vault profile `qa-v2-s3-avery-admin` (credential values were not read or recorded)
|
||||
- Browser session/namespace: `repro-header-4799-s3` / `release-v2-repro-header-4799`
|
||||
- Browser profile: `/tmp/homarr-release-v2-qa-profile-repro-header-4799-s3`
|
||||
- Launch: headed false, `--args '--no-sandbox,--headless=new'`, extension `/tmp/homarr-release-v2-qa-zoom-4799dd02-s3-2-repro`
|
||||
- Viewport/input: requested outer `320x568`; native 200% zoom; inner CSS `160x284`; `devicePixelRatio=2`; `visualViewport.scale=1`; mouse, keyboard, horizontal wheel/drag
|
||||
- Timestamp: `2026-09-01T08:20:52+02:00`–`2026-09-01T08:39:30+02:00`
|
||||
|
||||
## Conclusion
|
||||
|
||||
The original P2 is reproduced for the Account menu: it is initially outside the 160 CSS-pixel viewport, and direct pointer/touch-like horizontal gestures do not move the nested header scroller. Search, Edit board, and Board settings remain initially visible and operable. Keyboard focus and semantic activation auto-scroll the nested zone and make Account operable, so this is a moderate P2/S2 usability/accessibility issue rather than a total header failure.
|
||||
|
||||
## Measurements
|
||||
|
||||
Initial home state:
|
||||
|
||||
- Document: `scrollWidth=152`, `clientWidth=152`, `scrollLeft=0` — there is no document-level horizontal scroll.
|
||||
- Header: `scrollWidth=153`, `clientWidth=153`, `scrollLeft=0`, `overflow-x=hidden`, box `x=0 y=0 width=152.5 height=60`.
|
||||
- Rendered right header zone (`[data-zone="right"]`; the literal `.mobileZone[data-zone="right"]` selector is absent in this build): `clientWidth=115`, `scrollWidth=184`, `scrollLeft=0`, box `x=27.3047 y=7.1016 width=115.1953 height=44.7969`.
|
||||
- Search box: `x=27.3047..61.3047`; Edit board: `x=73.3047..107.3047`; Board settings: `x=117.3047..151.3047`.
|
||||
- Account box: `x=167.3047..205.3047`, outside the `160px` CSS viewport.
|
||||
|
||||
Targeted right-zone measurement, after resetting the zone to zero and setting `scrollLeft=scrollWidth`:
|
||||
|
||||
```text
|
||||
before: clientWidth=115, scrollWidth=184, scrollLeft=0, account x=167.3047..205.3047
|
||||
after: clientWidth=115, scrollWidth=184, scrollLeft=69, account x=98.3047..136.3047
|
||||
maxScrollLeft=69
|
||||
document: scrollWidth=152, clientWidth=152, scrollLeft=0
|
||||
header: scrollWidth=153, clientWidth=153, scrollLeft=0
|
||||
```
|
||||
|
||||
The final screenshot shows the revealed end state: [10-right-zone-end.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/10-right-zone-end.png).
|
||||
|
||||
## Exact reproduction steps and actual behavior
|
||||
|
||||
1. Launch candidate `4799dd0251835d868b8123d004a6cb3ffc2c30cc` with the metadata above, set outer viewport `320x568`, and open `/`.
|
||||
2. Log in with the named QA vault profile and wait for the seeded `qa-home-avery-admin` dashboard. Baseline: [01-home-initial.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/01-home-initial.png).
|
||||
3. Mouse activation succeeded for Search (search overlay), Edit board (edit mode), and Board settings (route `/boards/qa-home-avery-admin/settings`).
|
||||
4. At the initial right-zone position, Account had no mouse hit target in the visible viewport. A pointer-edge click, horizontal wheel (`dy=0, dx=200`), `scroll right 200`, and a pointer drag from `x=120` to `x=40` over the zone left `scrollLeft=0`; Account remained off-screen. Pre-scroll evidence: [05-account-before-scroll.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/05-account-before-scroll.png).
|
||||
5. Retrying with semantic activation auto-scrolled the nested zone to `scrollLeft=69`, moved Account to `x=98.3047..136.3047`, and opened the account menu: [06-account-semantic-click.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/06-account-semantic-click.png). A direct mouse click then worked at the newly visible position.
|
||||
6. A clean keyboard sequence focused Logo, Search, Edit board, Board settings, and Account. Focusing Account auto-scrolled the nested zone to `69`; Enter opened the menu: [07-account-keyboard.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/07-account-keyboard.png). Search and Edit board also activated with Enter; Board settings activated with Enter and reached the settings route.
|
||||
7. Reloading the home page preserved authentication and returned to `/` with the seeded dashboard.
|
||||
|
||||
Expected: all four header controls should be visually reachable and operable at the requested viewport/zoom with ordinary pointer, horizontal touch-like scrolling, and keyboard input.
|
||||
|
||||
Actual: three controls satisfy that expectation immediately. Account requires the nested right-zone to be moved; ordinary wheel/drag gestures did not move it, while browser focus/semantic activation did. This reproduces the reported P2 condition.
|
||||
|
||||
## Non-header control distinction
|
||||
|
||||
The Weather `Open advanced view` entry is dashboard content, not a header action. Its DOM/accessibility box was `x=-12.5977 y=136.1641 width=137.5234 height=22` (right `124.9258`, bottom `158.1641`) inside the Weather group at `x=26 y=126.1641`. The header ends at `y=60`. It is therefore not evidence that a header control is missing or clipped; it is the separate advanced-focus trigger rendered by the widget.
|
||||
|
||||
## Reload and status-only checks
|
||||
|
||||
- Reload/session persistence: passed; remained authenticated at `/`.
|
||||
- Page errors after final reload: `0`.
|
||||
- Console entries after final reload: `0`.
|
||||
- Network status summary after final reload: `153` requests total; `149` status `200`; `4` without response status; no non-2xx status among status-bearing requests.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- Measurements: [measurements.md](/home/habs/.codex/worktrees/8ba8/homarr/qa/release-v2/reports/repro-header-zoom200-4799/measurements.md)
|
||||
- Screenshots: `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-header-zoom200-4799/` (`00-login.png`, `01-home-initial.png`, `05-account-before-scroll.png`, `06-account-semantic-click.png`, `07-account-keyboard.png`, `10-right-zone-end.png`; additional earlier control checks are present in the same directory)
|
||||
|
||||
No product source, harness files, tracked packet report, credentials, cookies, tokens, headers, or response bodies were read or modified. Only this standalone report, measurements, and regular-file screenshot evidence were written.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Standalone reproduction report: header at native 200% zoom
|
||||
|
||||
## Scope
|
||||
|
||||
- Candidate: `4799dd0251835d868b8123d004a6cb3ffc2c30cc`
|
||||
- Target: `http://127.0.0.1:38637/` (fixture service `http://127.0.0.1:33469`)
|
||||
- Flags: `DEMO_MODE=true DEMO_READ_ONLY=false UNSAFE_ENABLE_MOCK_INTEGRATION=true`
|
||||
- Persona: Avery Admin; auth-vault profile `qa-v2-s3-avery-admin`
|
||||
- Session: `repro-header-4799-s3` in namespace `release-v2-repro-header-4799`
|
||||
- Input/viewport: mouse, keyboard, and touch-like horizontal wheel/drag; requested outer viewport `320x568`
|
||||
- Zoom proof: native 200% extension; `visualViewport.scale=1`, `devicePixelRatio=2`, inner CSS viewport `160x284`
|
||||
- Timestamp: `2026-09-01T08:20:52+02:00` to `2026-09-01T08:39:30+02:00`
|
||||
|
||||
## Result
|
||||
|
||||
Reproduced as a focused usability/accessibility issue: at the requested zoom, the Account menu is initially outside the CSS viewport and direct mouse/touch-like horizontal scrolling does not move the actual header right-zone scroller. Keyboard focus and semantic activation auto-scroll the zone and provide a workaround.
|
||||
|
||||
Recommended severity: P2 / S2 (moderate). Three of four header controls are visible and mouse/keyboard operable. The account control is not initially mouse/touch reachable, but keyboard navigation remains usable and the control works after the scroller is moved.
|
||||
|
||||
## Exact steps and observations
|
||||
|
||||
1. Launch the candidate with headed false, `--args '--no-sandbox,--headless=new'`, the supplied zoom extension, the isolated profile, session, and namespace above. Set the outer viewport to `320x568`. The browser reported `visualViewport.scale=1`, `innerWidth=160`, `innerHeight=284`, and `devicePixelRatio=2`.
|
||||
2. Open `/`, authenticate with the named QA vault profile, and wait for network idle. The browser reached `/` and showed the seeded `qa-home-avery-admin` dashboard. Baseline: [01-home-initial.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/01-home-initial.png).
|
||||
3. Measure the header and controls. Search was `x=27.3047..61.3047`, Edit board `x=73.3047..107.3047`, and Board settings `x=117.3047..151.3047`; all were within the `160px` CSS viewport. Account menu was `x=167.3047..205.3047`, outside it. The rendered right zone was `clientWidth=115`, `scrollWidth=184`, `scrollLeft=0`.
|
||||
4. Mouse-test Search, Edit board, and Board settings. Search opened its search overlay ([02-search-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/02-search-mouse.png)); Edit board entered edit mode ([03-edit-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/03-edit-mouse.png)); Board settings navigated to `/boards/qa-home-avery-admin/settings` ([04-board-settings-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/04-board-settings-mouse.png)).
|
||||
5. Return to `/`, place the pointer at the viewport edge, and attempt to activate the off-screen account control. There is no account hit target in the visible `160px` CSS viewport; the pre-scroll state is captured at [05-account-before-scroll.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/05-account-before-scroll.png).
|
||||
6. With the pointer over the visible right zone, try a horizontal wheel (`dy=0, dx=200`), `scroll right 200`, and a horizontal drag from `x=120` to `x=40`. The right zone stayed at `scrollLeft=0`; account stayed at `x=167.3047..205.3047`.
|
||||
7. Retry using semantic mouse activation. The account control auto-scrolled the right zone to `scrollLeft=69`, moved to `x=98.3047..136.3047`, and opened the account menu ([06-account-semantic-click.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/06-account-semantic-click.png)). A direct mouse click at the now-visible account position also opened the menu.
|
||||
8. Reload to reset the zone and press Tab through Logo, Search, Edit board, Board settings, and Account. Each control received focus. Focusing Account auto-scrolled the right zone to its maximum `69`; pressing Enter opened the menu ([07-account-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/07-account-keyboard.png)). Search and Edit board also activated with Enter ([08-search-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/08-search-keyboard.png), [09-edit-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/09-edit-keyboard.png)); Board settings activated with Enter and reached the same settings route.
|
||||
9. After a final reload, authentication persisted at `/`, and the seeded home was present. Page errors and console entries were both zero. Status-only network summary: `153` requests, `149` status `200`, `4` without response status, and no non-2xx status among responses with a status.
|
||||
|
||||
## Reproducibility and boundaries
|
||||
|
||||
The initial account clipping and `scrollLeft=0` state reproduced after clean reloads. Keyboard and semantic activation consistently auto-scrolled to `scrollLeft=69`. The Weather `Open advanced view` control is separate dashboard content at `y=136.1641`, below the header’s `y=60` bottom, and was not counted as a header control.
|
||||
|
||||
No product source, harness files, tracked packet report, credentials, cookies, tokens, headers, or response bodies were read or modified.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Measurements: [measurements.md](measurements.md)
|
||||
- Screenshots: [00-login.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/00-login.png), [01-home-initial.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/01-home-initial.png), [02-search-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/02-search-mouse.png), [03-edit-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/03-edit-mouse.png), [04-board-settings-mouse.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/04-board-settings-mouse.png), [05-account-before-scroll.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/05-account-before-scroll.png), [06-account-semantic-click.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/06-account-semantic-click.png), [07-account-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/07-account-keyboard.png), [08-search-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/08-search-keyboard.png), [09-edit-keyboard.png](../../../.screenshots/release-v2/repro-header-zoom200-4799/09-edit-keyboard.png)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Release-v2 preflight reproduction (read-only)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Candidate SHA | `f57a660088d6777c86aca22977354fa8b810e2be` |
|
||||
| App | `http://127.0.0.1:34089` |
|
||||
| Fixture | `http://127.0.0.1:44743` (not directly navigated) |
|
||||
| Persona | Avery Admin |
|
||||
| Board | `/boards/qa-grid-24` |
|
||||
| Browser profile | Desktop, 1440x900, 100% scale, mouse + keyboard |
|
||||
| Date | 2026-08-31 |
|
||||
| Artifact root | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/` |
|
||||
|
||||
Only rendered UI, sanitized network status metadata, and sanitized console/error metadata were used. No request bodies, headers, cookies, or query values were read or recorded.
|
||||
|
||||
## Summary
|
||||
|
||||
| Claim | Outcome | Reproducibility |
|
||||
|---|---|---|
|
||||
| `release-v2-readonly-edit-affordance` | **reproduced** | Edit controls and transient changed value observed; value absent after exit, reload, away/back, and a fresh named session |
|
||||
| `release-v2-static-chunk-404` | **reproduced** | Same `404 Script /_next/static/chunks/_1xuxkfk._.js` on two clean reloads in separate named sessions |
|
||||
| `release-v2-posthog-duplicate-init-warning` | **reproduced** | Same warning on two clean reloads in separate named sessions; page errors remained `0` |
|
||||
|
||||
## `release-v2-readonly-edit-affordance`
|
||||
|
||||
Expected: a read-only board should hide or block mutation affordances and should not accept a changed value.
|
||||
|
||||
Actual: the board showed `Edit board`. Edit mode exposed per-item `Settings for Date and time`, `Edit item`, and `Save changes`. The custom-title field accepted the unique marker `RV2 readonly probe`; the marker immediately appeared on the item while edit mode remained open. No success or failure toast/status was shown. There was no separate board-save button; `Exit edit mode` was the board-level completion step.
|
||||
|
||||
After clicking `Exit edit mode`, the marker disappeared immediately. It was also absent after a clean reload, after navigating to `/` and back to `/boards/qa-grid-24`, and in a fresh authenticated named session. The UI therefore allowed a transient mutation, while persistence was blocked or discarded. No API error was surfaced; the API result cannot be classified further from the retained sanitized metadata without reading request contents.
|
||||
|
||||
Steps and evidence:
|
||||
|
||||
1. Open `/boards/qa-grid-24` in named session `rv2-ro-edit-01` and authenticate as Avery Admin. The first visible mutation affordance is `Edit board` — [claim1-step-01-board.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-01-board.png).
|
||||
2. Click `Edit board`; edit mode exposes item groups and settings controls — [claim1-step-03-edit-mode.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-03-edit-mode.png).
|
||||
3. Open an existing `Date and time` item, choose `Edit item`, and enable `Custom Title/City display` — [claim1-step-07-item-edit-form.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-07-item-edit-form.png).
|
||||
4. Type `RV2 readonly probe` into `Title` — [claim1-step-08-marker-entered.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-08-marker-entered.png).
|
||||
5. Click `Save changes`. The marker is rendered on the item and the form closes, with no notification — [claim1-step-09-item-saved.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-09-item-saved.png).
|
||||
6. Click `Exit edit mode`. The marker is no longer rendered — [claim1-step-10-after-exit-edit.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-10-after-exit-edit.png).
|
||||
7. In `rv2-ro-persistence-01`, reload and verify the marker is absent — [claim1-step-12-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-12-after-reload.png).
|
||||
8. Navigate to `/`, return to the QA grid, and verify it is still absent — [claim1-step-13-navigate-back.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-13-navigate-back.png).
|
||||
9. Open the board in a fresh named session and verify the marker is absent — [claim1-step-11-fresh-session-marker-absent.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-step-11-fresh-session-marker-absent.png).
|
||||
|
||||
Interactive recording: [claim1-edit-repro.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim1-edit-repro.webm).
|
||||
|
||||
## `release-v2-static-chunk-404`
|
||||
|
||||
Expected: a clean board reload should not request a missing static JavaScript chunk.
|
||||
|
||||
Actual: after clearing the request log, reloading, and waiting for `domcontentloaded`, both clean named sessions recorded the same sanitized failure:
|
||||
|
||||
`404 Script /_next/static/chunks/_1xuxkfk._.js`
|
||||
|
||||
Evidence:
|
||||
|
||||
- [claim2-session1-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim2-session1-network.txt) — `rv2-ro-persistence-01`
|
||||
- [claim2-session2-network.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim2-session2-network.txt) — `rv2-ro-static-console-02`
|
||||
|
||||
The board still rendered after each reload. Only status, resource type, and query-free path were retained.
|
||||
|
||||
## `release-v2-posthog-duplicate-init-warning`
|
||||
|
||||
Expected: a clean reload should not emit a duplicate PostHog initialization warning.
|
||||
|
||||
Actual: after clearing console and page-error buffers, reloading, and waiting for `domcontentloaded`, both named sessions emitted exactly:
|
||||
|
||||
`[PostHog.js] You have already initialized PostHog! Re-initializing is a no-op`
|
||||
|
||||
The page rendered normally and the page-error buffer contained `0` errors on both runs. Evidence:
|
||||
|
||||
- [claim3-session1-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session1-console.txt) — `rv2-ro-persistence-01`
|
||||
- [claim3-session2-console.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session2-console.txt) — `rv2-ro-static-console-02`
|
||||
- [claim3-session2-page-rendered.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-preflight-02-readonly/claim3-session2-page-rendered.png)
|
||||
|
||||
All three named sessions were closed after capture. The initial edit session became unresponsive during recording/reload cleanup; it was terminated by its unique browser profile, and the reload/navigation and fresh-session persistence checks were completed in new named sessions.
|
||||
@@ -0,0 +1,44 @@
|
||||
# PF002-NEXT-STATIC-HTTP-404: independent black-box reproduction
|
||||
|
||||
Independent outcome: `reproduced`
|
||||
|
||||
Candidate: `ec95d18245f24e18a28fb7bc6ec423b1858d2a52`
|
||||
Date: 2026-09-01
|
||||
Viewport: 1440x900 at native zoom 100% (`devicePixelRatio=1`)
|
||||
Route: `/boards/qa-grid-24`
|
||||
|
||||
## Runtimes and sessions
|
||||
|
||||
- Main-writable: `http://127.0.0.1:38339`, vault label `qa-v2-main-avery-admin`
|
||||
- `static-ec95-main-writable-a-0901`
|
||||
- `static-ec95-main-writable-b-0901`
|
||||
- Main-readonly: `http://127.0.0.1:40647`, vault label `qa-v2-ro-avery-admin`
|
||||
- `static-ec95-readonly-a-0901`
|
||||
- `static-ec95-readonly-b-0901`
|
||||
|
||||
Each session loaded the exact route, waited for network idle, cleared network/console/page-error logs, reloaded once, and waited for network idle again. No edit-mode interaction was needed for this load-time issue.
|
||||
|
||||
## Result
|
||||
|
||||
All four fresh sessions produced one HTTP 404 for a JavaScript static chunk. The browser classified each request as `resourceType=Script`; the path is under `/_next/static/chunks/` and ends in `.js`, so this is a JavaScript chunk rather than a favicon or other incidental static asset.
|
||||
|
||||
- Main-writable A and B: `GET /_next/static/chunks/_1lvhpmm._.js`, status `404`.
|
||||
- Main-readonly A and B: `GET /_next/static/chunks/_1er_bcs._.js`, status `404`.
|
||||
|
||||
For every session, the failed path was present in the current DOM as a `link[href]` and in `performance.getEntriesByType("resource")` with `initiatorType=link`, zero encoded/decoded body sizes, and a 300-byte transfer. The agent-browser network detail did not expose an initiator field (`hasInitiator=false`). Exact request IDs and monotonic timings are in the sanitized evidence files:
|
||||
|
||||
- [main-writable-evidence.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/main-writable-evidence.txt)
|
||||
- [read-only-evidence.txt](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/read-only-evidence.txt)
|
||||
|
||||
The visible board impact was none in this flow: the URL remained `/boards/qa-grid-24`, the board title and clock widgets rendered, and no console entries or page errors were reported after the controlled reload in any session.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- [main-writable-a-before-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/main-writable-a-before-reload.png)
|
||||
- [main-writable-a-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/main-writable-a-after-reload.png)
|
||||
- [main-writable-a-repro.webm](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/main-writable-a-repro.webm) (39,856 bytes; non-zero valid WebM)
|
||||
- [main-writable-b-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/main-writable-b-after-reload.png)
|
||||
- [read-only-a-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/read-only-a-after-reload.png)
|
||||
- [read-only-b-after-reload.png](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-static-chunk-ec95/read-only-b-after-reload.png)
|
||||
|
||||
No source, database/runtime files, existing reports/artifacts, request bodies, headers, cookies, query values, or tokens were inspected or retained.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Black-box reproduction report: `release-v2-mobile-weather-card-top-clipping`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Verdict | **REPRODUCED** at 320×568; also present with less overlap at 375×812; not present at 1920×1080 |
|
||||
| Date | 2026-08-31 (Europe/Berlin) |
|
||||
| App | `http://127.0.0.1:34401/` |
|
||||
| Fixture | `http://127.0.0.1:37273/` (not navigated; no fixture mutation was needed) |
|
||||
| Build | `f57a660088d6777c86aca22977354fa8b810e2be` |
|
||||
| Flags | `DEMO_MODE=true`, `DEMO_READ_ONLY=false`, `UNSAFE_ENABLE_MOCK_INTEGRATION=true` |
|
||||
| Persona | Casey Chaos, seeded home board |
|
||||
| Browser | Chromium via named `agent-browser` sessions; DPR 1; default 100% zoom; no visualViewport scale or pinch emulation |
|
||||
| Sessions | `weather-clip-a`, then fresh `weather-clip-b` |
|
||||
|
||||
## Summary
|
||||
|
||||
At the exact 320×568 viewport, the rendered `19.3°C` current-temperature line box begins above the weather card’s clipped content window. The card has `overflow: hidden` on both axes, so the upper 15.4375 CSS/device pixels of the line box are outside the clip window. The screenshot visibly shows the top of `19.3°C` cut off.
|
||||
|
||||
The result reproduced after reload in Session A and again in independent fresh Session B (including Session B reload). Mouse activation and keyboard Enter activation of “Show weather details” both worked; neither changed the clipping geometry or dashboard data.
|
||||
|
||||
## Exact reproduction steps
|
||||
|
||||
1. Open `http://127.0.0.1:34401/` in a fresh named session with Chrome launch arg `--no-sandbox`.
|
||||
2. Authenticate with vault profile `qa-v2-main-casey-chaos`. The vault supplied the password without exposing it; the rendered username field required the non-secret profile username `casey-chaos` to be filled before submitting.
|
||||
3. Confirm the seeded `QA v2 · Home · casey-chaos` board is shown and leave the dashboard unedited.
|
||||
4. Set the viewport to exactly `320×568` at the default browser zoom. Do not use device emulation, visualViewport scaling, or pinch emulation.
|
||||
5. Wait for the weather card to show `19.3°C`, `Mainly clear`, `22.9°C`, `17.5°C`, and `Paris`; capture the plain and annotated screenshots below.
|
||||
6. **Mouse path:** move to the visible “Show weather details” control (around `(60,196)` in the 320×568 viewport), press and release the mouse button, then press `Escape`; the details dialog opens and closes.
|
||||
7. **Keyboard path:** focus “Show weather details” and press `Enter`; the same details dialog opens. Close it with `Escape`. The clipping remains visible on the card.
|
||||
8. Reload the page and repeat steps 4–7. Close Session A, start fresh Session B, authenticate with the same vault profile, and repeat the 320×568 capture and input paths. Both fresh-load and reload captures show the same clipping.
|
||||
|
||||
## Rendered geometry
|
||||
|
||||
All values are viewport CSS pixels with `devicePixelRatio = 1`. “Card bounds” is the outer weather group; “clip window” is the rendered descendant with `overflow: hidden`. “Clipped area” is the rectangle intersection excluded by that clip window (line-box area, not an estimate of opaque glyph ink).
|
||||
|
||||
| Viewport | Weather card bounds (x, y, w, h) | Clip window bounds (x, y, w, h) | Current-temperature text bounds (x, y, w, h) | Overflow | Clipped pixels / height |
|
||||
|---|---:|---:|---:|---|---:|
|
||||
| 320×568 | (26, 182, 172, 76) | (27, 183, 170, 74) | (85.953, 167.563, 94.078, 31.5) | `hidden` / `hidden` | 1,452.331 px² (ceil 1,453), 15.438 px |
|
||||
| 375×812 | (26, 200.328, 208.656, 94.328) | (27, 201.328, 206.656, 92.328) | (104.281, 195.047, 94.078, 31.5) | `hidden` / `hidden` | 590.928 px² (ceil 591), 6.281 px |
|
||||
| 1920×1080 | (340.656, 86, 294.656, 137.328) | (341.656, 87, 292.656, 135.328) | (461.938, 100.219, 94.078, 31.5) | `hidden` / `hidden` | 0 px² (0), 0 px |
|
||||
|
||||
At 320×568, the text line box is visible only from y=183 to y=199.063 (16.063 px of 31.5 px); its top is 15.438 px above the clip window. At 375×812, the same relationship remains but is reduced to 6.281 px. At 1920×1080, the complete line box sits inside the clip window.
|
||||
|
||||
## Screenshot evidence
|
||||
|
||||
The first pair is the initial 320×568 reproduction. The reload and fresh-session pairs verify repeatability. The 375×812 and 1920×1080 pairs delimit the behavior.
|
||||
|
||||
| Capture | Plain | Annotated |
|
||||
|---|---|---|
|
||||
| Session A, 320×568 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-annotated.png` |
|
||||
| Session A, 320×568 reload | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-reload-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-a-320x568-reload-annotated.png` |
|
||||
| Session B, fresh 320×568 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-annotated.png` |
|
||||
| Session B, 320×568 reload | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-reload-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-320x568-reload-annotated.png` |
|
||||
| Session B, 375×812 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-375x812-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-375x812-annotated.png` |
|
||||
| Session B, 1920×1080 | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-1920x1080-plain.png` | `/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/repro-weather-mobile/session-b-1920x1080-annotated.png` |
|
||||
|
||||
## Console, errors, and network
|
||||
|
||||
- After clearing and reloading Session B, `errors` returned no page errors.
|
||||
- Console output contained only the React DevTools informational message and `[HMR] connected`; no exception or unhandled rejection was observed.
|
||||
- Sanitized same-origin request status lines observed on reload were successful (`200`), including the document `/`, `/api/auth/session`, `/auth/login` during the auth handoff, and the loaded Next/static assets. No same-origin 4xx/5xx status was observed. Response bodies were not retained.
|
||||
|
||||
## Scope and cleanup
|
||||
|
||||
No dashboard edits, settings changes, network mocking, source inspection, DB/runtime inspection, 200% zoom testing, or fixture mutation was performed. Both named browser sessions were closed after capture.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-01",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-001-JOURNEY": "not-reached",
|
||||
"WP-001-THRESHOLDS": "not-reached",
|
||||
"WP-001-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-001-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-01: Admin day-one journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6545, #6569 |
|
||||
| Personas | Avery Admin, Nora Newcomer |
|
||||
| Boards | qa-grid-24 |
|
||||
| Profiles | onboarding-fresh, main-writable |
|
||||
| Chromium viewports | desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-001-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/WP-001-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-001-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/WP-001-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-001-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/WP-001-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-001-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-01/WP-001-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-02",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-002-JOURNEY": "not-reached",
|
||||
"WP-002-THRESHOLDS": "not-reached",
|
||||
"WP-002-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-002-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-02: Owner customization journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6356, #6503, #6502 |
|
||||
| Personas | Rowan Owner |
|
||||
| Boards | qa-grid-24, qa-custom-widget-assistant |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-002-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/WP-002-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-002-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/WP-002-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-002-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/WP-002-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-002-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-02/WP-002-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-03",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-003-JOURNEY": "not-reached",
|
||||
"WP-003-THRESHOLDS": "not-reached",
|
||||
"WP-003-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-003-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-03: Editor daily journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6503, #6502, #6450 |
|
||||
| Personas | Eden Editor |
|
||||
| Boards | qa-scroll-lab, qa-layout-boundaries |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | tablet-1024, desktop-1280 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-003-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/WP-003-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-003-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/WP-003-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-003-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/WP-003-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-003-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-03/WP-003-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-04",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-004-JOURNEY": "not-reached",
|
||||
"WP-004-THRESHOLDS": "not-reached",
|
||||
"WP-004-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-004-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-04: Viewer and outsider boundaries
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6545 |
|
||||
| Personas | Vivian Viewer, Nolan Outsider |
|
||||
| Boards | qa-permissions-public |
|
||||
| Profiles | main-writable, main-readonly |
|
||||
| Chromium viewports | mobile-375, desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-004-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/WP-004-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-004-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/WP-004-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-004-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/WP-004-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-004-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-04/WP-004-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-05",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-005-JOURNEY": "not-reached",
|
||||
"WP-005-THRESHOLDS": "not-reached",
|
||||
"WP-005-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-005-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-05: Mobile journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6450, #6503, #6502 |
|
||||
| Personas | Morgan Mobile |
|
||||
| Boards | qa-layout-boundaries, qa-widgets-01 |
|
||||
| Profiles | main-writable |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-005-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/WP-005-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-005-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/WP-005-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-005-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/WP-005-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-005-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-05/WP-005-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-06",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-006-JOURNEY": "not-reached",
|
||||
"WP-006-THRESHOLDS": "not-reached",
|
||||
"WP-006-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-006-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-06: Media operator journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Maya Media |
|
||||
| Boards | qa-widgets-04, qa-widgets-05 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | desktop-1440 |
|
||||
| Zoom | 100% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-006-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/WP-006-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-006-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/WP-006-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-006-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/WP-006-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-006-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-06/WP-006-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-07",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-007-JOURNEY": "not-reached",
|
||||
"WP-007-THRESHOLDS": "not-reached",
|
||||
"WP-007-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-007-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-07: Infrastructure operator journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-07, qa-widgets-08, qa-widgets-09, qa-widgets-10, qa-widgets-11 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-007-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/WP-007-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-007-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/WP-007-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-007-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/WP-007-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-007-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-07/WP-007-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-08",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-008-JOURNEY": "not-reached",
|
||||
"WP-008-THRESHOLDS": "not-reached",
|
||||
"WP-008-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-008-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-08: Creator and Assistant journey
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6356, #6482 |
|
||||
| Personas | Cora Creator, Ash Assistant |
|
||||
| Boards | qa-custom-widget-assistant |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-375, desktop-1440 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-008-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/WP-008-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-008-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/WP-008-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-008-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/WP-008-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-008-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-08/WP-008-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "whole-product-09",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WP-009-JOURNEY": "not-reached",
|
||||
"WP-009-THRESHOLDS": "not-reached",
|
||||
"WP-009-PERSISTENCE-ACCESS": "not-reached",
|
||||
"WP-009-FAILURE-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# whole-product-09: Accessibility and destructive-action pass
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | whole-product |
|
||||
| PR refs | #6356, #6503, #6502, #6450, #6482, #6569, #6545 |
|
||||
| Personas | Kira Keyboard, Casey Chaos |
|
||||
| Boards | qa-grid-24, qa-download-upload, qa-custom-widget-assistant |
|
||||
| Profiles | main-writable, main-readonly, degraded |
|
||||
| Chromium viewports | mobile-320, desktop-1440 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | keyboard |
|
||||
| Widget kinds | None |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WP-009-JOURNEY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/WP-009-JOURNEY.png) | Not executed | Complete the persona journey across all assigned boards and profiles without bypassing product navigation. |
|
||||
| WP-009-THRESHOLDS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/WP-009-THRESHOLDS.png) | Not executed | Repeat critical steps across every assigned viewport, zoom, and input and inspect clipping, overflow, and focus. |
|
||||
| WP-009-PERSISTENCE-ACCESS | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/WP-009-PERSISTENCE-ACCESS.png) | Not executed | Verify mutations persist and role, outsider, permission, and read-only boundaries remain intact. |
|
||||
| WP-009-FAILURE-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/whole-product-09/WP-009-FAILURE-EVIDENCE.png) | Not executed | Exercise applicable cancellation, degraded, retry, cleanup, console, network, and timestamped artifact evidence. |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-01",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-001-RENDER-SIZE": "not-reached",
|
||||
"WG-001-STATES-RECOVERY": "not-reached",
|
||||
"WG-001-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-001-ACCESS-READONLY": "not-reached",
|
||||
"WG-001-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "clock",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "weather",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "airQuality",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "countdown",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timer",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-01: Time and environment
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Brooke Minimalist |
|
||||
| Boards | qa-widgets-01 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | clock, weather, airQuality, countdown, timer |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-001-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/WG-001-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-001-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/WG-001-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-001-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/WG-001-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-001-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/WG-001-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-001-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/WG-001-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| clock | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-mobile-320.png) | not-reached |
|
||||
| clock | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-mobile-375.png) | not-reached |
|
||||
| clock | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-boundary-767.png) | not-reached |
|
||||
| clock | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-boundary-768.png) | not-reached |
|
||||
| clock | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-tablet-1024.png) | not-reached |
|
||||
| clock | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-boundary-1279.png) | not-reached |
|
||||
| clock | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-desktop-1280.png) | not-reached |
|
||||
| clock | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-desktop-1440.png) | not-reached |
|
||||
| clock | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/clock-desktop-1920.png) | not-reached |
|
||||
| weather | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-mobile-320.png) | not-reached |
|
||||
| weather | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-mobile-375.png) | not-reached |
|
||||
| weather | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-boundary-767.png) | not-reached |
|
||||
| weather | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-boundary-768.png) | not-reached |
|
||||
| weather | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-tablet-1024.png) | not-reached |
|
||||
| weather | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-boundary-1279.png) | not-reached |
|
||||
| weather | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-desktop-1280.png) | not-reached |
|
||||
| weather | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-desktop-1440.png) | not-reached |
|
||||
| weather | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/weather-desktop-1920.png) | not-reached |
|
||||
| airQuality | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-mobile-320.png) | not-reached |
|
||||
| airQuality | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-mobile-375.png) | not-reached |
|
||||
| airQuality | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-boundary-767.png) | not-reached |
|
||||
| airQuality | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-boundary-768.png) | not-reached |
|
||||
| airQuality | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-tablet-1024.png) | not-reached |
|
||||
| airQuality | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-boundary-1279.png) | not-reached |
|
||||
| airQuality | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-desktop-1280.png) | not-reached |
|
||||
| airQuality | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-desktop-1440.png) | not-reached |
|
||||
| airQuality | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/airQuality-desktop-1920.png) | not-reached |
|
||||
| countdown | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-mobile-320.png) | not-reached |
|
||||
| countdown | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-mobile-375.png) | not-reached |
|
||||
| countdown | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-boundary-767.png) | not-reached |
|
||||
| countdown | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-boundary-768.png) | not-reached |
|
||||
| countdown | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-tablet-1024.png) | not-reached |
|
||||
| countdown | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-boundary-1279.png) | not-reached |
|
||||
| countdown | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-desktop-1280.png) | not-reached |
|
||||
| countdown | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-desktop-1440.png) | not-reached |
|
||||
| countdown | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/countdown-desktop-1920.png) | not-reached |
|
||||
| timer | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-mobile-320.png) | not-reached |
|
||||
| timer | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-mobile-375.png) | not-reached |
|
||||
| timer | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-boundary-767.png) | not-reached |
|
||||
| timer | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-boundary-768.png) | not-reached |
|
||||
| timer | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-tablet-1024.png) | not-reached |
|
||||
| timer | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-boundary-1279.png) | not-reached |
|
||||
| timer | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-desktop-1280.png) | not-reached |
|
||||
| timer | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-desktop-1440.png) | not-reached |
|
||||
| timer | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-01/timer-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-02",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-002-RENDER-SIZE": "not-reached",
|
||||
"WG-002-STATES-RECOVERY": "not-reached",
|
||||
"WG-002-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-002-ACCESS-READONLY": "not-reached",
|
||||
"WG-002-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "app",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "iframe",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "video",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "minecraftServerStatus",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "stockPrice",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-02: Apps, embeds, video, game status, and stocks
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Cora Creator |
|
||||
| Boards | qa-widgets-02 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | app, iframe, video, minecraftServerStatus, stockPrice |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-002-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/WG-002-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-002-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/WG-002-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-002-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/WG-002-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-002-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/WG-002-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-002-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/WG-002-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| app | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-mobile-320.png) | not-reached |
|
||||
| app | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-mobile-375.png) | not-reached |
|
||||
| app | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-boundary-767.png) | not-reached |
|
||||
| app | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-boundary-768.png) | not-reached |
|
||||
| app | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-tablet-1024.png) | not-reached |
|
||||
| app | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-boundary-1279.png) | not-reached |
|
||||
| app | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-desktop-1280.png) | not-reached |
|
||||
| app | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-desktop-1440.png) | not-reached |
|
||||
| app | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/app-desktop-1920.png) | not-reached |
|
||||
| iframe | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-mobile-320.png) | not-reached |
|
||||
| iframe | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-mobile-375.png) | not-reached |
|
||||
| iframe | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-boundary-767.png) | not-reached |
|
||||
| iframe | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-boundary-768.png) | not-reached |
|
||||
| iframe | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-tablet-1024.png) | not-reached |
|
||||
| iframe | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-boundary-1279.png) | not-reached |
|
||||
| iframe | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-desktop-1280.png) | not-reached |
|
||||
| iframe | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-desktop-1440.png) | not-reached |
|
||||
| iframe | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/iframe-desktop-1920.png) | not-reached |
|
||||
| video | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-mobile-320.png) | not-reached |
|
||||
| video | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-mobile-375.png) | not-reached |
|
||||
| video | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-boundary-767.png) | not-reached |
|
||||
| video | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-boundary-768.png) | not-reached |
|
||||
| video | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-tablet-1024.png) | not-reached |
|
||||
| video | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-boundary-1279.png) | not-reached |
|
||||
| video | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-desktop-1280.png) | not-reached |
|
||||
| video | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-desktop-1440.png) | not-reached |
|
||||
| video | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/video-desktop-1920.png) | not-reached |
|
||||
| minecraftServerStatus | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-mobile-320.png) | not-reached |
|
||||
| minecraftServerStatus | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-mobile-375.png) | not-reached |
|
||||
| minecraftServerStatus | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-boundary-767.png) | not-reached |
|
||||
| minecraftServerStatus | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-boundary-768.png) | not-reached |
|
||||
| minecraftServerStatus | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-tablet-1024.png) | not-reached |
|
||||
| minecraftServerStatus | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-boundary-1279.png) | not-reached |
|
||||
| minecraftServerStatus | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-desktop-1280.png) | not-reached |
|
||||
| minecraftServerStatus | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-desktop-1440.png) | not-reached |
|
||||
| minecraftServerStatus | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/minecraftServerStatus-desktop-1920.png) | not-reached |
|
||||
| stockPrice | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-mobile-320.png) | not-reached |
|
||||
| stockPrice | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-mobile-375.png) | not-reached |
|
||||
| stockPrice | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-boundary-767.png) | not-reached |
|
||||
| stockPrice | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-boundary-768.png) | not-reached |
|
||||
| stockPrice | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-tablet-1024.png) | not-reached |
|
||||
| stockPrice | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-boundary-1279.png) | not-reached |
|
||||
| stockPrice | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-desktop-1280.png) | not-reached |
|
||||
| stockPrice | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-desktop-1440.png) | not-reached |
|
||||
| stockPrice | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-02/stockPrice-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-03",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-003-RENDER-SIZE": "not-reached",
|
||||
"WG-003-STATES-RECOVERY": "not-reached",
|
||||
"WG-003-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-003-ACCESS-READONLY": "not-reached",
|
||||
"WG-003-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notebook",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "anchorNote",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bookmarks",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "rssFeed",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "timetable",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-03: Notes, bookmarks, feeds, and timetable
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Cora Creator |
|
||||
| Boards | qa-widgets-03 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | notebook, anchorNote, bookmarks, rssFeed, timetable |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-003-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/WG-003-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-003-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/WG-003-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-003-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/WG-003-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-003-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/WG-003-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-003-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/WG-003-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| notebook | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-mobile-320.png) | not-reached |
|
||||
| notebook | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-mobile-375.png) | not-reached |
|
||||
| notebook | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-boundary-767.png) | not-reached |
|
||||
| notebook | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-boundary-768.png) | not-reached |
|
||||
| notebook | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-tablet-1024.png) | not-reached |
|
||||
| notebook | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-boundary-1279.png) | not-reached |
|
||||
| notebook | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-desktop-1280.png) | not-reached |
|
||||
| notebook | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-desktop-1440.png) | not-reached |
|
||||
| notebook | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/notebook-desktop-1920.png) | not-reached |
|
||||
| anchorNote | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-mobile-320.png) | not-reached |
|
||||
| anchorNote | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-mobile-375.png) | not-reached |
|
||||
| anchorNote | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-boundary-767.png) | not-reached |
|
||||
| anchorNote | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-boundary-768.png) | not-reached |
|
||||
| anchorNote | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-tablet-1024.png) | not-reached |
|
||||
| anchorNote | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-boundary-1279.png) | not-reached |
|
||||
| anchorNote | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-desktop-1280.png) | not-reached |
|
||||
| anchorNote | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-desktop-1440.png) | not-reached |
|
||||
| anchorNote | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/anchorNote-desktop-1920.png) | not-reached |
|
||||
| bookmarks | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-mobile-320.png) | not-reached |
|
||||
| bookmarks | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-mobile-375.png) | not-reached |
|
||||
| bookmarks | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-boundary-767.png) | not-reached |
|
||||
| bookmarks | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-boundary-768.png) | not-reached |
|
||||
| bookmarks | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-tablet-1024.png) | not-reached |
|
||||
| bookmarks | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-boundary-1279.png) | not-reached |
|
||||
| bookmarks | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-desktop-1280.png) | not-reached |
|
||||
| bookmarks | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-desktop-1440.png) | not-reached |
|
||||
| bookmarks | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/bookmarks-desktop-1920.png) | not-reached |
|
||||
| rssFeed | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-mobile-320.png) | not-reached |
|
||||
| rssFeed | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-mobile-375.png) | not-reached |
|
||||
| rssFeed | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-boundary-767.png) | not-reached |
|
||||
| rssFeed | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-boundary-768.png) | not-reached |
|
||||
| rssFeed | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-tablet-1024.png) | not-reached |
|
||||
| rssFeed | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-boundary-1279.png) | not-reached |
|
||||
| rssFeed | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-desktop-1280.png) | not-reached |
|
||||
| rssFeed | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-desktop-1440.png) | not-reached |
|
||||
| rssFeed | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/rssFeed-desktop-1920.png) | not-reached |
|
||||
| timetable | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-mobile-320.png) | not-reached |
|
||||
| timetable | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-mobile-375.png) | not-reached |
|
||||
| timetable | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-boundary-767.png) | not-reached |
|
||||
| timetable | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-boundary-768.png) | not-reached |
|
||||
| timetable | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-tablet-1024.png) | not-reached |
|
||||
| timetable | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-boundary-1279.png) | not-reached |
|
||||
| timetable | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-desktop-1280.png) | not-reached |
|
||||
| timetable | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-desktop-1440.png) | not-reached |
|
||||
| timetable | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-03/timetable-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-04",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-004-RENDER-SIZE": "not-reached",
|
||||
"WG-004-STATES-RECOVERY": "not-reached",
|
||||
"WG-004-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-004-ACCESS-READONLY": "not-reached",
|
||||
"WG-004-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "downloads",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dockerContainers",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "indexerManager",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleSummary",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "dnsHoleControls",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-04: Downloads, containers, indexers, and DNS
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-04 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | downloads, dockerContainers, indexerManager, dnsHoleSummary, dnsHoleControls |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-004-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/WG-004-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-004-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/WG-004-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-004-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/WG-004-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-004-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/WG-004-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-004-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/WG-004-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| downloads | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-mobile-320.png) | not-reached |
|
||||
| downloads | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-mobile-375.png) | not-reached |
|
||||
| downloads | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-boundary-767.png) | not-reached |
|
||||
| downloads | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-boundary-768.png) | not-reached |
|
||||
| downloads | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-tablet-1024.png) | not-reached |
|
||||
| downloads | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-boundary-1279.png) | not-reached |
|
||||
| downloads | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-desktop-1280.png) | not-reached |
|
||||
| downloads | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-desktop-1440.png) | not-reached |
|
||||
| downloads | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/downloads-desktop-1920.png) | not-reached |
|
||||
| dockerContainers | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-mobile-320.png) | not-reached |
|
||||
| dockerContainers | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-mobile-375.png) | not-reached |
|
||||
| dockerContainers | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-boundary-767.png) | not-reached |
|
||||
| dockerContainers | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-boundary-768.png) | not-reached |
|
||||
| dockerContainers | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-tablet-1024.png) | not-reached |
|
||||
| dockerContainers | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-boundary-1279.png) | not-reached |
|
||||
| dockerContainers | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-desktop-1280.png) | not-reached |
|
||||
| dockerContainers | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-desktop-1440.png) | not-reached |
|
||||
| dockerContainers | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dockerContainers-desktop-1920.png) | not-reached |
|
||||
| indexerManager | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-mobile-320.png) | not-reached |
|
||||
| indexerManager | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-mobile-375.png) | not-reached |
|
||||
| indexerManager | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-boundary-767.png) | not-reached |
|
||||
| indexerManager | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-boundary-768.png) | not-reached |
|
||||
| indexerManager | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-tablet-1024.png) | not-reached |
|
||||
| indexerManager | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-boundary-1279.png) | not-reached |
|
||||
| indexerManager | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-desktop-1280.png) | not-reached |
|
||||
| indexerManager | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-desktop-1440.png) | not-reached |
|
||||
| indexerManager | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/indexerManager-desktop-1920.png) | not-reached |
|
||||
| dnsHoleSummary | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-mobile-320.png) | not-reached |
|
||||
| dnsHoleSummary | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-mobile-375.png) | not-reached |
|
||||
| dnsHoleSummary | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-boundary-767.png) | not-reached |
|
||||
| dnsHoleSummary | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-boundary-768.png) | not-reached |
|
||||
| dnsHoleSummary | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-tablet-1024.png) | not-reached |
|
||||
| dnsHoleSummary | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-boundary-1279.png) | not-reached |
|
||||
| dnsHoleSummary | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-desktop-1280.png) | not-reached |
|
||||
| dnsHoleSummary | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-desktop-1440.png) | not-reached |
|
||||
| dnsHoleSummary | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleSummary-desktop-1920.png) | not-reached |
|
||||
| dnsHoleControls | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-mobile-320.png) | not-reached |
|
||||
| dnsHoleControls | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-mobile-375.png) | not-reached |
|
||||
| dnsHoleControls | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-boundary-767.png) | not-reached |
|
||||
| dnsHoleControls | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-boundary-768.png) | not-reached |
|
||||
| dnsHoleControls | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-tablet-1024.png) | not-reached |
|
||||
| dnsHoleControls | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-boundary-1279.png) | not-reached |
|
||||
| dnsHoleControls | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-desktop-1280.png) | not-reached |
|
||||
| dnsHoleControls | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-desktop-1440.png) | not-reached |
|
||||
| dnsHoleControls | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-04/dnsHoleControls-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-05",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-005-RENDER-SIZE": "not-reached",
|
||||
"WG-005-STATES-RECOVERY": "not-reached",
|
||||
"WG-005-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-005-ACCESS-READONLY": "not-reached",
|
||||
"WG-005-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-entityState",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "smartHome-executeAutomation",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "healthMonitoring",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemResources",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "systemDisks",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-05: Smart home, health, and system telemetry
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-05 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | smartHome-entityState, smartHome-executeAutomation, healthMonitoring, systemResources, systemDisks |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-005-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/WG-005-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-005-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/WG-005-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-005-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/WG-005-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-005-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/WG-005-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-005-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/WG-005-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| smartHome-entityState | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-mobile-320.png) | not-reached |
|
||||
| smartHome-entityState | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-mobile-375.png) | not-reached |
|
||||
| smartHome-entityState | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-boundary-767.png) | not-reached |
|
||||
| smartHome-entityState | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-boundary-768.png) | not-reached |
|
||||
| smartHome-entityState | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-tablet-1024.png) | not-reached |
|
||||
| smartHome-entityState | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-boundary-1279.png) | not-reached |
|
||||
| smartHome-entityState | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-desktop-1280.png) | not-reached |
|
||||
| smartHome-entityState | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-desktop-1440.png) | not-reached |
|
||||
| smartHome-entityState | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-entityState-desktop-1920.png) | not-reached |
|
||||
| smartHome-executeAutomation | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-mobile-320.png) | not-reached |
|
||||
| smartHome-executeAutomation | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-mobile-375.png) | not-reached |
|
||||
| smartHome-executeAutomation | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-boundary-767.png) | not-reached |
|
||||
| smartHome-executeAutomation | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-boundary-768.png) | not-reached |
|
||||
| smartHome-executeAutomation | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-tablet-1024.png) | not-reached |
|
||||
| smartHome-executeAutomation | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-boundary-1279.png) | not-reached |
|
||||
| smartHome-executeAutomation | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-desktop-1280.png) | not-reached |
|
||||
| smartHome-executeAutomation | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-desktop-1440.png) | not-reached |
|
||||
| smartHome-executeAutomation | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/smartHome-executeAutomation-desktop-1920.png) | not-reached |
|
||||
| healthMonitoring | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-mobile-320.png) | not-reached |
|
||||
| healthMonitoring | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-mobile-375.png) | not-reached |
|
||||
| healthMonitoring | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-boundary-767.png) | not-reached |
|
||||
| healthMonitoring | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-boundary-768.png) | not-reached |
|
||||
| healthMonitoring | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-tablet-1024.png) | not-reached |
|
||||
| healthMonitoring | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-boundary-1279.png) | not-reached |
|
||||
| healthMonitoring | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-desktop-1280.png) | not-reached |
|
||||
| healthMonitoring | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-desktop-1440.png) | not-reached |
|
||||
| healthMonitoring | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/healthMonitoring-desktop-1920.png) | not-reached |
|
||||
| systemResources | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-mobile-320.png) | not-reached |
|
||||
| systemResources | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-mobile-375.png) | not-reached |
|
||||
| systemResources | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-boundary-767.png) | not-reached |
|
||||
| systemResources | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-boundary-768.png) | not-reached |
|
||||
| systemResources | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-tablet-1024.png) | not-reached |
|
||||
| systemResources | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-boundary-1279.png) | not-reached |
|
||||
| systemResources | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-desktop-1280.png) | not-reached |
|
||||
| systemResources | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-desktop-1440.png) | not-reached |
|
||||
| systemResources | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemResources-desktop-1920.png) | not-reached |
|
||||
| systemDisks | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-mobile-320.png) | not-reached |
|
||||
| systemDisks | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-mobile-375.png) | not-reached |
|
||||
| systemDisks | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-boundary-767.png) | not-reached |
|
||||
| systemDisks | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-boundary-768.png) | not-reached |
|
||||
| systemDisks | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-tablet-1024.png) | not-reached |
|
||||
| systemDisks | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-boundary-1279.png) | not-reached |
|
||||
| systemDisks | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-desktop-1280.png) | not-reached |
|
||||
| systemDisks | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-desktop-1440.png) | not-reached |
|
||||
| systemDisks | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-05/systemDisks-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-06",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-006-RENDER-SIZE": "not-reached",
|
||||
"WG-006-STATES-RECOVERY": "not-reached",
|
||||
"WG-006-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-006-ACCESS-READONLY": "not-reached",
|
||||
"WG-006-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "firewall",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "notifications",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerSummary",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "networkControllerStatus",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "uptimeKuma",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-06: Network availability and operations
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-06 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | firewall, notifications, networkControllerSummary, networkControllerStatus, uptimeKuma |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-006-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/WG-006-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-006-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/WG-006-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-006-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/WG-006-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-006-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/WG-006-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-006-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/WG-006-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| firewall | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-mobile-320.png) | not-reached |
|
||||
| firewall | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-mobile-375.png) | not-reached |
|
||||
| firewall | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-boundary-767.png) | not-reached |
|
||||
| firewall | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-boundary-768.png) | not-reached |
|
||||
| firewall | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-tablet-1024.png) | not-reached |
|
||||
| firewall | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-boundary-1279.png) | not-reached |
|
||||
| firewall | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-desktop-1280.png) | not-reached |
|
||||
| firewall | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-desktop-1440.png) | not-reached |
|
||||
| firewall | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/firewall-desktop-1920.png) | not-reached |
|
||||
| notifications | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-mobile-320.png) | not-reached |
|
||||
| notifications | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-mobile-375.png) | not-reached |
|
||||
| notifications | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-boundary-767.png) | not-reached |
|
||||
| notifications | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-boundary-768.png) | not-reached |
|
||||
| notifications | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-tablet-1024.png) | not-reached |
|
||||
| notifications | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-boundary-1279.png) | not-reached |
|
||||
| notifications | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-desktop-1280.png) | not-reached |
|
||||
| notifications | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-desktop-1440.png) | not-reached |
|
||||
| notifications | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/notifications-desktop-1920.png) | not-reached |
|
||||
| networkControllerSummary | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-mobile-320.png) | not-reached |
|
||||
| networkControllerSummary | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-mobile-375.png) | not-reached |
|
||||
| networkControllerSummary | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-boundary-767.png) | not-reached |
|
||||
| networkControllerSummary | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-boundary-768.png) | not-reached |
|
||||
| networkControllerSummary | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-tablet-1024.png) | not-reached |
|
||||
| networkControllerSummary | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-boundary-1279.png) | not-reached |
|
||||
| networkControllerSummary | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-desktop-1280.png) | not-reached |
|
||||
| networkControllerSummary | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-desktop-1440.png) | not-reached |
|
||||
| networkControllerSummary | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerSummary-desktop-1920.png) | not-reached |
|
||||
| networkControllerStatus | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-mobile-320.png) | not-reached |
|
||||
| networkControllerStatus | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-mobile-375.png) | not-reached |
|
||||
| networkControllerStatus | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-boundary-767.png) | not-reached |
|
||||
| networkControllerStatus | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-boundary-768.png) | not-reached |
|
||||
| networkControllerStatus | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-tablet-1024.png) | not-reached |
|
||||
| networkControllerStatus | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-boundary-1279.png) | not-reached |
|
||||
| networkControllerStatus | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-desktop-1280.png) | not-reached |
|
||||
| networkControllerStatus | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-desktop-1440.png) | not-reached |
|
||||
| networkControllerStatus | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/networkControllerStatus-desktop-1920.png) | not-reached |
|
||||
| uptimeKuma | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-mobile-320.png) | not-reached |
|
||||
| uptimeKuma | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-mobile-375.png) | not-reached |
|
||||
| uptimeKuma | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-boundary-767.png) | not-reached |
|
||||
| uptimeKuma | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-boundary-768.png) | not-reached |
|
||||
| uptimeKuma | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-tablet-1024.png) | not-reached |
|
||||
| uptimeKuma | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-boundary-1279.png) | not-reached |
|
||||
| uptimeKuma | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-desktop-1280.png) | not-reached |
|
||||
| uptimeKuma | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-desktop-1440.png) | not-reached |
|
||||
| uptimeKuma | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-06/uptimeKuma-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-07",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-007-RENDER-SIZE": "not-reached",
|
||||
"WG-007-STATES-RECOVERY": "not-reached",
|
||||
"WG-007-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-007-ACCESS-READONLY": "not-reached",
|
||||
"WG-007-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemTable",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemGrid",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelAlerts",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "beszelSystemStats",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "wud",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-07: Beszel and update monitoring
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-07 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | beszelSystemTable, beszelSystemGrid, beszelAlerts, beszelSystemStats, wud |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-007-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/WG-007-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-007-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/WG-007-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-007-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/WG-007-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-007-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/WG-007-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-007-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/WG-007-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| beszelSystemTable | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-mobile-320.png) | not-reached |
|
||||
| beszelSystemTable | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-mobile-375.png) | not-reached |
|
||||
| beszelSystemTable | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-boundary-767.png) | not-reached |
|
||||
| beszelSystemTable | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-boundary-768.png) | not-reached |
|
||||
| beszelSystemTable | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-tablet-1024.png) | not-reached |
|
||||
| beszelSystemTable | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-boundary-1279.png) | not-reached |
|
||||
| beszelSystemTable | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-desktop-1280.png) | not-reached |
|
||||
| beszelSystemTable | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-desktop-1440.png) | not-reached |
|
||||
| beszelSystemTable | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemTable-desktop-1920.png) | not-reached |
|
||||
| beszelSystemGrid | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-mobile-320.png) | not-reached |
|
||||
| beszelSystemGrid | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-mobile-375.png) | not-reached |
|
||||
| beszelSystemGrid | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-boundary-767.png) | not-reached |
|
||||
| beszelSystemGrid | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-boundary-768.png) | not-reached |
|
||||
| beszelSystemGrid | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-tablet-1024.png) | not-reached |
|
||||
| beszelSystemGrid | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-boundary-1279.png) | not-reached |
|
||||
| beszelSystemGrid | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-desktop-1280.png) | not-reached |
|
||||
| beszelSystemGrid | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-desktop-1440.png) | not-reached |
|
||||
| beszelSystemGrid | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemGrid-desktop-1920.png) | not-reached |
|
||||
| beszelAlerts | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-mobile-320.png) | not-reached |
|
||||
| beszelAlerts | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-mobile-375.png) | not-reached |
|
||||
| beszelAlerts | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-boundary-767.png) | not-reached |
|
||||
| beszelAlerts | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-boundary-768.png) | not-reached |
|
||||
| beszelAlerts | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-tablet-1024.png) | not-reached |
|
||||
| beszelAlerts | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-boundary-1279.png) | not-reached |
|
||||
| beszelAlerts | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-desktop-1280.png) | not-reached |
|
||||
| beszelAlerts | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-desktop-1440.png) | not-reached |
|
||||
| beszelAlerts | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelAlerts-desktop-1920.png) | not-reached |
|
||||
| beszelSystemStats | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-mobile-320.png) | not-reached |
|
||||
| beszelSystemStats | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-mobile-375.png) | not-reached |
|
||||
| beszelSystemStats | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-boundary-767.png) | not-reached |
|
||||
| beszelSystemStats | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-boundary-768.png) | not-reached |
|
||||
| beszelSystemStats | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-tablet-1024.png) | not-reached |
|
||||
| beszelSystemStats | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-boundary-1279.png) | not-reached |
|
||||
| beszelSystemStats | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-desktop-1280.png) | not-reached |
|
||||
| beszelSystemStats | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-desktop-1440.png) | not-reached |
|
||||
| beszelSystemStats | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/beszelSystemStats-desktop-1920.png) | not-reached |
|
||||
| wud | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-mobile-320.png) | not-reached |
|
||||
| wud | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-mobile-375.png) | not-reached |
|
||||
| wud | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-boundary-767.png) | not-reached |
|
||||
| wud | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-boundary-768.png) | not-reached |
|
||||
| wud | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-tablet-1024.png) | not-reached |
|
||||
| wud | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-boundary-1279.png) | not-reached |
|
||||
| wud | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-desktop-1280.png) | not-reached |
|
||||
| wud | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-desktop-1440.png) | not-reached |
|
||||
| wud | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-07/wud-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-08",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-008-RENDER-SIZE": "not-reached",
|
||||
"WG-008-STATES-RECOVERY": "not-reached",
|
||||
"WG-008-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-008-ACCESS-READONLY": "not-reached",
|
||||
"WG-008-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "ups",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "vpn",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "speedtestTracker",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "traefik",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "umami",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-08: Power, VPN, speed, routing, and analytics
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-08 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | ups, vpn, speedtestTracker, traefik, umami |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-008-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/WG-008-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-008-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/WG-008-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-008-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/WG-008-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-008-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/WG-008-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-008-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/WG-008-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ups | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-mobile-320.png) | not-reached |
|
||||
| ups | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-mobile-375.png) | not-reached |
|
||||
| ups | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-boundary-767.png) | not-reached |
|
||||
| ups | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-boundary-768.png) | not-reached |
|
||||
| ups | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-tablet-1024.png) | not-reached |
|
||||
| ups | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-boundary-1279.png) | not-reached |
|
||||
| ups | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-desktop-1280.png) | not-reached |
|
||||
| ups | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-desktop-1440.png) | not-reached |
|
||||
| ups | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/ups-desktop-1920.png) | not-reached |
|
||||
| vpn | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-mobile-320.png) | not-reached |
|
||||
| vpn | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-mobile-375.png) | not-reached |
|
||||
| vpn | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-boundary-767.png) | not-reached |
|
||||
| vpn | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-boundary-768.png) | not-reached |
|
||||
| vpn | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-tablet-1024.png) | not-reached |
|
||||
| vpn | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-boundary-1279.png) | not-reached |
|
||||
| vpn | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-desktop-1280.png) | not-reached |
|
||||
| vpn | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-desktop-1440.png) | not-reached |
|
||||
| vpn | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/vpn-desktop-1920.png) | not-reached |
|
||||
| speedtestTracker | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-mobile-320.png) | not-reached |
|
||||
| speedtestTracker | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-mobile-375.png) | not-reached |
|
||||
| speedtestTracker | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-boundary-767.png) | not-reached |
|
||||
| speedtestTracker | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-boundary-768.png) | not-reached |
|
||||
| speedtestTracker | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-tablet-1024.png) | not-reached |
|
||||
| speedtestTracker | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-boundary-1279.png) | not-reached |
|
||||
| speedtestTracker | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-desktop-1280.png) | not-reached |
|
||||
| speedtestTracker | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-desktop-1440.png) | not-reached |
|
||||
| speedtestTracker | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/speedtestTracker-desktop-1920.png) | not-reached |
|
||||
| traefik | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-mobile-320.png) | not-reached |
|
||||
| traefik | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-mobile-375.png) | not-reached |
|
||||
| traefik | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-boundary-767.png) | not-reached |
|
||||
| traefik | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-boundary-768.png) | not-reached |
|
||||
| traefik | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-tablet-1024.png) | not-reached |
|
||||
| traefik | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-boundary-1279.png) | not-reached |
|
||||
| traefik | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-desktop-1280.png) | not-reached |
|
||||
| traefik | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-desktop-1440.png) | not-reached |
|
||||
| traefik | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/traefik-desktop-1920.png) | not-reached |
|
||||
| umami | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-mobile-320.png) | not-reached |
|
||||
| umami | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-mobile-375.png) | not-reached |
|
||||
| umami | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-boundary-767.png) | not-reached |
|
||||
| umami | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-boundary-768.png) | not-reached |
|
||||
| umami | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-tablet-1024.png) | not-reached |
|
||||
| umami | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-boundary-1279.png) | not-reached |
|
||||
| umami | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-desktop-1280.png) | not-reached |
|
||||
| umami | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-desktop-1440.png) | not-reached |
|
||||
| umami | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-08/umami-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-09",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-009-RENDER-SIZE": "not-reached",
|
||||
"WG-009-STATES-RECOVERY": "not-reached",
|
||||
"WG-009-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-009-ACCESS-READONLY": "not-reached",
|
||||
"WG-009-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "calendar",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaServer",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestList",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaRequests-requestStats",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaMissing",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-09: Media overview and requests
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Maya Media |
|
||||
| Boards | qa-widgets-09 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | calendar, mediaServer, mediaRequests-requestList, mediaRequests-requestStats, mediaMissing |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-009-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/WG-009-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-009-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/WG-009-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-009-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/WG-009-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-009-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/WG-009-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-009-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/WG-009-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| calendar | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-mobile-320.png) | not-reached |
|
||||
| calendar | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-mobile-375.png) | not-reached |
|
||||
| calendar | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-boundary-767.png) | not-reached |
|
||||
| calendar | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-boundary-768.png) | not-reached |
|
||||
| calendar | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-tablet-1024.png) | not-reached |
|
||||
| calendar | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-boundary-1279.png) | not-reached |
|
||||
| calendar | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-desktop-1280.png) | not-reached |
|
||||
| calendar | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-desktop-1440.png) | not-reached |
|
||||
| calendar | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/calendar-desktop-1920.png) | not-reached |
|
||||
| mediaServer | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-mobile-320.png) | not-reached |
|
||||
| mediaServer | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-mobile-375.png) | not-reached |
|
||||
| mediaServer | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-boundary-767.png) | not-reached |
|
||||
| mediaServer | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-boundary-768.png) | not-reached |
|
||||
| mediaServer | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-tablet-1024.png) | not-reached |
|
||||
| mediaServer | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-boundary-1279.png) | not-reached |
|
||||
| mediaServer | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-desktop-1280.png) | not-reached |
|
||||
| mediaServer | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-desktop-1440.png) | not-reached |
|
||||
| mediaServer | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaServer-desktop-1920.png) | not-reached |
|
||||
| mediaRequests-requestList | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-mobile-320.png) | not-reached |
|
||||
| mediaRequests-requestList | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-mobile-375.png) | not-reached |
|
||||
| mediaRequests-requestList | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-boundary-767.png) | not-reached |
|
||||
| mediaRequests-requestList | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-boundary-768.png) | not-reached |
|
||||
| mediaRequests-requestList | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-tablet-1024.png) | not-reached |
|
||||
| mediaRequests-requestList | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-boundary-1279.png) | not-reached |
|
||||
| mediaRequests-requestList | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-desktop-1280.png) | not-reached |
|
||||
| mediaRequests-requestList | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-desktop-1440.png) | not-reached |
|
||||
| mediaRequests-requestList | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestList-desktop-1920.png) | not-reached |
|
||||
| mediaRequests-requestStats | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-mobile-320.png) | not-reached |
|
||||
| mediaRequests-requestStats | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-mobile-375.png) | not-reached |
|
||||
| mediaRequests-requestStats | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-boundary-767.png) | not-reached |
|
||||
| mediaRequests-requestStats | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-boundary-768.png) | not-reached |
|
||||
| mediaRequests-requestStats | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-tablet-1024.png) | not-reached |
|
||||
| mediaRequests-requestStats | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-boundary-1279.png) | not-reached |
|
||||
| mediaRequests-requestStats | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-desktop-1280.png) | not-reached |
|
||||
| mediaRequests-requestStats | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-desktop-1440.png) | not-reached |
|
||||
| mediaRequests-requestStats | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaRequests-requestStats-desktop-1920.png) | not-reached |
|
||||
| mediaMissing | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-mobile-320.png) | not-reached |
|
||||
| mediaMissing | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-mobile-375.png) | not-reached |
|
||||
| mediaMissing | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-boundary-767.png) | not-reached |
|
||||
| mediaMissing | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-boundary-768.png) | not-reached |
|
||||
| mediaMissing | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-tablet-1024.png) | not-reached |
|
||||
| mediaMissing | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-boundary-1279.png) | not-reached |
|
||||
| mediaMissing | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-desktop-1280.png) | not-reached |
|
||||
| mediaMissing | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-desktop-1440.png) | not-reached |
|
||||
| mediaMissing | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-09/mediaMissing-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-10",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-010-RENDER-SIZE": "not-reached",
|
||||
"WG-010-STATES-RECOVERY": "not-reached",
|
||||
"WG-010-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-010-ACCESS-READONLY": "not-reached",
|
||||
"WG-010-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaReleases",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "mediaTranscoding",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-serverStats",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "immich-albumCarousel",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "audioStats",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-10: Media activity, Immich, and audio
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Maya Media |
|
||||
| Boards | qa-widgets-10 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 125% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | mediaReleases, mediaTranscoding, immich-serverStats, immich-albumCarousel, audioStats |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-010-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/WG-010-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-010-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/WG-010-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-010-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/WG-010-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-010-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/WG-010-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-010-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/WG-010-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| mediaReleases | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-mobile-320.png) | not-reached |
|
||||
| mediaReleases | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-mobile-375.png) | not-reached |
|
||||
| mediaReleases | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-boundary-767.png) | not-reached |
|
||||
| mediaReleases | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-boundary-768.png) | not-reached |
|
||||
| mediaReleases | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-tablet-1024.png) | not-reached |
|
||||
| mediaReleases | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-boundary-1279.png) | not-reached |
|
||||
| mediaReleases | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-desktop-1280.png) | not-reached |
|
||||
| mediaReleases | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-desktop-1440.png) | not-reached |
|
||||
| mediaReleases | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaReleases-desktop-1920.png) | not-reached |
|
||||
| mediaTranscoding | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-mobile-320.png) | not-reached |
|
||||
| mediaTranscoding | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-mobile-375.png) | not-reached |
|
||||
| mediaTranscoding | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-boundary-767.png) | not-reached |
|
||||
| mediaTranscoding | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-boundary-768.png) | not-reached |
|
||||
| mediaTranscoding | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-tablet-1024.png) | not-reached |
|
||||
| mediaTranscoding | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-boundary-1279.png) | not-reached |
|
||||
| mediaTranscoding | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-desktop-1280.png) | not-reached |
|
||||
| mediaTranscoding | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-desktop-1440.png) | not-reached |
|
||||
| mediaTranscoding | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/mediaTranscoding-desktop-1920.png) | not-reached |
|
||||
| immich-serverStats | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-mobile-320.png) | not-reached |
|
||||
| immich-serverStats | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-mobile-375.png) | not-reached |
|
||||
| immich-serverStats | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-boundary-767.png) | not-reached |
|
||||
| immich-serverStats | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-boundary-768.png) | not-reached |
|
||||
| immich-serverStats | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-tablet-1024.png) | not-reached |
|
||||
| immich-serverStats | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-boundary-1279.png) | not-reached |
|
||||
| immich-serverStats | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-desktop-1280.png) | not-reached |
|
||||
| immich-serverStats | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-desktop-1440.png) | not-reached |
|
||||
| immich-serverStats | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-serverStats-desktop-1920.png) | not-reached |
|
||||
| immich-albumCarousel | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-mobile-320.png) | not-reached |
|
||||
| immich-albumCarousel | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-mobile-375.png) | not-reached |
|
||||
| immich-albumCarousel | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-boundary-767.png) | not-reached |
|
||||
| immich-albumCarousel | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-boundary-768.png) | not-reached |
|
||||
| immich-albumCarousel | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-tablet-1024.png) | not-reached |
|
||||
| immich-albumCarousel | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-boundary-1279.png) | not-reached |
|
||||
| immich-albumCarousel | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-desktop-1280.png) | not-reached |
|
||||
| immich-albumCarousel | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-desktop-1440.png) | not-reached |
|
||||
| immich-albumCarousel | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/immich-albumCarousel-desktop-1920.png) | not-reached |
|
||||
| audioStats | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-mobile-320.png) | not-reached |
|
||||
| audioStats | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-mobile-375.png) | not-reached |
|
||||
| audioStats | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-boundary-767.png) | not-reached |
|
||||
| audioStats | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-boundary-768.png) | not-reached |
|
||||
| audioStats | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-tablet-1024.png) | not-reached |
|
||||
| audioStats | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-boundary-1279.png) | not-reached |
|
||||
| audioStats | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-desktop-1280.png) | not-reached |
|
||||
| audioStats | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-desktop-1440.png) | not-reached |
|
||||
| audioStats | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-10/audioStats-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,409 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-11",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-011-RENDER-SIZE": "not-reached",
|
||||
"WG-011-STATES-RECOVERY": "not-reached",
|
||||
"WG-011-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-011-ACCESS-READONLY": "not-reached",
|
||||
"WG-011-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "paperlessNgx",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "patchmon",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "bazarr",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "tracearr",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "releases",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-11: Documents, patching, media services, and releases
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6555 |
|
||||
| Personas | Ingrid Infra |
|
||||
| Boards | qa-widgets-11 |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 100%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | paperlessNgx, patchmon, bazarr, tracearr, releases |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-011-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/WG-011-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-011-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/WG-011-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-011-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/WG-011-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-011-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/WG-011-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-011-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/WG-011-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| paperlessNgx | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-mobile-320.png) | not-reached |
|
||||
| paperlessNgx | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-mobile-375.png) | not-reached |
|
||||
| paperlessNgx | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-boundary-767.png) | not-reached |
|
||||
| paperlessNgx | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-boundary-768.png) | not-reached |
|
||||
| paperlessNgx | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-tablet-1024.png) | not-reached |
|
||||
| paperlessNgx | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-boundary-1279.png) | not-reached |
|
||||
| paperlessNgx | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-desktop-1280.png) | not-reached |
|
||||
| paperlessNgx | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-desktop-1440.png) | not-reached |
|
||||
| paperlessNgx | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/paperlessNgx-desktop-1920.png) | not-reached |
|
||||
| patchmon | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-mobile-320.png) | not-reached |
|
||||
| patchmon | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-mobile-375.png) | not-reached |
|
||||
| patchmon | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-boundary-767.png) | not-reached |
|
||||
| patchmon | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-boundary-768.png) | not-reached |
|
||||
| patchmon | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-tablet-1024.png) | not-reached |
|
||||
| patchmon | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-boundary-1279.png) | not-reached |
|
||||
| patchmon | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-desktop-1280.png) | not-reached |
|
||||
| patchmon | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-desktop-1440.png) | not-reached |
|
||||
| patchmon | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/patchmon-desktop-1920.png) | not-reached |
|
||||
| bazarr | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-mobile-320.png) | not-reached |
|
||||
| bazarr | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-mobile-375.png) | not-reached |
|
||||
| bazarr | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-boundary-767.png) | not-reached |
|
||||
| bazarr | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-boundary-768.png) | not-reached |
|
||||
| bazarr | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-tablet-1024.png) | not-reached |
|
||||
| bazarr | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-boundary-1279.png) | not-reached |
|
||||
| bazarr | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-desktop-1280.png) | not-reached |
|
||||
| bazarr | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-desktop-1440.png) | not-reached |
|
||||
| bazarr | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/bazarr-desktop-1920.png) | not-reached |
|
||||
| tracearr | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-mobile-320.png) | not-reached |
|
||||
| tracearr | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-mobile-375.png) | not-reached |
|
||||
| tracearr | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-boundary-767.png) | not-reached |
|
||||
| tracearr | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-boundary-768.png) | not-reached |
|
||||
| tracearr | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-tablet-1024.png) | not-reached |
|
||||
| tracearr | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-boundary-1279.png) | not-reached |
|
||||
| tracearr | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-desktop-1280.png) | not-reached |
|
||||
| tracearr | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-desktop-1440.png) | not-reached |
|
||||
| tracearr | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/tracearr-desktop-1920.png) | not-reached |
|
||||
| releases | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-mobile-320.png) | not-reached |
|
||||
| releases | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-mobile-375.png) | not-reached |
|
||||
| releases | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-boundary-767.png) | not-reached |
|
||||
| releases | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-boundary-768.png) | not-reached |
|
||||
| releases | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-tablet-1024.png) | not-reached |
|
||||
| releases | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-boundary-1279.png) | not-reached |
|
||||
| releases | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-desktop-1280.png) | not-reached |
|
||||
| releases | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-desktop-1440.png) | not-reached |
|
||||
| releases | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-11/releases-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,346 @@
|
||||
<!-- release-v2-qa-report
|
||||
{
|
||||
"packetId": "widgets-12",
|
||||
"status": "not-reached",
|
||||
"caseStatuses": {
|
||||
"WG-012-RENDER-SIZE": "not-reached",
|
||||
"WG-012-STATES-RECOVERY": "not-reached",
|
||||
"WG-012-OPTIONS-PERSISTENCE": "not-reached",
|
||||
"WG-012-ACCESS-READONLY": "not-reached",
|
||||
"WG-012-EVIDENCE": "not-reached"
|
||||
},
|
||||
"execution": {
|
||||
"candidateSha": "2d5edf46513b96c8b9f973d4a121b396d688e9bb",
|
||||
"url": null,
|
||||
"actualPort": null,
|
||||
"runtimeProfile": null,
|
||||
"runtimeFlags": [],
|
||||
"persona": null,
|
||||
"sessionId": null,
|
||||
"timestamp": null,
|
||||
"viewport": null,
|
||||
"input": null,
|
||||
"zoom": null
|
||||
},
|
||||
"findings": [],
|
||||
"artifacts": [],
|
||||
"widgetChecks": [
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "coolify",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "archiveTeamWarrior",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "customApi",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "mobile-320",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "mobile-375",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "boundary-767",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "boundary-768",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "tablet-1024",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "boundary-1279",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "desktop-1280",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "desktop-1440",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
},
|
||||
{
|
||||
"widgetKind": "assistant",
|
||||
"viewport": "desktop-1920",
|
||||
"sizeRequirement": "every width 1-24 × every height 1-6",
|
||||
"status": "not-reached"
|
||||
}
|
||||
],
|
||||
"performance": {
|
||||
"measurements": [],
|
||||
"limitations": []
|
||||
},
|
||||
"independentReproductions": [],
|
||||
"notes": "Not executed."
|
||||
}
|
||||
-->
|
||||
|
||||
# widgets-12: Coolify, ArchiveTeam, Custom API, and Assistant
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | not-reached |
|
||||
| Wave | widgets |
|
||||
| PR refs | #6502, #6482, #6555 |
|
||||
| Personas | Ash Assistant, Cora Creator |
|
||||
| Boards | qa-widgets-12, qa-custom-widget-assistant |
|
||||
| Profiles | main-writable, degraded |
|
||||
| Chromium viewports | mobile-320, mobile-375, boundary-767, boundary-768, tablet-1024, boundary-1279, desktop-1280, desktop-1440, desktop-1920 |
|
||||
| Zoom | 80%, 100%, 125%, 200% |
|
||||
| Input | mouse, touch, keyboard |
|
||||
| Widget kinds | coolify, archiveTeamWarrior, customApi, assistant |
|
||||
| Artifact directory | [/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12) |
|
||||
| Candidate SHA | 2d5edf46513b96c8b9f973d4a121b396d688e9bb |
|
||||
| URL / actual port | not-recorded |
|
||||
| Runtime profile / flags | not-recorded |
|
||||
| Executed persona | not-recorded |
|
||||
| Browser session ID | not-recorded |
|
||||
| Timestamp | not-recorded |
|
||||
| Executed viewport / input / zoom | not-recorded |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| WG-012-RENDER-SIZE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/WG-012-RENDER-SIZE.png) | Not executed | Render every assigned widget kind at every assigned viewport and exercise default, minimum, maximum, and compact sizes where supported. |
|
||||
| WG-012-STATES-RECOVERY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/WG-012-STATES-RECOVERY.png) | Not executed | Verify loading, populated, empty, error, stale, retry, and recovered states for every assigned widget kind. |
|
||||
| WG-012-OPTIONS-PERSISTENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/WG-012-OPTIONS-PERSISTENCE.png) | Not executed | Mutate meaningful options, save, reload, reset, and confirm each assigned widget kind persists correctly. |
|
||||
| WG-012-ACCESS-READONLY | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/WG-012-ACCESS-READONLY.png) | Not executed | Verify Owner, Editor, Viewer, missing-integration, permission-denied, and global read-only behavior for every assigned widget kind. |
|
||||
| WG-012-EVIDENCE | not-reached | [planned screenshot](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/WG-012-EVIDENCE.png) | Not executed | Capture screenshot, console, network, overflow, clipping, focus, and cleanup evidence for every kind and viewport row. |
|
||||
|
||||
## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| coolify | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-mobile-320.png) | not-reached |
|
||||
| coolify | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-mobile-375.png) | not-reached |
|
||||
| coolify | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-boundary-767.png) | not-reached |
|
||||
| coolify | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-boundary-768.png) | not-reached |
|
||||
| coolify | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-tablet-1024.png) | not-reached |
|
||||
| coolify | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-boundary-1279.png) | not-reached |
|
||||
| coolify | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-desktop-1280.png) | not-reached |
|
||||
| coolify | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-desktop-1440.png) | not-reached |
|
||||
| coolify | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/coolify-desktop-1920.png) | not-reached |
|
||||
| archiveTeamWarrior | mobile-320 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-mobile-320.png) | not-reached |
|
||||
| archiveTeamWarrior | mobile-375 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-mobile-375.png) | not-reached |
|
||||
| archiveTeamWarrior | boundary-767 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-boundary-767.png) | not-reached |
|
||||
| archiveTeamWarrior | boundary-768 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-boundary-768.png) | not-reached |
|
||||
| archiveTeamWarrior | tablet-1024 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-tablet-1024.png) | not-reached |
|
||||
| archiveTeamWarrior | boundary-1279 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-boundary-1279.png) | not-reached |
|
||||
| archiveTeamWarrior | desktop-1280 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-desktop-1280.png) | not-reached |
|
||||
| archiveTeamWarrior | desktop-1440 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-desktop-1440.png) | not-reached |
|
||||
| archiveTeamWarrior | desktop-1920 | minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/archiveTeamWarrior-desktop-1920.png) | not-reached |
|
||||
| customApi | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-mobile-320.png) | not-reached |
|
||||
| customApi | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-mobile-375.png) | not-reached |
|
||||
| customApi | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-boundary-767.png) | not-reached |
|
||||
| customApi | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-boundary-768.png) | not-reached |
|
||||
| customApi | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-tablet-1024.png) | not-reached |
|
||||
| customApi | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-boundary-1279.png) | not-reached |
|
||||
| customApi | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-desktop-1280.png) | not-reached |
|
||||
| customApi | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-desktop-1440.png) | not-reached |
|
||||
| customApi | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/customApi-desktop-1920.png) | not-reached |
|
||||
| assistant | mobile-320 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-mobile-320.png) | not-reached |
|
||||
| assistant | mobile-375 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-mobile-375.png) | not-reached |
|
||||
| assistant | boundary-767 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-boundary-767.png) | not-reached |
|
||||
| assistant | boundary-768 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-boundary-768.png) | not-reached |
|
||||
| assistant | tablet-1024 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-tablet-1024.png) | not-reached |
|
||||
| assistant | boundary-1279 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-boundary-1279.png) | not-reached |
|
||||
| assistant | desktop-1280 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-desktop-1280.png) | not-reached |
|
||||
| assistant | desktop-1440 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-desktop-1440.png) | not-reached |
|
||||
| assistant | desktop-1920 | every width 1-24 × every height 1-6 | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](/home/habs/.codex/worktrees/8ba8/homarr/.screenshots/release-v2/widgets-12/assistant-desktop-1920.png) | not-reached |
|
||||
|
||||
## Performance measurements and limitations
|
||||
|
||||
Record structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
Record a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.
|
||||
|
||||
## Findings
|
||||
|
||||
No findings recorded. Add structured findings to the metadata block and mirror them here.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Not executed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Record created resources, cleanup outcome, console errors, and network failures. Never include secret values.
|
||||
@@ -0,0 +1,349 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { readFile, mkdir, open, rename, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { validateLoopbackHost } from "./safety.mts";
|
||||
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
||||
const MAX_SLOW_MS = 30_000;
|
||||
|
||||
const png = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64",
|
||||
);
|
||||
|
||||
const videoPath = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../apps/docs/src/components/pages/home/drag-and-drop/showcase-dark.mp4",
|
||||
);
|
||||
|
||||
const createWav = () => {
|
||||
const sampleRate = 8_000;
|
||||
const sampleCount = sampleRate / 4;
|
||||
const dataLength = sampleCount * 2;
|
||||
const buffer = Buffer.alloc(44 + dataLength);
|
||||
buffer.write("RIFF", 0);
|
||||
buffer.writeUInt32LE(36 + dataLength, 4);
|
||||
buffer.write("WAVEfmt ", 8);
|
||||
buffer.writeUInt32LE(16, 16);
|
||||
buffer.writeUInt16LE(1, 20);
|
||||
buffer.writeUInt16LE(1, 22);
|
||||
buffer.writeUInt32LE(sampleRate, 24);
|
||||
buffer.writeUInt32LE(sampleRate * 2, 28);
|
||||
buffer.writeUInt16LE(2, 32);
|
||||
buffer.writeUInt16LE(16, 34);
|
||||
buffer.write("data", 36);
|
||||
buffer.writeUInt32LE(dataLength, 40);
|
||||
for (let index = 0; index < sampleCount; index += 1) {
|
||||
const value = Math.sin((2 * Math.PI * 440 * index) / sampleRate) * 0.2;
|
||||
buffer.writeInt16LE(Math.round(value * 32_767), 44 + index * 2);
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
|
||||
const wav = createWav();
|
||||
|
||||
const customWidgetPayload = {
|
||||
title: "Release v2 QA fixture",
|
||||
status: "ok",
|
||||
items: [
|
||||
{ id: "alpha", label: "Alpha", value: 42 },
|
||||
{ id: "beta", label: "Beta", value: 7 },
|
||||
],
|
||||
};
|
||||
|
||||
const parseArgs = () => {
|
||||
const args = process.argv.slice(2).filter((argument) => argument !== "--");
|
||||
let host = DEFAULT_HOST;
|
||||
let port = 0;
|
||||
let readyFile: string | undefined;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const argument = args[index];
|
||||
const value = args[index + 1];
|
||||
if (argument === "--host" && value) {
|
||||
host = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--port" && value) {
|
||||
port = Number(value);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--ready-file" && value) {
|
||||
readyFile = path.resolve(value);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown or incomplete argument: ${argument ?? "<missing>"}`);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
||||
throw new Error("--port must be an integer between 0 and 65535");
|
||||
}
|
||||
return { host: validateLoopbackHost(host), port, readyFile };
|
||||
};
|
||||
|
||||
const setCommonHeaders = (response: ServerResponse) => {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-QA-Fixture");
|
||||
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, OPTIONS");
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
response.setHeader("X-QA-Fixture", "release-v2");
|
||||
};
|
||||
|
||||
const send = (response: ServerResponse, status: number, contentType: string, body: string | Buffer) => {
|
||||
setCommonHeaders(response);
|
||||
response.writeHead(status, { "Content-Type": contentType });
|
||||
response.end(body);
|
||||
};
|
||||
|
||||
const sendJson = (response: ServerResponse, status: number, value: unknown) => {
|
||||
send(response, status, "application/json; charset=utf-8", `${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const readBody = async (request: IncomingMessage) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
for await (const chunk of request) {
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
size += buffer.length;
|
||||
if (size > MAX_BODY_BYTES) throw new Error("UPLOAD_TOO_LARGE");
|
||||
chunks.push(buffer);
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
};
|
||||
|
||||
const handleRequest = async (request: IncomingMessage, response: ServerResponse) => {
|
||||
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? DEFAULT_HOST}`);
|
||||
const method = request.method ?? "GET";
|
||||
|
||||
if (method === "OPTIONS") {
|
||||
setCommonHeaders(response);
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
sendJson(response, 200, { status: "ok", fixture: "release-v2-qa" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/json") {
|
||||
sendJson(response, 200, {
|
||||
id: "qa-json-001",
|
||||
name: "Release v2 deterministic JSON",
|
||||
active: true,
|
||||
count: 3,
|
||||
tags: ["homarr", "release-v2", "qa"],
|
||||
nested: { score: 99, nullable: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/qa/custom-widget") {
|
||||
sendJson(response, 200, customWidgetPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/rss") {
|
||||
send(
|
||||
response,
|
||||
200,
|
||||
"application/rss+xml; charset=utf-8",
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"><channel><title>Release v2 QA</title><link>https://example.invalid/qa</link><description>Deterministic feed</description><item><guid>qa-entry-1</guid><title>Alpha release note</title><link>https://example.invalid/qa/alpha</link><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate><description>Stable fixture entry</description></item><item><guid>qa-entry-2</guid><title>Beta release note</title><link>https://example.invalid/qa/beta</link><pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate><description>Second stable fixture entry</description></item></channel></rss>\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/media/image.png") {
|
||||
send(response, 200, "image/png", png);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/media/image.svg") {
|
||||
send(
|
||||
response,
|
||||
200,
|
||||
"image/svg+xml; charset=utf-8",
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180"><rect width="320" height="180" fill="#111827"/><circle cx="72" cy="90" r="40" fill="#38bdf8"/><text x="132" y="98" fill="#f8fafc" font-family="sans-serif" font-size="22">Release v2 QA</text></svg>\n',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/media/audio.wav") {
|
||||
send(response, 200, "audio/wav", wav);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/media/video.mp4") {
|
||||
const video = await readFile(videoPath);
|
||||
const range = request.headers.range;
|
||||
setCommonHeaders(response);
|
||||
response.setHeader("Accept-Ranges", "bytes");
|
||||
if (!range) {
|
||||
response.writeHead(200, { "Content-Type": "video/mp4", "Content-Length": video.length });
|
||||
response.end(method === "HEAD" ? undefined : video);
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^bytes=(\d+)-(\d*)$/.exec(range);
|
||||
if (!match) {
|
||||
response.writeHead(416, { "Content-Range": `bytes */${video.length}` });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
const start = Number(match[1]);
|
||||
const requestedEnd = match[2] ? Number(match[2]) : video.length - 1;
|
||||
const end = Math.min(requestedEnd, video.length - 1);
|
||||
if (start > end || start >= video.length) {
|
||||
response.writeHead(416, { "Content-Range": `bytes */${video.length}` });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
const body = video.subarray(start, end + 1);
|
||||
response.writeHead(206, {
|
||||
"Content-Type": "video/mp4",
|
||||
"Content-Length": body.length,
|
||||
"Content-Range": `bytes ${start}-${end}/${video.length}`,
|
||||
});
|
||||
response.end(method === "HEAD" ? undefined : body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/iframe") {
|
||||
send(
|
||||
response,
|
||||
200,
|
||||
"text/html; charset=utf-8",
|
||||
'<!doctype html><html><head><meta charset="utf-8"><title>Release v2 QA iframe</title></head><body><main data-qa-fixture="iframe"><h1>Deterministic iframe</h1><p>release-v2-qa</p></main></body></html>\n',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/qa/download/sample.txt") {
|
||||
setCommonHeaders(response);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="release-v2-qa-sample.txt"',
|
||||
});
|
||||
response.end("Release v2 QA deterministic download\nline=2\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/upload") {
|
||||
if (method !== "POST" && method !== "PUT") {
|
||||
sendJson(response, 405, { error: "METHOD_NOT_ALLOWED", allowed: ["POST", "PUT"] });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = await readBody(request);
|
||||
sendJson(response, 200, {
|
||||
uploaded: true,
|
||||
method,
|
||||
bytes: body.length,
|
||||
contentType: request.headers["content-type"] ?? null,
|
||||
sha256: createHash("sha256").update(body).digest("hex"),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "UPLOAD_TOO_LARGE") {
|
||||
sendJson(response, 413, { error: "UPLOAD_TOO_LARGE", maxBytes: MAX_BODY_BYTES });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/empty") {
|
||||
setCommonHeaders(response);
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/malformed" || url.pathname === "/malformed/json") {
|
||||
send(response, 200, "application/json; charset=utf-8", '{"status":"broken","items":[1,2,}\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/slow") {
|
||||
const requestedMs = Number(url.searchParams.get("ms") ?? "1500");
|
||||
const delayMs = Number.isFinite(requestedMs) ? Math.max(0, Math.min(Math.trunc(requestedMs), MAX_SLOW_MS)) : 1500;
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
sendJson(response, 200, { status: "ok", delayedMs: delayMs });
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/error" || url.pathname.startsWith("/error/")) {
|
||||
const pathStatus = url.pathname.split("/")[2];
|
||||
const requestedStatus = Number(url.searchParams.get("status") ?? pathStatus ?? "503");
|
||||
const status =
|
||||
Number.isInteger(requestedStatus) && requestedStatus >= 400 && requestedStatus <= 599 ? requestedStatus : 503;
|
||||
sendJson(response, status, { error: "QA_CONFIGURED_HTTP_ERROR", status });
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(response, 404, { error: "NOT_FOUND", path: url.pathname });
|
||||
};
|
||||
|
||||
const writeJsonAtomic = async (filePath: string, value: unknown) => {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
||||
const handle = await open(
|
||||
temporaryPath,
|
||||
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
try {
|
||||
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
try {
|
||||
await rename(temporaryPath, filePath);
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { force: true });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const options = parseArgs();
|
||||
const server = createServer((request, response) => {
|
||||
void handleRequest(request, response).catch((error: unknown) => {
|
||||
console.error(error);
|
||||
if (!response.headersSent) sendJson(response, 500, { error: "FIXTURE_INTERNAL_ERROR" });
|
||||
else response.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(options.port, options.host, resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Fixture server did not expose a TCP address");
|
||||
const urlHost = options.host.includes(":") ? `[${options.host}]` : options.host;
|
||||
const url = `http://${urlHost}:${address.port}`;
|
||||
const ready = { schemaVersion: 1, pid: process.pid, host: options.host, port: address.port, url };
|
||||
if (options.readyFile) await writeJsonAtomic(options.readyFile, ready);
|
||||
process.stdout.write(`${JSON.stringify(ready)}\n`);
|
||||
|
||||
const shutdown = () => {
|
||||
server.close(() => process.exit(0));
|
||||
setTimeout(() => process.exit(1), 5_000).unref();
|
||||
};
|
||||
process.once("SIGINT", shutdown);
|
||||
process.once("SIGTERM", shutdown);
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { LayoutRole } from "../../packages/definitions/index.ts";
|
||||
import { getBoardLaneColumnCount, getRootSectionLane } from "../../packages/definitions/index.ts";
|
||||
|
||||
interface GeometryLayout {
|
||||
id: string;
|
||||
boardId: string;
|
||||
columnCount: number;
|
||||
leftGutterColumnCount: number;
|
||||
rightGutterColumnCount: number;
|
||||
role: LayoutRole;
|
||||
}
|
||||
|
||||
interface GeometrySection {
|
||||
id: string;
|
||||
boardId: string;
|
||||
kind: "empty" | "container";
|
||||
xOffset: number | null;
|
||||
}
|
||||
|
||||
interface GeometryPlacement {
|
||||
layoutId: string;
|
||||
sectionId: string;
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface GeometrySectionLayout extends GeometryPlacement {
|
||||
parentSectionId: string | null;
|
||||
}
|
||||
|
||||
interface GeometryItemLayout extends GeometryPlacement {
|
||||
itemId: string;
|
||||
boardId: string;
|
||||
}
|
||||
|
||||
export interface FixtureGeometry {
|
||||
layouts: GeometryLayout[];
|
||||
sections: GeometrySection[];
|
||||
sectionLayouts: GeometrySectionLayout[];
|
||||
itemLayouts: GeometryItemLayout[];
|
||||
}
|
||||
|
||||
const placementIsValid = (placement: GeometryPlacement) =>
|
||||
Number.isInteger(placement.xOffset) &&
|
||||
Number.isInteger(placement.yOffset) &&
|
||||
Number.isInteger(placement.width) &&
|
||||
Number.isInteger(placement.height) &&
|
||||
placement.xOffset >= 0 &&
|
||||
placement.yOffset >= 0 &&
|
||||
placement.width >= 1 &&
|
||||
placement.height >= 1;
|
||||
|
||||
const placementsOverlap = (first: GeometryPlacement, second: GeometryPlacement) =>
|
||||
first.xOffset < second.xOffset + second.width &&
|
||||
second.xOffset < first.xOffset + first.width &&
|
||||
first.yOffset < second.yOffset + second.height &&
|
||||
second.yOffset < first.yOffset + first.height;
|
||||
|
||||
const compareItemLayouts = (first: GeometryItemLayout, second: GeometryItemLayout) => {
|
||||
if (first.itemId < second.itemId) return -1;
|
||||
if (first.itemId > second.itemId) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const findFixtureGeometryErrors = (geometry: FixtureGeometry) => {
|
||||
const errors: string[] = [];
|
||||
const layoutsById = new Map(geometry.layouts.map((layout) => [layout.id, layout]));
|
||||
const sectionsById = new Map(geometry.sections.map((section) => [section.id, section]));
|
||||
const sectionLayoutsByKey = new Map<string, GeometrySectionLayout>();
|
||||
const itemsByDomain = new Map<string, GeometryItemLayout[]>();
|
||||
|
||||
const addItemLayout = (itemLayout: GeometryItemLayout) => {
|
||||
const key = `${itemLayout.layoutId}\0${itemLayout.sectionId}`;
|
||||
const itemLayouts = itemsByDomain.get(key) ?? [];
|
||||
itemLayouts.push(itemLayout);
|
||||
itemsByDomain.set(key, itemLayouts);
|
||||
};
|
||||
|
||||
for (const sectionLayout of geometry.sectionLayouts) {
|
||||
const key = `${sectionLayout.layoutId}\0${sectionLayout.sectionId}`;
|
||||
if (sectionLayoutsByKey.has(key)) {
|
||||
errors.push(`container ${sectionLayout.sectionId} has duplicate placement in layout ${sectionLayout.layoutId}`);
|
||||
continue;
|
||||
}
|
||||
sectionLayoutsByKey.set(key, sectionLayout);
|
||||
}
|
||||
|
||||
const parentCapacity = (
|
||||
layout: GeometryLayout,
|
||||
parentSection: GeometrySection,
|
||||
): { columns: number; kind: "lane" | "container" } | undefined => {
|
||||
if (parentSection.kind === "empty") {
|
||||
return {
|
||||
columns: getBoardLaneColumnCount(layout, getRootSectionLane(parentSection.xOffset)),
|
||||
kind: "lane",
|
||||
};
|
||||
}
|
||||
|
||||
const parentLayout = sectionLayoutsByKey.get(`${layout.id}\0${parentSection.id}`);
|
||||
if (!parentLayout) return undefined;
|
||||
return { columns: parentLayout.width, kind: "container" };
|
||||
};
|
||||
|
||||
for (const sectionLayout of geometry.sectionLayouts) {
|
||||
const layout = layoutsById.get(sectionLayout.layoutId);
|
||||
const section = sectionsById.get(sectionLayout.sectionId);
|
||||
const parent = sectionLayout.parentSectionId ? sectionsById.get(sectionLayout.parentSectionId) : undefined;
|
||||
if (!layout || !section || !parent) {
|
||||
errors.push(
|
||||
`container ${sectionLayout.sectionId} in layout ${sectionLayout.layoutId} has unknown geometry references`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (section.kind !== "container" || section.boardId !== layout.boardId || parent.boardId !== layout.boardId) {
|
||||
errors.push(`container ${section.id} in layout ${layout.id} crosses a board or root boundary`);
|
||||
continue;
|
||||
}
|
||||
if (!placementIsValid(sectionLayout)) {
|
||||
errors.push(`container ${section.id} in layout ${layout.id} has an invalid placement`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const capacity = parentCapacity(layout, parent);
|
||||
if (!capacity) {
|
||||
errors.push(`container ${section.id} in layout ${layout.id} has an unplaced parent container`);
|
||||
} else if (sectionLayout.xOffset + sectionLayout.width > capacity.columns) {
|
||||
errors.push(
|
||||
`container ${section.id} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`,
|
||||
);
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
let currentSectionId: string | null = section.id;
|
||||
while (currentSectionId) {
|
||||
if (visited.has(currentSectionId)) {
|
||||
errors.push(`container cycle detected in layout ${layout.id} at ${currentSectionId}`);
|
||||
break;
|
||||
}
|
||||
visited.add(currentSectionId);
|
||||
const current = sectionsById.get(currentSectionId);
|
||||
if (!current || current.kind === "empty") break;
|
||||
currentSectionId = sectionLayoutsByKey.get(`${layout.id}\0${currentSectionId}`)?.parentSectionId ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
for (const itemLayout of geometry.itemLayouts) {
|
||||
const layout = layoutsById.get(itemLayout.layoutId);
|
||||
const parent = sectionsById.get(itemLayout.sectionId);
|
||||
if (!layout || !parent || itemLayout.boardId !== layout.boardId || parent.boardId !== layout.boardId) {
|
||||
errors.push(`item ${itemLayout.itemId} in layout ${itemLayout.layoutId} crosses a board or section boundary`);
|
||||
continue;
|
||||
}
|
||||
if (!placementIsValid(itemLayout)) {
|
||||
errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an invalid placement`);
|
||||
continue;
|
||||
}
|
||||
const capacity = parentCapacity(layout, parent);
|
||||
if (!capacity) {
|
||||
errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an unplaced parent container`);
|
||||
continue;
|
||||
}
|
||||
if (itemLayout.xOffset + itemLayout.width > capacity.columns) {
|
||||
errors.push(
|
||||
`item ${itemLayout.itemId} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
addItemLayout(itemLayout);
|
||||
}
|
||||
|
||||
const sortedDomains = [...itemsByDomain.entries()].toSorted(([first], [second]) => {
|
||||
if (first < second) return -1;
|
||||
if (first > second) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (const [domain, itemLayouts] of sortedDomains) {
|
||||
const separatorIndex = domain.indexOf("\0");
|
||||
const layoutId = domain.slice(0, separatorIndex);
|
||||
const sectionId = domain.slice(separatorIndex + 1);
|
||||
const sortedItemLayouts = itemLayouts.toSorted(compareItemLayouts);
|
||||
for (const [index, first] of sortedItemLayouts.entries()) {
|
||||
for (const second of sortedItemLayouts.slice(index + 1)) {
|
||||
if (!placementsOverlap(first, second)) continue;
|
||||
errors.push(`item ${first.itemId} overlaps item ${second.itemId} in layout ${layoutId} section ${sectionId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
@@ -0,0 +1,262 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { FixtureGeometry } from "./geometry.mts";
|
||||
import { findFixtureGeometryErrors } from "./geometry.mts";
|
||||
|
||||
const baseGeometry = (): FixtureGeometry => ({
|
||||
layouts: [
|
||||
{
|
||||
id: "layout-mobile",
|
||||
boardId: "board",
|
||||
columnCount: 3,
|
||||
leftGutterColumnCount: 0,
|
||||
rightGutterColumnCount: 0,
|
||||
role: "mobile",
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{ id: "root", boardId: "board", kind: "empty", xOffset: 0 },
|
||||
{ id: "level-1", boardId: "board", kind: "container", xOffset: null },
|
||||
{ id: "level-2", boardId: "board", kind: "container", xOffset: null },
|
||||
{ id: "level-3", boardId: "board", kind: "container", xOffset: null },
|
||||
],
|
||||
sectionLayouts: [
|
||||
{
|
||||
sectionId: "level-1",
|
||||
layoutId: "layout-mobile",
|
||||
parentSectionId: "root",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 3,
|
||||
height: 6,
|
||||
},
|
||||
{
|
||||
sectionId: "level-2",
|
||||
layoutId: "layout-mobile",
|
||||
parentSectionId: "level-1",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 2,
|
||||
height: 5,
|
||||
},
|
||||
{
|
||||
sectionId: "level-3",
|
||||
layoutId: "layout-mobile",
|
||||
parentSectionId: "level-2",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 4,
|
||||
},
|
||||
],
|
||||
itemLayouts: [
|
||||
{
|
||||
itemId: "nested-item",
|
||||
boardId: "board",
|
||||
sectionId: "level-3",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe("release-v2 QA fixture geometry", () => {
|
||||
test("rejects exact duplicate item placements in the same section", () => {
|
||||
const geometry = baseGeometry();
|
||||
geometry.itemLayouts.push({
|
||||
itemId: "duplicate-item",
|
||||
boardId: "board",
|
||||
sectionId: "level-3",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 2,
|
||||
});
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry)).toContain(
|
||||
"item duplicate-item overlaps item nested-item in layout layout-mobile section level-3",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects partial item overlaps in the same section", () => {
|
||||
const geometry = baseGeometry();
|
||||
geometry.sections = [{ id: "root", boardId: "board", kind: "empty", xOffset: 0 }];
|
||||
geometry.sectionLayouts = [];
|
||||
geometry.itemLayouts = [
|
||||
{
|
||||
itemId: "first-item",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
itemId: "second-item",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 1,
|
||||
yOffset: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
];
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry)).toContain(
|
||||
"item first-item overlaps item second-item in layout layout-mobile section root",
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts tightly packed items that only share edges", () => {
|
||||
const geometry = baseGeometry();
|
||||
geometry.sections = [{ id: "root", boardId: "board", kind: "empty", xOffset: 0 }];
|
||||
geometry.sectionLayouts = [];
|
||||
geometry.itemLayouts = [
|
||||
{
|
||||
itemId: "top-left",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
itemId: "top-right",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 1,
|
||||
yOffset: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
itemId: "bottom",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 2,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
];
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry)).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not compare local coordinates from different parent sections", () => {
|
||||
const geometry = baseGeometry();
|
||||
geometry.sections = [
|
||||
{ id: "root", boardId: "board", kind: "empty", xOffset: 0 },
|
||||
{ id: "left-container", boardId: "board", kind: "container", xOffset: null },
|
||||
{ id: "right-container", boardId: "board", kind: "container", xOffset: null },
|
||||
];
|
||||
geometry.sectionLayouts = [
|
||||
{
|
||||
sectionId: "left-container",
|
||||
layoutId: "layout-mobile",
|
||||
parentSectionId: "root",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
sectionId: "right-container",
|
||||
layoutId: "layout-mobile",
|
||||
parentSectionId: "root",
|
||||
xOffset: 1,
|
||||
yOffset: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
];
|
||||
geometry.itemLayouts = [
|
||||
{
|
||||
itemId: "left-item",
|
||||
boardId: "board",
|
||||
sectionId: "left-container",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
itemId: "right-item",
|
||||
boardId: "board",
|
||||
sectionId: "right-container",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
];
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry)).toEqual([]);
|
||||
});
|
||||
|
||||
test("rejects an item that fits total columns but overflows the gutter-reduced main lane", () => {
|
||||
const geometry = baseGeometry();
|
||||
geometry.layouts[0] = {
|
||||
id: "layout-mobile",
|
||||
boardId: "board",
|
||||
role: "base",
|
||||
columnCount: 12,
|
||||
leftGutterColumnCount: 2,
|
||||
rightGutterColumnCount: 2,
|
||||
};
|
||||
geometry.sections = [{ id: "root", boardId: "board", kind: "empty", xOffset: 0 }];
|
||||
geometry.sectionLayouts = [];
|
||||
geometry.itemLayouts = [
|
||||
{
|
||||
itemId: "gutter-overflow",
|
||||
boardId: "board",
|
||||
sectionId: "root",
|
||||
layoutId: "layout-mobile",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 9,
|
||||
height: 1,
|
||||
},
|
||||
];
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry)).toContain(
|
||||
"item gutter-overflow in layout layout-mobile exceeds its 8-column parent lane",
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts valid three-level mobile nesting and rejects a child wider than its container", () => {
|
||||
const geometry = baseGeometry();
|
||||
expect(findFixtureGeometryErrors(geometry)).toEqual([]);
|
||||
|
||||
const nestedItem = geometry.itemLayouts.at(0);
|
||||
if (!nestedItem) throw new Error("Missing nested fixture item");
|
||||
nestedItem.width = 2;
|
||||
expect(findFixtureGeometryErrors(geometry)).toContain(
|
||||
"item nested-item in layout layout-mobile exceeds its 1-column parent container",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects recursive container parent cycles", () => {
|
||||
const geometry = baseGeometry();
|
||||
const firstSection = geometry.sectionLayouts.at(0);
|
||||
if (!firstSection) throw new Error("Missing nested fixture section");
|
||||
firstSection.parentSectionId = "level-3";
|
||||
|
||||
expect(findFixtureGeometryErrors(geometry).some((error) => error.includes("container cycle"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
export const releaseV2QaBoardAccessValues = ["none", "view", "modify", "full"] as const;
|
||||
|
||||
export type ReleaseV2QaBoardAccess = (typeof releaseV2QaBoardAccessValues)[number];
|
||||
export type ReleaseV2QaPacketBoardAccess = Record<string, Record<string, Record<string, ReleaseV2QaBoardAccess>>>;
|
||||
|
||||
export interface ReleaseV2QaCoverageAccessManifest {
|
||||
boards: { id: string }[];
|
||||
packets: { id: string; boards: string[]; personas: string[] }[];
|
||||
personas: string[];
|
||||
}
|
||||
|
||||
export const releaseV2QaPacketBoardAccess = {
|
||||
"preflight-01": {},
|
||||
"preflight-02": {
|
||||
"Avery Admin": { "qa-grid-24": "full", "qa-permissions-public": "full" },
|
||||
"Rowan Owner": { "qa-grid-24": "full", "qa-permissions-public": "full" },
|
||||
"Vivian Viewer": { "qa-grid-24": "view", "qa-permissions-public": "view" },
|
||||
"Nolan Outsider": { "qa-grid-24": "none", "qa-permissions-public": "none" },
|
||||
},
|
||||
"preflight-03": {},
|
||||
"board-01": {
|
||||
"Rowan Owner": { "qa-grid-24": "full" },
|
||||
},
|
||||
"board-02": {
|
||||
"Eden Editor": { "qa-scroll-lab": "modify" },
|
||||
},
|
||||
"board-03": {
|
||||
"Casey Chaos": { "qa-dense-collisions": "modify" },
|
||||
},
|
||||
"board-04": {
|
||||
"Rowan Owner": { "qa-nested-containers": "full" },
|
||||
},
|
||||
"board-05": {
|
||||
"Morgan Mobile": { "qa-layout-boundaries": "modify" },
|
||||
},
|
||||
"board-06": {
|
||||
"Brooke Minimalist": { "qa-icons-bookmarks": "modify" },
|
||||
},
|
||||
"board-07": {
|
||||
"Rowan Owner": { "qa-permissions-public": "full" },
|
||||
"Eden Editor": { "qa-permissions-public": "modify" },
|
||||
"Vivian Viewer": { "qa-permissions-public": "view" },
|
||||
"Nolan Outsider": { "qa-permissions-public": "none" },
|
||||
},
|
||||
"board-08": {
|
||||
"Avery Admin": { "qa-download-upload": "full" },
|
||||
},
|
||||
"board-09": {
|
||||
"Kira Keyboard": { "qa-grid-24": "modify" },
|
||||
},
|
||||
"widgets-01": {
|
||||
"Brooke Minimalist": { "qa-widgets-01": "modify" },
|
||||
},
|
||||
"widgets-02": {
|
||||
"Cora Creator": { "qa-widgets-02": "modify" },
|
||||
},
|
||||
"widgets-03": {
|
||||
"Cora Creator": { "qa-widgets-03": "modify" },
|
||||
},
|
||||
"widgets-04": {
|
||||
"Ingrid Infra": { "qa-widgets-04": "modify" },
|
||||
},
|
||||
"widgets-05": {
|
||||
"Ingrid Infra": { "qa-widgets-05": "modify" },
|
||||
},
|
||||
"widgets-06": {
|
||||
"Ingrid Infra": { "qa-widgets-06": "modify" },
|
||||
},
|
||||
"widgets-07": {
|
||||
"Ingrid Infra": { "qa-widgets-07": "modify" },
|
||||
},
|
||||
"widgets-08": {
|
||||
"Ingrid Infra": { "qa-widgets-08": "modify" },
|
||||
},
|
||||
"widgets-09": {
|
||||
"Maya Media": { "qa-widgets-09": "modify" },
|
||||
},
|
||||
"widgets-10": {
|
||||
"Maya Media": { "qa-widgets-10": "modify" },
|
||||
},
|
||||
"widgets-11": {
|
||||
"Ingrid Infra": { "qa-widgets-11": "modify" },
|
||||
},
|
||||
"widgets-12": {
|
||||
"Ash Assistant": { "qa-widgets-12": "modify", "qa-custom-widget-assistant": "modify" },
|
||||
"Cora Creator": { "qa-widgets-12": "modify", "qa-custom-widget-assistant": "modify" },
|
||||
},
|
||||
"core-v2-01": {
|
||||
"Cora Creator": { "qa-custom-widget-assistant": "modify" },
|
||||
},
|
||||
"core-v2-02": {
|
||||
"Ash Assistant": { "qa-custom-widget-assistant": "modify" },
|
||||
},
|
||||
"core-v2-03": {},
|
||||
"core-v2-04": {
|
||||
"Avery Admin": { "qa-permissions-public": "full" },
|
||||
"Nolan Outsider": { "qa-permissions-public": "none" },
|
||||
},
|
||||
"core-v2-05": {
|
||||
"Ingrid Infra": { "qa-widgets-07": "modify" },
|
||||
},
|
||||
"core-v2-06": {
|
||||
"Kira Keyboard": { "qa-grid-24": "modify" },
|
||||
},
|
||||
"core-v2-07": {
|
||||
"Vivian Viewer": { "qa-custom-widget-assistant": "view", "qa-permissions-public": "view" },
|
||||
},
|
||||
"core-v2-08": {
|
||||
"Casey Chaos": { "qa-custom-widget-assistant": "modify", "qa-widgets-12": "modify" },
|
||||
},
|
||||
"whole-product-01": {
|
||||
"Avery Admin": { "qa-grid-24": "full" },
|
||||
"Nora Newcomer": { "qa-grid-24": "modify" },
|
||||
},
|
||||
"whole-product-02": {
|
||||
"Rowan Owner": { "qa-grid-24": "full", "qa-custom-widget-assistant": "full" },
|
||||
},
|
||||
"whole-product-03": {
|
||||
"Eden Editor": { "qa-scroll-lab": "modify", "qa-layout-boundaries": "modify" },
|
||||
},
|
||||
"whole-product-04": {
|
||||
"Vivian Viewer": { "qa-permissions-public": "view" },
|
||||
"Nolan Outsider": { "qa-permissions-public": "none" },
|
||||
},
|
||||
"whole-product-05": {
|
||||
"Morgan Mobile": { "qa-layout-boundaries": "modify", "qa-widgets-01": "modify" },
|
||||
},
|
||||
"whole-product-06": {
|
||||
"Maya Media": { "qa-widgets-04": "modify", "qa-widgets-05": "modify" },
|
||||
},
|
||||
"whole-product-07": {
|
||||
"Ingrid Infra": {
|
||||
"qa-widgets-07": "modify",
|
||||
"qa-widgets-08": "modify",
|
||||
"qa-widgets-09": "modify",
|
||||
"qa-widgets-10": "modify",
|
||||
"qa-widgets-11": "modify",
|
||||
},
|
||||
},
|
||||
"whole-product-08": {
|
||||
"Cora Creator": { "qa-custom-widget-assistant": "modify" },
|
||||
"Ash Assistant": { "qa-custom-widget-assistant": "modify" },
|
||||
},
|
||||
"whole-product-09": {
|
||||
"Kira Keyboard": {
|
||||
"qa-grid-24": "full",
|
||||
"qa-download-upload": "full",
|
||||
"qa-custom-widget-assistant": "full",
|
||||
},
|
||||
"Casey Chaos": {
|
||||
"qa-grid-24": "full",
|
||||
"qa-download-upload": "full",
|
||||
"qa-custom-widget-assistant": "full",
|
||||
},
|
||||
},
|
||||
"performance-01": {
|
||||
"Avery Admin": { "qa-grid-24": "full", "qa-widgets-12": "full" },
|
||||
},
|
||||
"performance-02": {
|
||||
"Rowan Owner": { "qa-dense-collisions": "full", "qa-nested-containers": "full" },
|
||||
},
|
||||
"performance-03": {
|
||||
"Ingrid Infra": { "qa-widgets-04": "modify", "qa-widgets-07": "modify", "qa-widgets-12": "modify" },
|
||||
"Maya Media": { "qa-widgets-04": "modify", "qa-widgets-07": "modify", "qa-widgets-12": "modify" },
|
||||
},
|
||||
"performance-04": {
|
||||
"Casey Chaos": {
|
||||
"qa-grid-24": "modify",
|
||||
"qa-custom-widget-assistant": "modify",
|
||||
"qa-widgets-12": "modify",
|
||||
},
|
||||
},
|
||||
} as const satisfies ReleaseV2QaPacketBoardAccess;
|
||||
|
||||
const accessRank: Record<ReleaseV2QaBoardAccess, number> = {
|
||||
none: 0,
|
||||
view: 1,
|
||||
modify: 2,
|
||||
full: 3,
|
||||
};
|
||||
|
||||
export const getReleaseV2QaExpectedBoardAccess = (
|
||||
packetAccess: ReleaseV2QaPacketBoardAccess,
|
||||
): Record<string, Record<string, ReleaseV2QaBoardAccess>> => {
|
||||
const expected: Record<string, Record<string, ReleaseV2QaBoardAccess>> = {};
|
||||
for (const personaAccess of Object.values(packetAccess)) {
|
||||
for (const [persona, boards] of Object.entries(personaAccess)) {
|
||||
const expectedBoards = (expected[persona] ??= {});
|
||||
for (const [board, access] of Object.entries(boards)) {
|
||||
const current = expectedBoards[board];
|
||||
if (!current || accessRank[access] > accessRank[current]) expectedBoards[board] = access;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(expected)
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([persona, boards]) => [
|
||||
persona,
|
||||
Object.fromEntries(Object.entries(boards).toSorted(([left], [right]) => left.localeCompare(right))),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateValues = (values: string[]) => values.filter((value, index) => values.indexOf(value) !== index);
|
||||
|
||||
export const validateReleaseV2QaPacketBoardAccess = (
|
||||
manifest: ReleaseV2QaCoverageAccessManifest,
|
||||
packetAccess: ReleaseV2QaPacketBoardAccess,
|
||||
): string[] => {
|
||||
const errors: string[] = [];
|
||||
const packetIds = manifest.packets.map((packet) => packet.id);
|
||||
const accessPacketIds = Object.keys(packetAccess);
|
||||
for (const duplicate of new Set(duplicateValues(packetIds))) errors.push(`duplicate coverage packet ${duplicate}`);
|
||||
for (const packetId of packetIds) {
|
||||
if (!(packetId in packetAccess)) errors.push(`${packetId}: board access assignment is missing`);
|
||||
}
|
||||
for (const packetId of accessPacketIds) {
|
||||
if (!packetIds.includes(packetId)) errors.push(`${packetId}: board access assignment is not a coverage packet`);
|
||||
}
|
||||
|
||||
const knownPersonas = new Set(manifest.personas);
|
||||
const knownBoards = new Set(manifest.boards.map((board) => board.id));
|
||||
for (const packet of manifest.packets) {
|
||||
const assignedAccess = packetAccess[packet.id] ?? {};
|
||||
const expectedPersonas = packet.boards.length === 0 ? [] : packet.personas;
|
||||
for (const persona of expectedPersonas) {
|
||||
if (!(persona in assignedAccess)) errors.push(`${packet.id}: ${persona} board access assignment is missing`);
|
||||
}
|
||||
for (const persona of Object.keys(assignedAccess)) {
|
||||
if (!knownPersonas.has(persona)) errors.push(`${packet.id}: unknown persona ${persona}`);
|
||||
if (!packet.personas.includes(persona)) errors.push(`${packet.id}: unassigned persona ${persona}`);
|
||||
const assignedBoards = assignedAccess[persona] ?? {};
|
||||
for (const board of packet.boards) {
|
||||
if (!(board in assignedBoards)) errors.push(`${packet.id}: ${persona} access to ${board} is missing`);
|
||||
}
|
||||
for (const [board, access] of Object.entries(assignedBoards)) {
|
||||
if (!knownBoards.has(board)) errors.push(`${packet.id}: unknown board ${board}`);
|
||||
if (!packet.boards.includes(board)) errors.push(`${packet.id}: unassigned board ${board}`);
|
||||
if (!releaseV2QaBoardAccessValues.includes(access)) {
|
||||
errors.push(`${packet.id}: invalid ${persona} access to ${board}: ${String(access)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packet.boards.length === 0 && Object.keys(assignedAccess).length > 0) {
|
||||
errors.push(`${packet.id}: board-less packet must not assign board access`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getReleaseV2QaExpectedBoardAccess,
|
||||
releaseV2QaPacketBoardAccess,
|
||||
validateReleaseV2QaPacketBoardAccess,
|
||||
} from "./permissions.mts";
|
||||
|
||||
interface CoverageManifest {
|
||||
boards: { id: string }[];
|
||||
packets: { id: string; boards: string[]; personas: string[] }[];
|
||||
personas: string[];
|
||||
}
|
||||
|
||||
const coverageManifest = JSON.parse(
|
||||
await readFile(resolve(import.meta.dirname, "../../qa/release-v2/coverage-manifest.json"), "utf8"),
|
||||
) as CoverageManifest;
|
||||
|
||||
describe("release-v2 QA packet board access", () => {
|
||||
it("covers every assigned packet persona and board combination", () => {
|
||||
expect(validateReleaseV2QaPacketBoardAccess(coverageManifest, releaseV2QaPacketBoardAccess)).toEqual([]);
|
||||
expect(Object.keys(releaseV2QaPacketBoardAccess)).toHaveLength(45);
|
||||
});
|
||||
|
||||
it("grants mutation access to specialized packet owners without unrelated boards", () => {
|
||||
const access = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
|
||||
|
||||
expect(access["Casey Chaos"]).toEqual({
|
||||
"qa-custom-widget-assistant": "full",
|
||||
"qa-dense-collisions": "modify",
|
||||
"qa-download-upload": "full",
|
||||
"qa-grid-24": "full",
|
||||
"qa-widgets-12": "modify",
|
||||
});
|
||||
expect(access["Morgan Mobile"]).toEqual({
|
||||
"qa-layout-boundaries": "modify",
|
||||
"qa-widgets-01": "modify",
|
||||
});
|
||||
expect(access["Brooke Minimalist"]).toEqual({
|
||||
"qa-icons-bookmarks": "modify",
|
||||
"qa-widgets-01": "modify",
|
||||
});
|
||||
expect(access["Cora Creator"]).toEqual({
|
||||
"qa-custom-widget-assistant": "modify",
|
||||
"qa-widgets-02": "modify",
|
||||
"qa-widgets-03": "modify",
|
||||
"qa-widgets-12": "modify",
|
||||
});
|
||||
expect(access["Ash Assistant"]).toEqual({
|
||||
"qa-custom-widget-assistant": "modify",
|
||||
"qa-widgets-12": "modify",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves owner, editor, viewer, outsider, and administrator invariants", () => {
|
||||
const access = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
|
||||
|
||||
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("none");
|
||||
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("view");
|
||||
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("modify");
|
||||
expect(access["Rowan Owner"]?.["qa-permissions-public"]).toBe("full");
|
||||
expect(access["Eden Editor"]?.["qa-permissions-public"]).toBe("modify");
|
||||
expect(Object.values(access["Vivian Viewer"] ?? {})).toEqual(expect.arrayContaining(["view"]));
|
||||
expect(new Set(Object.values(access["Vivian Viewer"] ?? {}))).toEqual(new Set(["view"]));
|
||||
expect(new Set(Object.values(access["Nolan Outsider"] ?? {}))).toEqual(new Set(["none"]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const fullShaPattern = /^[0-9a-f]{40}$/iu;
|
||||
|
||||
export const releaseV2QaProfiles = ["main-writable", "main-readonly", "onboarding-fresh", "degraded"] as const;
|
||||
export type ReleaseV2QaProfile = (typeof releaseV2QaProfiles)[number];
|
||||
|
||||
export interface ReleaseV2QaProfileFlags {
|
||||
demoMode: boolean;
|
||||
demoReadOnly: boolean;
|
||||
unsafeMockIntegration: boolean;
|
||||
}
|
||||
|
||||
const expectedProfileFlags: Record<ReleaseV2QaProfile, ReleaseV2QaProfileFlags> = {
|
||||
"main-writable": { demoMode: true, demoReadOnly: false, unsafeMockIntegration: true },
|
||||
"main-readonly": { demoMode: true, demoReadOnly: true, unsafeMockIntegration: true },
|
||||
"onboarding-fresh": { demoMode: false, demoReadOnly: false, unsafeMockIntegration: true },
|
||||
degraded: { demoMode: true, demoReadOnly: false, unsafeMockIntegration: true },
|
||||
};
|
||||
|
||||
const profileFlagEnvironmentNames = {
|
||||
demoMode: "DEMO_MODE",
|
||||
demoReadOnly: "DEMO_READ_ONLY",
|
||||
unsafeMockIntegration: "UNSAFE_ENABLE_MOCK_INTEGRATION",
|
||||
} as const satisfies Record<keyof ReleaseV2QaProfileFlags, string>;
|
||||
|
||||
const parseStrictBoolean = (value: string | undefined) => {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const assertReleaseV2QaProfileEnvironment = (
|
||||
profile: ReleaseV2QaProfile,
|
||||
environment: NodeJS.ProcessEnv,
|
||||
): ReleaseV2QaProfileFlags => {
|
||||
const expected = expectedProfileFlags[profile];
|
||||
const invalidFlags: string[] = [];
|
||||
|
||||
for (const [flag, environmentName] of Object.entries(profileFlagEnvironmentNames) as Array<
|
||||
[keyof ReleaseV2QaProfileFlags, string]
|
||||
>) {
|
||||
const expectedValue = expected[flag];
|
||||
const actualValue = parseStrictBoolean(environment[environmentName]);
|
||||
if (actualValue !== expectedValue) invalidFlags.push(`${environmentName} (expected ${String(expectedValue)})`);
|
||||
}
|
||||
|
||||
if (invalidFlags.length > 0) {
|
||||
throw new Error(`QA profile '${profile}' has invalid or missing environment flags: ${invalidFlags.join(", ")}`);
|
||||
}
|
||||
|
||||
return { ...expected };
|
||||
};
|
||||
|
||||
export const validateCandidateSha = (candidateSha: string, checkoutSha: string) => {
|
||||
if (!fullShaPattern.test(candidateSha)) {
|
||||
throw new Error("QA candidate must be a full 40-character Git SHA");
|
||||
}
|
||||
if (!fullShaPattern.test(checkoutSha)) {
|
||||
throw new Error("The checked-out HEAD did not resolve to a full 40-character Git SHA");
|
||||
}
|
||||
if (candidateSha.toLowerCase() !== checkoutSha.toLowerCase()) {
|
||||
throw new Error("QA candidate does not match the checked-out HEAD");
|
||||
}
|
||||
return checkoutSha.toLowerCase();
|
||||
};
|
||||
|
||||
export const resolveCheckoutCandidateSha = (repoRoot: string) => {
|
||||
const result = spawnSync("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0) throw new Error("Could not resolve the checked-out HEAD for QA provenance");
|
||||
const checkoutSha = result.stdout.trim();
|
||||
return validateCandidateSha(checkoutSha, checkoutSha);
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { assertReleaseV2QaProfileEnvironment, releaseV2QaProfiles, validateCandidateSha } from "./provenance.mts";
|
||||
import type { ReleaseV2QaProfile } from "./provenance.mts";
|
||||
|
||||
const disposableDirectories: string[] = [];
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const profileEnvironmentNames = ["DEMO_MODE", "DEMO_READ_ONLY", "UNSAFE_ENABLE_MOCK_INTEGRATION"] as const;
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
disposableDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })),
|
||||
);
|
||||
});
|
||||
|
||||
const environmentForProfile = (profile: ReleaseV2QaProfile): NodeJS.ProcessEnv => {
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
DEMO_MODE: "true",
|
||||
DEMO_READ_ONLY: "false",
|
||||
UNSAFE_ENABLE_MOCK_INTEGRATION: "true",
|
||||
};
|
||||
if (profile === "main-readonly") environment.DEMO_READ_ONLY = "true";
|
||||
if (profile === "onboarding-fresh") environment.DEMO_MODE = "false";
|
||||
return environment;
|
||||
};
|
||||
|
||||
describe("release-v2 QA candidate provenance", () => {
|
||||
test("accepts only the exact checked-out commit", () => {
|
||||
const checkoutSha = "a".repeat(40);
|
||||
|
||||
expect(validateCandidateSha(checkoutSha.toUpperCase(), checkoutSha)).toBe(checkoutSha);
|
||||
expect(() => validateCandidateSha("b".repeat(40), checkoutSha)).toThrow("checked-out HEAD");
|
||||
expect(() => validateCandidateSha("not-a-sha", checkoutSha)).toThrow("full 40-character Git SHA");
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA profile provenance", () => {
|
||||
test.each(releaseV2QaProfiles)("accepts the exact %s profile matrix", (profile) => {
|
||||
expect(assertReleaseV2QaProfileEnvironment(profile, environmentForProfile(profile))).toEqual({
|
||||
demoMode: profile !== "onboarding-fresh",
|
||||
demoReadOnly: profile === "main-readonly",
|
||||
unsafeMockIntegration: true,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(profileEnvironmentNames)("rejects an unset %s flag", (environmentName) => {
|
||||
const environment = environmentForProfile("main-writable");
|
||||
delete environment[environmentName];
|
||||
|
||||
expect(() => assertReleaseV2QaProfileEnvironment("main-writable", environment)).toThrow(
|
||||
new RegExp(`${environmentName} \\(expected (?:true|false)\\)`, "u"),
|
||||
);
|
||||
});
|
||||
|
||||
test.each(["1", "0", "TRUE", "False", "yes", " true ", "", "unexpected-private-marker"])(
|
||||
"rejects the malformed strict boolean %j without echoing it",
|
||||
(malformedValue) => {
|
||||
for (const environmentName of profileEnvironmentNames) {
|
||||
const environment = environmentForProfile("main-writable");
|
||||
environment[environmentName] = malformedValue;
|
||||
|
||||
expect(() => assertReleaseV2QaProfileEnvironment("main-writable", environment)).toThrow(environmentName);
|
||||
try {
|
||||
assertReleaseV2QaProfileEnvironment("main-writable", environment);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).not.toContain(malformedValue || "unexpected-private-marker");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(releaseV2QaProfiles)("rejects every mismatched flag for %s", (profile) => {
|
||||
const expectedEnvironment = environmentForProfile(profile);
|
||||
for (const environmentName of profileEnvironmentNames) {
|
||||
const environment = { ...expectedEnvironment };
|
||||
environment[environmentName] = environment[environmentName] === "true" ? "false" : "true";
|
||||
expect(() => assertReleaseV2QaProfileEnvironment(profile, environment)).toThrow(environmentName);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a writable ambient environment before a read-only reset or manifest write", async () => {
|
||||
const disposableDirectory = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-profile-flags-"));
|
||||
disposableDirectories.push(disposableDirectory);
|
||||
const databasePath = path.join(disposableDirectory, "fixture.sqlite");
|
||||
const outputDirectory = path.join(disposableDirectory, "output");
|
||||
const database = new Database(databasePath);
|
||||
database.exec("CREATE TABLE mutation_guard (value TEXT NOT NULL); INSERT INTO mutation_guard VALUES ('preserved')");
|
||||
database.close();
|
||||
const databaseBefore = await readFile(databasePath);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
path.join(repoRoot, "scripts/release-v2-qa/seed.mts"),
|
||||
"--profile",
|
||||
"main-readonly",
|
||||
"--output",
|
||||
outputDirectory,
|
||||
"--reset",
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
DB_DRIVER: "better-sqlite3",
|
||||
DB_URL: databasePath,
|
||||
DEMO_MODE: "true",
|
||||
DEMO_READ_ONLY: "false",
|
||||
UNSAFE_ENABLE_MOCK_INTEGRATION: "true",
|
||||
},
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("DEMO_READ_ONLY (expected true)");
|
||||
expect(result.stderr).not.toContain("no such table");
|
||||
expect(await readFile(databasePath)).toEqual(databaseBefore);
|
||||
await expect(access(path.join(outputDirectory, "fixture-manifest.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveCheckoutCandidateSha } from "./provenance.mts";
|
||||
import { validateResolvedArtifactPath, writeSafeReportFile } from "./report-path-integrity.mts";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const qaRoot = path.join(repoRoot, "qa/release-v2");
|
||||
const candidateSha = resolveCheckoutCandidateSha(repoRoot);
|
||||
|
||||
type Status = "passed" | "failed" | "blocked" | "not-reached";
|
||||
|
||||
interface Packet {
|
||||
id: string;
|
||||
wave: string;
|
||||
title: string;
|
||||
prRefs: number[];
|
||||
personas: string[];
|
||||
boards: string[];
|
||||
profiles: string[];
|
||||
viewports: string[];
|
||||
zooms: number[];
|
||||
inputs: string[];
|
||||
widgetKinds?: string[];
|
||||
cases: string[];
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
caseDimensions: Record<string, { id: string; expected: string }[]>;
|
||||
packets: Packet[];
|
||||
}
|
||||
|
||||
interface Options {
|
||||
packetId: string;
|
||||
url: string;
|
||||
profile: string;
|
||||
persona: string;
|
||||
session: string;
|
||||
viewport: string;
|
||||
input: string;
|
||||
zoom: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const profileFlags: Record<string, string[]> = {
|
||||
"main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"main-readonly": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
};
|
||||
|
||||
const exhaustiveSizeWidgetKinds = new Set([
|
||||
"downloads",
|
||||
"calendar",
|
||||
"mediaServer",
|
||||
"mediaRequests-requestList",
|
||||
"mediaRequests-requestStats",
|
||||
"systemResources",
|
||||
"systemDisks",
|
||||
"dockerContainers",
|
||||
"networkControllerSummary",
|
||||
"networkControllerStatus",
|
||||
"beszelSystemStats",
|
||||
"uptimeKuma",
|
||||
"bookmarks",
|
||||
"notebook",
|
||||
"iframe",
|
||||
"customApi",
|
||||
"assistant",
|
||||
]);
|
||||
|
||||
const parseOptions = (): Options => {
|
||||
const values = new Map<string, string>();
|
||||
const args = process.argv.slice(2).filter((argument) => argument !== "--");
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const key = args[index];
|
||||
const value = args[index + 1];
|
||||
if (!key?.startsWith("--") || !value) throw new Error(`Invalid argument ${key ?? "<missing>"}`);
|
||||
values.set(key.slice(2), value);
|
||||
}
|
||||
|
||||
const required = (key: string) => {
|
||||
const value = values.get(key);
|
||||
if (!value) throw new Error(`--${key} is required`);
|
||||
return value;
|
||||
};
|
||||
const zoom = Number(required("zoom"));
|
||||
if (!Number.isFinite(zoom) || zoom <= 0) throw new Error("--zoom must be a positive number");
|
||||
return {
|
||||
packetId: required("packet"),
|
||||
url: required("url"),
|
||||
profile: required("profile"),
|
||||
persona: required("persona"),
|
||||
session: required("session"),
|
||||
viewport: required("viewport"),
|
||||
input: required("input"),
|
||||
zoom,
|
||||
timestamp: values.get("timestamp") ?? new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
const listFiles = async (directory: string): Promise<string[]> => {
|
||||
const output: string[] = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) output.push(...(await listFiles(entryPath)));
|
||||
else if (entry.isFile()) {
|
||||
const artifactError = await validateResolvedArtifactPath(directory, entryPath, "blocked-report artifact");
|
||||
if (artifactError) throw new Error(artifactError);
|
||||
const file = await lstat(entryPath);
|
||||
if (!entryPath.endsWith(".webm") || file.size > 0) output.push(entryPath);
|
||||
} else {
|
||||
throw new Error(`Blocked-report artifact tree contains a non-regular entry: ${entry.name}`);
|
||||
}
|
||||
}
|
||||
return output.toSorted();
|
||||
};
|
||||
|
||||
const escapeCell = (value: string) => value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
||||
const list = (values: (string | number)[]) => {
|
||||
if (values.length === 0) return "None";
|
||||
return values.join(", ");
|
||||
};
|
||||
|
||||
const sizeRequirement = (widgetKind: string) => {
|
||||
if (exhaustiveSizeWidgetKinds.has(widgetKind)) return "every width 1-24 × every height 1-6";
|
||||
return "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds";
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const options = parseOptions();
|
||||
const manifest = JSON.parse(await readFile(path.join(qaRoot, "coverage-manifest.json"), "utf8")) as Manifest;
|
||||
const packet = manifest.packets.find((candidate) => candidate.id === options.packetId);
|
||||
if (!packet) throw new Error(`Unknown packet ${options.packetId}`);
|
||||
if (!packet.profiles.includes(options.profile)) throw new Error(`${packet.id} does not include ${options.profile}`);
|
||||
if (!packet.personas.includes(options.persona)) throw new Error(`${packet.id} does not include ${options.persona}`);
|
||||
if (!packet.viewports.includes(options.viewport))
|
||||
throw new Error(`${packet.id} does not include ${options.viewport}`);
|
||||
if (!packet.inputs.includes(options.input)) throw new Error(`${packet.id} does not include ${options.input}`);
|
||||
if (!packet.zooms.includes(options.zoom)) throw new Error(`${packet.id} does not include ${options.zoom}%`);
|
||||
const flags = profileFlags[options.profile];
|
||||
if (!flags) throw new Error(`Unknown runtime profile ${options.profile}`);
|
||||
|
||||
const runtimeUrl = new URL(options.url);
|
||||
const actualPort = Number(runtimeUrl.port);
|
||||
if (!Number.isInteger(actualPort) || actualPort <= 0) throw new Error("Runtime URL must include an explicit port");
|
||||
const artifactDirectory = path.join(repoRoot, ".screenshots/release-v2", packet.id);
|
||||
const artifacts = await listFiles(artifactDirectory);
|
||||
if (artifacts.length === 0) throw new Error(`${packet.id} has no browser artifacts`);
|
||||
|
||||
const dimensions = manifest.caseDimensions[packet.wave] ?? [];
|
||||
const cases = packet.cases.flatMap((baseId) =>
|
||||
dimensions.map((dimension) => ({ id: `${baseId}-${dimension.id}`, expected: dimension.expected })),
|
||||
);
|
||||
const caseStatuses: Record<string, Status> = {};
|
||||
for (const testCase of cases) {
|
||||
let status: Status = "blocked";
|
||||
if (packet.wave === "preflight" && testCase.id.endsWith("-ENVIRONMENT")) status = "failed";
|
||||
if (packet.wave === "preflight" && testCase.id.endsWith("-EVIDENCE")) status = "passed";
|
||||
caseStatuses[testCase.id] = status;
|
||||
}
|
||||
const status: Status = Object.values(caseStatuses).includes("failed") ? "failed" : "blocked";
|
||||
const blockedCaseIds = Object.entries(caseStatuses)
|
||||
.filter(([, caseStatus]) => caseStatus !== "passed")
|
||||
.map(([caseId]) => caseId);
|
||||
const findingEvidence = artifacts.filter(
|
||||
(artifact) => artifact.endsWith(".png") || artifact.endsWith(".webm") || artifact.includes("network"),
|
||||
);
|
||||
const evidence = findingEvidence.length > 0 ? findingEvidence : artifacts;
|
||||
const hasRetryEvidence = artifacts.some((artifact) => /fresh|retry/u.test(path.basename(artifact)));
|
||||
let findingSummary =
|
||||
"Chromium repeatedly requests the same entry route and ends on ERR_TOO_MANY_REDIRECTS, preventing the assigned browser flow.";
|
||||
let retryLimitation =
|
||||
"The packet's retry did not produce a separate artifact; the same finding has independent reproductions in the preflight reports.";
|
||||
let retryStep = "5. Compare the result with the independently reproduced preflight evidence.";
|
||||
let reproducibility = "reproduced independently by the preflight agents";
|
||||
if (hasRetryEvidence) {
|
||||
findingSummary =
|
||||
"A fresh Chromium session repeatedly requests the same entry route and ends on ERR_TOO_MANY_REDIRECTS; a fresh-context retry does the same.";
|
||||
retryLimitation =
|
||||
"The finding was retried in a fresh Chromium context; console output was empty while document requests repeated until Chromium stopped the loop.";
|
||||
retryStep = "5. Close the context, retry in a fresh context, and observe the same result.";
|
||||
reproducibility = "reproduced on retry and independently by the preflight agents";
|
||||
}
|
||||
const reproductionAgent = packet.id === "preflight-01" ? "qa-v2-preflight-02" : "qa-v2-preflight-01";
|
||||
const widgetChecks = (packet.widgetKinds ?? []).flatMap((widgetKind) =>
|
||||
packet.viewports.map((viewport) => ({
|
||||
widgetKind,
|
||||
viewport,
|
||||
sizeRequirement: sizeRequirement(widgetKind),
|
||||
status: "blocked" as const,
|
||||
})),
|
||||
);
|
||||
const metadata = {
|
||||
packetId: packet.id,
|
||||
status,
|
||||
caseStatuses,
|
||||
execution: {
|
||||
candidateSha,
|
||||
url: options.url,
|
||||
actualPort,
|
||||
runtimeProfile: options.profile,
|
||||
runtimeFlags: flags,
|
||||
persona: options.persona,
|
||||
sessionId: options.session,
|
||||
timestamp: options.timestamp,
|
||||
viewport: options.viewport,
|
||||
input: options.input,
|
||||
zoom: options.zoom,
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
id: "RV2-P1-001",
|
||||
fingerprint: "release-v2-auth-locale-redirect-loop",
|
||||
severity: "P1",
|
||||
area: "routing/authentication",
|
||||
title: "Locale rewrite causes an infinite browser redirect before the application renders",
|
||||
summary: findingSummary,
|
||||
caseIds: blockedCaseIds,
|
||||
evidence,
|
||||
},
|
||||
],
|
||||
artifacts,
|
||||
widgetChecks,
|
||||
performance: {
|
||||
measurements: [],
|
||||
limitations: [
|
||||
"The application never rendered, so assigned feature, mutation, persistence, permission, viewport, and performance checks were blocked.",
|
||||
"No credentials were submitted and no product state was mutated.",
|
||||
retryLimitation,
|
||||
],
|
||||
},
|
||||
independentReproductions: [
|
||||
{
|
||||
findingFingerprint: "release-v2-auth-locale-redirect-loop",
|
||||
agentId: reproductionAgent,
|
||||
outcome: "reproduced",
|
||||
evidence: [],
|
||||
notes: `See ${reproductionAgent.replace("qa-v2-", "")} for an independent Luna Max reproduction.`,
|
||||
},
|
||||
],
|
||||
notes: "Report-only black-box run. No defect was fixed and no secret was recorded.",
|
||||
};
|
||||
|
||||
const caseRows = cases
|
||||
.map((testCase) => {
|
||||
const caseStatus = caseStatuses[testCase.id];
|
||||
return `| ${testCase.id} | ${caseStatus} | [browser artifacts](${artifactDirectory}) | The application entry repeated redirects until Chromium stopped navigation; the assigned flow was not reachable. | ${escapeCell(testCase.expected)} |`;
|
||||
})
|
||||
.join("\n");
|
||||
let widgetSection = "";
|
||||
if (widgetChecks.length > 0) {
|
||||
const widgetRows = widgetChecks
|
||||
.map(
|
||||
(check) =>
|
||||
`| ${check.widgetKind} | ${check.viewport} | ${check.sizeRequirement} | blocked before render | blocked before authentication | not reached | redirect retry reproduced | [artifacts](${artifactDirectory}) | ${check.status} |`,
|
||||
)
|
||||
.join("\n");
|
||||
widgetSection = `\n## Widget kind × viewport checklist\n\n| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n${widgetRows}\n`;
|
||||
}
|
||||
const artifactRows = artifacts.map((artifact) => `- [${path.basename(artifact)}](${artifact})`).join("\n");
|
||||
const report = `<!-- release-v2-qa-report
|
||||
${JSON.stringify(metadata, null, 2)}
|
||||
-->
|
||||
|
||||
# ${packet.id}: ${packet.title}
|
||||
|
||||
| Metadata | Value |
|
||||
| --- | --- |
|
||||
| Status | ${status} |
|
||||
| Wave | ${packet.wave} |
|
||||
| PR refs | ${packet.prRefs.map((number) => `#${number}`).join(", ")} |
|
||||
| Personas | ${list(packet.personas)} |
|
||||
| Boards | ${list(packet.boards)} |
|
||||
| Profiles | ${list(packet.profiles)} |
|
||||
| Chromium viewports | ${list(packet.viewports)} |
|
||||
| Zoom | ${list(packet.zooms.map((zoom) => `${zoom}%`))} |
|
||||
| Input | ${list(packet.inputs)} |
|
||||
| Widget kinds | ${list(packet.widgetKinds ?? [])} |
|
||||
| Artifact directory | [${artifactDirectory}](${artifactDirectory}) |
|
||||
| Candidate SHA | ${candidateSha} |
|
||||
| URL / actual port | ${options.url} / ${actualPort} |
|
||||
| Runtime profile / flags | ${options.profile}; ${flags.join(", ")} |
|
||||
| Executed persona | ${options.persona} |
|
||||
| Browser session ID | ${options.session} |
|
||||
| Timestamp | ${options.timestamp} |
|
||||
| Executed viewport / input / zoom | ${options.viewport} / ${options.input} / ${options.zoom}% |
|
||||
|
||||
## Dogfood evidence
|
||||
|
||||
| Case ID | Status | Evidence | Observed | Expected |
|
||||
| --- | --- | --- | --- | --- |
|
||||
${caseRows}
|
||||
${widgetSection}
|
||||
## Performance measurements and limitations
|
||||
|
||||
No valid feature timing could be recorded because no application document rendered. Cold/warm, polling, rendering, memory, and recovery measurements remain blocked.
|
||||
|
||||
## Independent reproductions
|
||||
|
||||
The common routing failure was independently reproduced by ${reproductionAgent}; the corresponding tracked packet contains its own browser evidence.
|
||||
|
||||
## Findings
|
||||
|
||||
### RV2-P1-001 — P1: locale rewrite causes an infinite browser redirect
|
||||
|
||||
Preconditions: candidate ${candidateSha}, ${options.profile}, ${options.viewport}, ${options.zoom}% zoom, ${options.input}, fresh named Chromium session ${options.session}.
|
||||
|
||||
1. Launch a fresh Chromium context with the Chromium argument --no-sandbox.
|
||||
2. Set the assigned viewport and open ${options.url} or its authentication entry.
|
||||
3. Observe repeated document requests to the same route.
|
||||
4. Wait until Chromium displays ERR_TOO_MANY_REDIRECTS or chrome-error://chromewebdata/.
|
||||
${retryStep}
|
||||
|
||||
Expected: the application renders the assigned entry, permitting authentication and the packet flow. Actual: the redirect loop prevents the application from rendering. Reproducibility: ${reproducibility}. Console output was empty; network and visual evidence are linked below.
|
||||
|
||||
## Artifacts
|
||||
|
||||
${artifactRows}
|
||||
|
||||
## Mutations and persistence
|
||||
|
||||
No credentials were submitted and no product mutation occurred. Reload and fresh-session retries reproduced the routing failure, so application persistence was not reachable.
|
||||
|
||||
## Blockers and gaps
|
||||
|
||||
Every non-evidence case listed as blocked remains untested beyond the routing entry. No blocked cell is treated as passed.
|
||||
|
||||
## Cleanup
|
||||
|
||||
The named browser session was closed by the browser agent. Artifacts remain under the untracked packet directory; no secret values were captured.
|
||||
`;
|
||||
const reportPath = path.join(qaRoot, "reports", packet.id, "report.md");
|
||||
await writeSafeReportFile(qaRoot, reportPath, report, `${packet.id} report path`);
|
||||
console.log(`Normalized blocked browser report ${packet.id} with ${artifacts.length} artifact(s)`);
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,663 @@
|
||||
export type QaStatus = "passed" | "failed" | "blocked" | "not-reached";
|
||||
|
||||
export interface QaWidgetCheck {
|
||||
widgetKind: string;
|
||||
viewport: string;
|
||||
sizeRequirement: string;
|
||||
status: QaStatus;
|
||||
}
|
||||
|
||||
export type QaSeverity = "P0" | "P1" | "P2" | "P3";
|
||||
|
||||
export interface QaFinding {
|
||||
id?: string;
|
||||
fingerprint?: string;
|
||||
severity: QaSeverity;
|
||||
title: string;
|
||||
area?: string;
|
||||
summary?: string;
|
||||
caseIds?: string[];
|
||||
evidence?: string[];
|
||||
}
|
||||
|
||||
export interface QaIndependentReproduction {
|
||||
findingFingerprint: string;
|
||||
agentId: string;
|
||||
outcome: string;
|
||||
evidence: string[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface AggregatedQaFinding extends QaFinding {
|
||||
packets: string[];
|
||||
caseIds: string[];
|
||||
evidence: string[];
|
||||
reproductions: QaIndependentReproduction[];
|
||||
}
|
||||
|
||||
export interface QaReportMetadata {
|
||||
packetId: string;
|
||||
status: QaStatus;
|
||||
caseStatuses: Record<string, QaStatus>;
|
||||
execution: {
|
||||
candidateSha: string | null;
|
||||
url: string | null;
|
||||
actualPort: number | null;
|
||||
runtimeProfile: string | null;
|
||||
runtimeFlags: string[];
|
||||
persona: string | null;
|
||||
sessionId: string | null;
|
||||
timestamp: string | null;
|
||||
viewport: string | null;
|
||||
input: string | null;
|
||||
zoom: number | null;
|
||||
};
|
||||
findings: QaFinding[];
|
||||
artifacts: string[];
|
||||
widgetChecks: QaWidgetCheck[];
|
||||
performance: {
|
||||
measurements: {
|
||||
name: string;
|
||||
value: number | null;
|
||||
unit: string;
|
||||
threshold: string;
|
||||
status: QaStatus;
|
||||
evidence: string[];
|
||||
}[];
|
||||
limitations: string[];
|
||||
};
|
||||
independentReproductions: QaIndependentReproduction[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface CoverageGap {
|
||||
packetId: string;
|
||||
kind: "case" | "widget" | "metadata";
|
||||
itemId: string;
|
||||
status: QaStatus;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface NormalizeOptions {
|
||||
packetId: string;
|
||||
expectedCaseIds: string[];
|
||||
expectedWidgetChecks: QaWidgetCheck[];
|
||||
}
|
||||
|
||||
export interface ReportIntegrityOptions {
|
||||
campaignCandidateSha: string;
|
||||
packetId: string;
|
||||
profiles: string[];
|
||||
personas: string[];
|
||||
viewports: string[];
|
||||
zooms: number[];
|
||||
inputs: string[];
|
||||
expectedProfileFlags: Record<string, string[]>;
|
||||
}
|
||||
|
||||
const allowedStatuses = new Set<QaStatus>(["passed", "failed", "blocked", "not-reached"]);
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const textOrNull = (value: unknown): string | null => (typeof value === "string" ? value : null);
|
||||
const numberOrNull = (value: unknown): number | null => (typeof value === "number" ? value : null);
|
||||
const strings = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
|
||||
|
||||
const statusOrNotReached = (value: unknown): QaStatus =>
|
||||
allowedStatuses.has(value as QaStatus) ? (value as QaStatus) : "not-reached";
|
||||
const severityOrP3 = (value: unknown): QaSeverity =>
|
||||
["P0", "P1", "P2", "P3"].includes(String(value)) ? (value as QaSeverity) : "P3";
|
||||
|
||||
const checkKey = (check: Pick<QaWidgetCheck, "widgetKind" | "viewport">): string =>
|
||||
`${check.widgetKind}\u0000${check.viewport}`;
|
||||
|
||||
const sameSet = <T,>(left: T[], right: T[]): boolean =>
|
||||
left.length === right.length && new Set(left).size === left.length && left.every((value) => right.includes(value));
|
||||
|
||||
const severityRank: Record<QaSeverity, number> = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
||||
|
||||
const uniqueStrings = (values: string[]): string[] => [...new Set(values)];
|
||||
|
||||
const reproductionKey = (reproduction: QaIndependentReproduction): string =>
|
||||
`${reproduction.findingFingerprint}\u0000${reproduction.agentId}\u0000${reproduction.outcome}`;
|
||||
|
||||
const mergeReproduction = (finding: AggregatedQaFinding, reproduction: QaIndependentReproduction): void => {
|
||||
const key = reproductionKey(reproduction);
|
||||
const existing = finding.reproductions.find((candidate) => reproductionKey(candidate) === key);
|
||||
if (!existing) {
|
||||
finding.reproductions.push({ ...reproduction, evidence: uniqueStrings(reproduction.evidence) });
|
||||
return;
|
||||
}
|
||||
existing.evidence = uniqueStrings([...existing.evidence, ...reproduction.evidence]);
|
||||
if (reproduction.notes && !existing.notes.includes(reproduction.notes)) {
|
||||
existing.notes = [existing.notes, reproduction.notes].filter(Boolean).join("; ");
|
||||
}
|
||||
};
|
||||
|
||||
export const aggregateQaFindings = (
|
||||
reports: {
|
||||
packetId: string;
|
||||
findings: QaFinding[];
|
||||
independentReproductions: QaIndependentReproduction[];
|
||||
}[],
|
||||
): AggregatedQaFinding[] => {
|
||||
const findings = new Map<string, AggregatedQaFinding>();
|
||||
for (const report of reports) {
|
||||
for (const finding of report.findings) {
|
||||
const key =
|
||||
finding.fingerprint ?? `${finding.severity}:${finding.area ?? "general"}:${finding.title}`.toLowerCase();
|
||||
const existing = findings.get(key);
|
||||
if (!existing) {
|
||||
findings.set(key, {
|
||||
...finding,
|
||||
packets: [report.packetId],
|
||||
caseIds: uniqueStrings(finding.caseIds ?? []),
|
||||
evidence: uniqueStrings(finding.evidence ?? []),
|
||||
reproductions: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.packets = uniqueStrings([...existing.packets, report.packetId]);
|
||||
existing.caseIds = uniqueStrings([...existing.caseIds, ...(finding.caseIds ?? [])]);
|
||||
existing.evidence = uniqueStrings([...existing.evidence, ...(finding.evidence ?? [])]);
|
||||
if (severityRank[finding.severity] < severityRank[existing.severity]) {
|
||||
existing.severity = finding.severity;
|
||||
existing.id = finding.id ?? existing.id;
|
||||
existing.title = finding.title;
|
||||
existing.area = finding.area ?? existing.area;
|
||||
existing.summary = finding.summary ?? existing.summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const report of reports) {
|
||||
for (const reproduction of report.independentReproductions) {
|
||||
const finding = findings.get(reproduction.findingFingerprint);
|
||||
if (finding) mergeReproduction(finding, reproduction);
|
||||
}
|
||||
}
|
||||
|
||||
return [...findings.values()].toSorted(
|
||||
(left, right) =>
|
||||
severityRank[left.severity] - severityRank[right.severity] || left.title.localeCompare(right.title),
|
||||
);
|
||||
};
|
||||
|
||||
const markdownTableCells = (line: string): string[] | null => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) return null;
|
||||
return trimmed
|
||||
.slice(1, -1)
|
||||
.split(/(?<!\\)\|/u)
|
||||
.map((cell) => cell.trim().replaceAll("\\|", "|"));
|
||||
};
|
||||
|
||||
export const collectHumanWidgetStatusErrors = (
|
||||
content: string,
|
||||
packetId: string,
|
||||
expectedChecks: QaWidgetCheck[],
|
||||
): string[] => {
|
||||
const heading = "## Widget kind × viewport checklist";
|
||||
const sectionStart = content.indexOf(heading);
|
||||
if (expectedChecks.length === 0) {
|
||||
if (sectionStart >= 0) return [`${packetId}: non-widget report must not contain a human widget checklist`];
|
||||
return [];
|
||||
}
|
||||
if (sectionStart < 0) return [`${packetId}: human widget checklist is missing`];
|
||||
|
||||
const sectionBodyStart = sectionStart + heading.length;
|
||||
const duplicateSection = content.indexOf(heading, sectionBodyStart);
|
||||
const nextSection = content.indexOf("\n## ", sectionBodyStart);
|
||||
const section = content.slice(sectionBodyStart, nextSection < 0 ? undefined : nextSection);
|
||||
const rows = new Map<string, { status: string; count: number }>();
|
||||
for (const line of section.split("\n")) {
|
||||
const cells = markdownTableCells(line);
|
||||
if (!cells || cells.length !== 9 || cells[0] === "Widget kind" || cells.every((cell) => /^-+$/u.test(cell))) {
|
||||
continue;
|
||||
}
|
||||
const key = `${cells[0]}\u0000${cells[1]}`;
|
||||
const existing = rows.get(key);
|
||||
rows.set(key, { status: cells[8] ?? "", count: (existing?.count ?? 0) + 1 });
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
if (duplicateSection >= 0) errors.push(`${packetId}: human widget checklist is duplicated`);
|
||||
const expectedKeys = new Set(expectedChecks.map(checkKey));
|
||||
for (const check of expectedChecks) {
|
||||
const key = checkKey(check);
|
||||
const row = rows.get(key);
|
||||
if (!row) {
|
||||
errors.push(`${packetId}: human widget row is missing ${check.widgetKind} at ${check.viewport}`);
|
||||
continue;
|
||||
}
|
||||
if (row.count !== 1) {
|
||||
errors.push(`${packetId}: human widget row is duplicated ${check.widgetKind} at ${check.viewport}`);
|
||||
}
|
||||
if (row.status !== check.status) {
|
||||
errors.push(
|
||||
`${packetId}/${check.widgetKind}/${check.viewport}: human widget status ${row.status || "<empty>"} differs from structured status ${check.status}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const key of rows.keys()) {
|
||||
if (expectedKeys.has(key)) continue;
|
||||
const [widgetKind, viewport] = key.split("\u0000");
|
||||
errors.push(`${packetId}: human widget checklist contains unassigned row ${widgetKind}@${viewport}`);
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const rollupQaStatuses = (statuses: QaStatus[]): QaStatus => {
|
||||
if (statuses.includes("failed")) return "failed";
|
||||
if (statuses.includes("blocked")) return "blocked";
|
||||
if (statuses.includes("not-reached")) return "not-reached";
|
||||
return "passed";
|
||||
};
|
||||
|
||||
const hasExecutionResults = (metadata: QaReportMetadata): boolean =>
|
||||
metadata.status !== "not-reached" ||
|
||||
Object.values(metadata.caseStatuses).some((status) => status !== "not-reached") ||
|
||||
metadata.widgetChecks.some((check) => check.status !== "not-reached") ||
|
||||
metadata.findings.length > 0 ||
|
||||
metadata.artifacts.length > 0 ||
|
||||
metadata.performance.measurements.length > 0 ||
|
||||
metadata.performance.limitations.length > 0 ||
|
||||
metadata.independentReproductions.length > 0;
|
||||
|
||||
export const collectReportIntegrityErrors = (metadata: QaReportMetadata, options: ReportIntegrityOptions): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (metadata.packetId !== options.packetId) {
|
||||
errors.push(`${options.packetId}: metadata packetId is ${metadata.packetId}`);
|
||||
}
|
||||
|
||||
const caseRollup = rollupQaStatuses(Object.values(metadata.caseStatuses));
|
||||
if (metadata.status !== caseRollup) {
|
||||
errors.push(`${options.packetId}: report status ${metadata.status} does not match case rollup ${caseRollup}`);
|
||||
}
|
||||
|
||||
if (metadata.execution.candidateSha !== options.campaignCandidateSha) {
|
||||
errors.push(`${options.packetId}: candidateSha must equal the campaign candidate ${options.campaignCandidateSha}`);
|
||||
}
|
||||
|
||||
if (!hasExecutionResults(metadata)) return errors;
|
||||
|
||||
const execution = metadata.execution;
|
||||
const requiredText = {
|
||||
candidateSha: execution.candidateSha,
|
||||
url: execution.url,
|
||||
runtimeProfile: execution.runtimeProfile,
|
||||
persona: execution.persona,
|
||||
sessionId: execution.sessionId,
|
||||
timestamp: execution.timestamp,
|
||||
viewport: execution.viewport,
|
||||
input: execution.input,
|
||||
};
|
||||
for (const [field, value] of Object.entries(requiredText)) {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
errors.push(`${options.packetId}: execution.${field} is required after execution`);
|
||||
}
|
||||
}
|
||||
if (!Number.isInteger(execution.actualPort) || (execution.actualPort ?? 0) <= 0) {
|
||||
errors.push(`${options.packetId}: execution.actualPort must be a positive integer after execution`);
|
||||
}
|
||||
if (!Number.isFinite(execution.zoom) || (execution.zoom ?? 0) <= 0) {
|
||||
errors.push(`${options.packetId}: execution.zoom must be positive after execution`);
|
||||
}
|
||||
if (metadata.artifacts.length === 0) {
|
||||
errors.push(`${options.packetId}: executed report must link at least one artifact`);
|
||||
}
|
||||
|
||||
if (execution.runtimeProfile && !options.profiles.includes(execution.runtimeProfile)) {
|
||||
errors.push(`${options.packetId}: execution.runtimeProfile is outside the packet assignment`);
|
||||
}
|
||||
if (execution.persona && !options.personas.includes(execution.persona)) {
|
||||
errors.push(`${options.packetId}: execution.persona is outside the packet assignment`);
|
||||
}
|
||||
if (execution.viewport && !options.viewports.includes(execution.viewport)) {
|
||||
errors.push(`${options.packetId}: execution.viewport is outside the packet assignment`);
|
||||
}
|
||||
if (execution.input && !options.inputs.includes(execution.input)) {
|
||||
errors.push(`${options.packetId}: execution.input is outside the packet assignment`);
|
||||
}
|
||||
if (execution.zoom && !options.zooms.includes(execution.zoom)) {
|
||||
errors.push(`${options.packetId}: execution.zoom is outside the packet assignment`);
|
||||
}
|
||||
|
||||
const expectedFlags = execution.runtimeProfile ? options.expectedProfileFlags[execution.runtimeProfile] : undefined;
|
||||
if (!expectedFlags || !sameSet(execution.runtimeFlags, expectedFlags)) {
|
||||
errors.push(
|
||||
`${options.packetId}: execution.runtimeFlags do not match profile ${execution.runtimeProfile ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (execution.timestamp && Number.isNaN(Date.parse(execution.timestamp))) {
|
||||
errors.push(`${options.packetId}: execution.timestamp must be ISO-compatible`);
|
||||
}
|
||||
|
||||
if (execution.url) {
|
||||
try {
|
||||
const url = new URL(execution.url);
|
||||
if (!["http:", "https:"].includes(url.protocol)) {
|
||||
errors.push(`${options.packetId}: execution.url must use HTTP(S)`);
|
||||
}
|
||||
const defaultPort = url.protocol === "https:" ? 443 : 80;
|
||||
const urlPort = url.port.length > 0 ? Number(url.port) : defaultPort;
|
||||
if (execution.actualPort !== null && urlPort !== execution.actualPort) {
|
||||
errors.push(`${options.packetId}: execution.url port does not match execution.actualPort`);
|
||||
}
|
||||
} catch {
|
||||
errors.push(`${options.packetId}: execution.url must be an absolute HTTP(S) URL`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const normalizeReportMetadata = (
|
||||
value: unknown,
|
||||
options: NormalizeOptions,
|
||||
): { metadata: QaReportMetadata; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
const root = isRecord(value) ? value : {};
|
||||
if (!isRecord(value)) errors.push(`${options.packetId}: report metadata must be an object`);
|
||||
if (typeof root.packetId !== "string") errors.push(`${options.packetId}: packetId must be a string`);
|
||||
|
||||
const caseStatuses: Record<string, QaStatus> = {};
|
||||
if (!isRecord(root.caseStatuses)) {
|
||||
errors.push(`${options.packetId}: caseStatuses must be an object`);
|
||||
}
|
||||
const rawCaseStatuses = isRecord(root.caseStatuses) ? root.caseStatuses : {};
|
||||
for (const caseId of options.expectedCaseIds) {
|
||||
const rawStatus = rawCaseStatuses[caseId];
|
||||
if (rawStatus === undefined) {
|
||||
errors.push(`${options.packetId}: caseStatuses is missing assigned case ${caseId}`);
|
||||
} else if (!allowedStatuses.has(rawStatus as QaStatus)) {
|
||||
errors.push(`${options.packetId}/${caseId}: invalid case status ${String(rawStatus)}`);
|
||||
}
|
||||
caseStatuses[caseId] = statusOrNotReached(rawStatus);
|
||||
}
|
||||
for (const caseId of Object.keys(rawCaseStatuses)) {
|
||||
if (!options.expectedCaseIds.includes(caseId)) {
|
||||
errors.push(`${options.packetId}: caseStatuses contains unknown case ${caseId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(root.widgetChecks)) {
|
||||
errors.push(`${options.packetId}: widgetChecks must be an array`);
|
||||
}
|
||||
const expectedChecks = new Map(options.expectedWidgetChecks.map((check) => [checkKey(check), check]));
|
||||
const suppliedChecks = new Map<string, QaWidgetCheck>();
|
||||
for (const [index, valueAtIndex] of (Array.isArray(root.widgetChecks) ? root.widgetChecks : []).entries()) {
|
||||
if (!isRecord(valueAtIndex)) {
|
||||
errors.push(`${options.packetId}: widgetChecks[${index}] must be an object`);
|
||||
continue;
|
||||
}
|
||||
const widgetKind = typeof valueAtIndex.widgetKind === "string" ? valueAtIndex.widgetKind : "";
|
||||
const viewport = typeof valueAtIndex.viewport === "string" ? valueAtIndex.viewport : "";
|
||||
const key = checkKey({ widgetKind, viewport });
|
||||
if (!expectedChecks.has(key)) {
|
||||
errors.push(`${options.packetId}: widgetChecks contains unknown row ${widgetKind}@${viewport}`);
|
||||
continue;
|
||||
}
|
||||
if (suppliedChecks.has(key)) {
|
||||
errors.push(`${options.packetId}: widgetChecks contains duplicate row ${widgetKind}@${viewport}`);
|
||||
continue;
|
||||
}
|
||||
if (!allowedStatuses.has(valueAtIndex.status as QaStatus)) {
|
||||
errors.push(`${options.packetId}/${widgetKind}/${viewport}: invalid widget check status`);
|
||||
}
|
||||
if (typeof valueAtIndex.sizeRequirement !== "string") {
|
||||
errors.push(`${options.packetId}/${widgetKind}/${viewport}: sizeRequirement must be a string`);
|
||||
} else if (valueAtIndex.sizeRequirement !== expectedChecks.get(key)?.sizeRequirement) {
|
||||
errors.push(`${options.packetId}/${widgetKind}/${viewport}: sizeRequirement does not match the assignment`);
|
||||
}
|
||||
suppliedChecks.set(key, {
|
||||
widgetKind,
|
||||
viewport,
|
||||
sizeRequirement: typeof valueAtIndex.sizeRequirement === "string" ? valueAtIndex.sizeRequirement : "",
|
||||
status: statusOrNotReached(valueAtIndex.status),
|
||||
});
|
||||
}
|
||||
const widgetChecks = options.expectedWidgetChecks.map((expected) => {
|
||||
const supplied = suppliedChecks.get(checkKey(expected));
|
||||
if (supplied) return supplied;
|
||||
errors.push(
|
||||
`${options.packetId}: widgetChecks is missing assigned row ${expected.widgetKind}@${expected.viewport}`,
|
||||
);
|
||||
return { ...expected, status: "not-reached" as const };
|
||||
});
|
||||
|
||||
if (!Array.isArray(root.findings)) errors.push(`${options.packetId}: findings must be an array`);
|
||||
const findings = (Array.isArray(root.findings) ? root.findings : []).flatMap((finding, index) => {
|
||||
if (!isRecord(finding)) {
|
||||
errors.push(`${options.packetId}: findings[${index}] must be an object`);
|
||||
return [];
|
||||
}
|
||||
if (!["P0", "P1", "P2", "P3"].includes(String(finding.severity))) {
|
||||
errors.push(`${options.packetId}: findings[${index}] has an invalid severity`);
|
||||
}
|
||||
if (typeof finding.title !== "string") {
|
||||
errors.push(`${options.packetId}: findings[${index}].title must be a string`);
|
||||
}
|
||||
if (finding.caseIds !== undefined && !Array.isArray(finding.caseIds)) {
|
||||
errors.push(`${options.packetId}: findings[${index}].caseIds must be an array`);
|
||||
} else if (Array.isArray(finding.caseIds) && finding.caseIds.some((caseId) => typeof caseId !== "string")) {
|
||||
errors.push(`${options.packetId}: findings[${index}].caseIds must contain only strings`);
|
||||
} else if (
|
||||
Array.isArray(finding.caseIds) &&
|
||||
finding.caseIds.some((caseId) => !options.expectedCaseIds.includes(String(caseId)))
|
||||
) {
|
||||
errors.push(`${options.packetId}: findings[${index}].caseIds contains an unknown assigned case`);
|
||||
}
|
||||
if (finding.evidence !== undefined && !Array.isArray(finding.evidence)) {
|
||||
errors.push(`${options.packetId}: findings[${index}].evidence must be an array`);
|
||||
} else if (Array.isArray(finding.evidence) && finding.evidence.some((entry) => typeof entry !== "string")) {
|
||||
errors.push(`${options.packetId}: findings[${index}].evidence must contain only strings`);
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: typeof finding.id === "string" ? finding.id : undefined,
|
||||
fingerprint: typeof finding.fingerprint === "string" ? finding.fingerprint : undefined,
|
||||
severity: severityOrP3(finding.severity),
|
||||
title: typeof finding.title === "string" ? finding.title : "",
|
||||
area: typeof finding.area === "string" ? finding.area : undefined,
|
||||
summary: typeof finding.summary === "string" ? finding.summary : undefined,
|
||||
caseIds: Array.isArray(finding.caseIds) ? strings(finding.caseIds) : undefined,
|
||||
evidence: Array.isArray(finding.evidence) ? strings(finding.evidence) : undefined,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
if (!Array.isArray(root.artifacts)) errors.push(`${options.packetId}: artifacts must be an array`);
|
||||
if (Array.isArray(root.artifacts) && root.artifacts.some((artifact) => typeof artifact !== "string")) {
|
||||
errors.push(`${options.packetId}: artifacts must contain only strings`);
|
||||
}
|
||||
const artifacts = strings(root.artifacts);
|
||||
|
||||
const execution = isRecord(root.execution) ? root.execution : {};
|
||||
if (!isRecord(root.execution)) errors.push(`${options.packetId}: execution metadata must be an object`);
|
||||
if (!Array.isArray(execution.runtimeFlags)) {
|
||||
errors.push(`${options.packetId}: execution.runtimeFlags must be an array`);
|
||||
} else if (execution.runtimeFlags.some((flag) => typeof flag !== "string")) {
|
||||
errors.push(`${options.packetId}: execution.runtimeFlags must contain only strings`);
|
||||
}
|
||||
for (const field of [
|
||||
"candidateSha",
|
||||
"url",
|
||||
"runtimeProfile",
|
||||
"persona",
|
||||
"sessionId",
|
||||
"timestamp",
|
||||
"viewport",
|
||||
"input",
|
||||
]) {
|
||||
const fieldValue = execution[field];
|
||||
if (fieldValue !== undefined && fieldValue !== null && typeof fieldValue !== "string") {
|
||||
errors.push(`${options.packetId}: execution.${field} must be a string or null`);
|
||||
}
|
||||
}
|
||||
for (const field of ["actualPort", "zoom"]) {
|
||||
const fieldValue = execution[field];
|
||||
if (fieldValue !== undefined && fieldValue !== null && typeof fieldValue !== "number") {
|
||||
errors.push(`${options.packetId}: execution.${field} must be a number or null`);
|
||||
}
|
||||
}
|
||||
|
||||
const performance = isRecord(root.performance) ? root.performance : {};
|
||||
if (!isRecord(root.performance)) errors.push(`${options.packetId}: performance must be an object`);
|
||||
if (!Array.isArray(performance.measurements)) {
|
||||
errors.push(`${options.packetId}: performance.measurements must be an array`);
|
||||
}
|
||||
if (!Array.isArray(performance.limitations)) {
|
||||
errors.push(`${options.packetId}: performance.limitations must be an array`);
|
||||
} else if (performance.limitations.some((limitation) => typeof limitation !== "string")) {
|
||||
errors.push(`${options.packetId}: performance.limitations must contain only strings`);
|
||||
}
|
||||
const measurements = (Array.isArray(performance.measurements) ? performance.measurements : []).flatMap(
|
||||
(measurement, index) => {
|
||||
if (!isRecord(measurement)) {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}] must be an object`);
|
||||
return [];
|
||||
}
|
||||
for (const field of ["name", "unit", "threshold"]) {
|
||||
if (typeof measurement[field] !== "string") {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}].${field} must be a string`);
|
||||
}
|
||||
}
|
||||
if (measurement.value !== null && typeof measurement.value !== "number") {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}].value must be a number or null`);
|
||||
}
|
||||
if (!allowedStatuses.has(measurement.status as QaStatus)) {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}].status is invalid`);
|
||||
}
|
||||
if (!Array.isArray(measurement.evidence)) {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}].evidence must be an array`);
|
||||
} else if (measurement.evidence.some((entry) => typeof entry !== "string")) {
|
||||
errors.push(`${options.packetId}: performance.measurements[${index}].evidence must contain only strings`);
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: typeof measurement.name === "string" ? measurement.name : "",
|
||||
value: numberOrNull(measurement.value),
|
||||
unit: typeof measurement.unit === "string" ? measurement.unit : "",
|
||||
threshold: typeof measurement.threshold === "string" ? measurement.threshold : "",
|
||||
status: statusOrNotReached(measurement.status),
|
||||
evidence: strings(measurement.evidence),
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
if (!Array.isArray(root.independentReproductions)) {
|
||||
errors.push(`${options.packetId}: independentReproductions must be an array`);
|
||||
}
|
||||
const independentReproductions = (
|
||||
Array.isArray(root.independentReproductions) ? root.independentReproductions : []
|
||||
).flatMap((reproduction, index) => {
|
||||
if (!isRecord(reproduction)) {
|
||||
errors.push(`${options.packetId}: independentReproductions[${index}] must be an object`);
|
||||
return [];
|
||||
}
|
||||
for (const field of ["findingFingerprint", "agentId", "notes"]) {
|
||||
if (typeof reproduction[field] !== "string") {
|
||||
errors.push(`${options.packetId}: independentReproductions[${index}].${field} must be a string`);
|
||||
}
|
||||
}
|
||||
if (!["reproduced", "not-reproduced", "blocked", "not-reached"].includes(String(reproduction.outcome))) {
|
||||
errors.push(`${options.packetId}: independentReproductions[${index}].outcome is invalid`);
|
||||
}
|
||||
if (!Array.isArray(reproduction.evidence)) {
|
||||
errors.push(`${options.packetId}: independentReproductions[${index}].evidence must be an array`);
|
||||
} else if (reproduction.evidence.some((entry) => typeof entry !== "string")) {
|
||||
errors.push(`${options.packetId}: independentReproductions[${index}].evidence must contain only strings`);
|
||||
}
|
||||
return [
|
||||
{
|
||||
findingFingerprint: typeof reproduction.findingFingerprint === "string" ? reproduction.findingFingerprint : "",
|
||||
agentId: typeof reproduction.agentId === "string" ? reproduction.agentId : "",
|
||||
outcome: typeof reproduction.outcome === "string" ? reproduction.outcome : "not-reached",
|
||||
evidence: strings(reproduction.evidence),
|
||||
notes: typeof reproduction.notes === "string" ? reproduction.notes : "",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
if (!allowedStatuses.has(root.status as QaStatus)) {
|
||||
errors.push(`${options.packetId}: invalid report status ${String(root.status)}`);
|
||||
}
|
||||
if (typeof root.notes !== "string") errors.push(`${options.packetId}: notes must be a string`);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
packetId: typeof root.packetId === "string" ? root.packetId : options.packetId,
|
||||
status: statusOrNotReached(root.status),
|
||||
caseStatuses,
|
||||
execution: {
|
||||
candidateSha: textOrNull(execution.candidateSha),
|
||||
url: textOrNull(execution.url),
|
||||
actualPort: numberOrNull(execution.actualPort),
|
||||
runtimeProfile: textOrNull(execution.runtimeProfile),
|
||||
runtimeFlags: strings(execution.runtimeFlags),
|
||||
persona: textOrNull(execution.persona),
|
||||
sessionId: textOrNull(execution.sessionId),
|
||||
timestamp: textOrNull(execution.timestamp),
|
||||
viewport: textOrNull(execution.viewport),
|
||||
input: textOrNull(execution.input),
|
||||
zoom: numberOrNull(execution.zoom),
|
||||
},
|
||||
findings,
|
||||
artifacts,
|
||||
widgetChecks,
|
||||
performance: {
|
||||
measurements,
|
||||
limitations: strings(performance.limitations),
|
||||
},
|
||||
independentReproductions,
|
||||
notes: typeof root.notes === "string" ? root.notes : "",
|
||||
},
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
export const collectCriticalCoverageGaps = (
|
||||
reports: {
|
||||
packetId: string;
|
||||
caseStatuses: Record<string, QaStatus>;
|
||||
widgetChecks: QaWidgetCheck[];
|
||||
metadataErrors?: string[];
|
||||
}[],
|
||||
): CoverageGap[] =>
|
||||
reports.flatMap((report) => {
|
||||
const caseGaps = Object.entries(report.caseStatuses)
|
||||
.filter(([, status]) => status !== "passed")
|
||||
.map(([caseId, status]) => ({
|
||||
packetId: report.packetId,
|
||||
kind: "case" as const,
|
||||
itemId: caseId,
|
||||
status,
|
||||
}));
|
||||
const widgetGaps = report.widgetChecks
|
||||
.filter((check) => check.status !== "passed")
|
||||
.map((check) => ({
|
||||
packetId: report.packetId,
|
||||
kind: "widget" as const,
|
||||
itemId: `${check.widgetKind}@${check.viewport}`,
|
||||
status: check.status,
|
||||
}));
|
||||
const metadataGaps = (report.metadataErrors ?? []).map((detail, index) => ({
|
||||
packetId: report.packetId,
|
||||
kind: "metadata" as const,
|
||||
itemId: `metadata-${index + 1}`,
|
||||
status: "blocked" as const,
|
||||
detail,
|
||||
}));
|
||||
return [...caseGaps, ...widgetGaps, ...metadataGaps];
|
||||
});
|
||||
|
||||
export const releaseDecision = (severityTotals: { P0: number; P1: number }, gaps: CoverageGap[]): "GO" | "NO-GO" => {
|
||||
if (severityTotals.P0 > 0 || severityTotals.P1 > 0 || gaps.length > 0) return "NO-GO";
|
||||
return "GO";
|
||||
};
|
||||
@@ -0,0 +1,281 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
aggregateQaFindings,
|
||||
collectCriticalCoverageGaps,
|
||||
collectHumanWidgetStatusErrors,
|
||||
collectReportIntegrityErrors,
|
||||
normalizeReportMetadata,
|
||||
releaseDecision,
|
||||
} from "./report-integrity.mts";
|
||||
import type { QaReportMetadata, QaWidgetCheck, ReportIntegrityOptions } from "./report-integrity.mts";
|
||||
|
||||
const assignedWidgetCheck: QaWidgetCheck = {
|
||||
widgetKind: "weather",
|
||||
viewport: "320x568",
|
||||
sizeRequirement: "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds",
|
||||
status: "not-reached",
|
||||
};
|
||||
const campaignCandidateSha = "f57a660088d6777c86aca22977354fa8b810e2be";
|
||||
const integrityOptions: ReportIntegrityOptions = {
|
||||
campaignCandidateSha,
|
||||
packetId: "preflight-01",
|
||||
profiles: ["main-writable"],
|
||||
personas: ["avery-admin"],
|
||||
viewports: ["1280x900"],
|
||||
zooms: [100],
|
||||
inputs: ["mouse"],
|
||||
expectedProfileFlags: {
|
||||
"main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
},
|
||||
};
|
||||
const validMetadata = (): QaReportMetadata => ({
|
||||
packetId: "preflight-01",
|
||||
status: "passed",
|
||||
caseStatuses: { "PF01-FIXTURE-STATE": "passed" },
|
||||
execution: {
|
||||
candidateSha: campaignCandidateSha,
|
||||
url: "http://127.0.0.1:34401",
|
||||
actualPort: 34401,
|
||||
runtimeProfile: "main-writable",
|
||||
runtimeFlags: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
persona: "avery-admin",
|
||||
sessionId: "qa-v2-main-avery-admin",
|
||||
timestamp: "2026-09-01T00:00:00.000Z",
|
||||
viewport: "1280x900",
|
||||
input: "mouse",
|
||||
zoom: 100,
|
||||
},
|
||||
findings: [],
|
||||
artifacts: ["/tmp/qa-artifact.png"],
|
||||
widgetChecks: [],
|
||||
performance: { measurements: [], limitations: [] },
|
||||
independentReproductions: [],
|
||||
notes: "Complete.",
|
||||
});
|
||||
|
||||
describe("release-v2 report integrity", () => {
|
||||
it("keeps the highest severity while merging duplicate finding evidence and reproductions", () => {
|
||||
const fingerprint = "same-defect";
|
||||
const findings = aggregateQaFindings([
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
findings: [
|
||||
{
|
||||
fingerprint,
|
||||
severity: "P3",
|
||||
title: "Initial low-severity title",
|
||||
caseIds: ["W01-RENDER-STATE"],
|
||||
evidence: ["/tmp/first.png"],
|
||||
},
|
||||
],
|
||||
independentReproductions: [
|
||||
{
|
||||
findingFingerprint: fingerprint,
|
||||
agentId: "reproducer-01",
|
||||
outcome: "reproduced",
|
||||
evidence: ["/tmp/repro-first.png"],
|
||||
notes: "First replay.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
packetId: "widgets-02",
|
||||
findings: [
|
||||
{
|
||||
fingerprint,
|
||||
severity: "P1",
|
||||
title: "Confirmed release blocker",
|
||||
caseIds: ["W02-RENDER-STATE"],
|
||||
evidence: ["/tmp/first.png", "/tmp/second.png"],
|
||||
},
|
||||
],
|
||||
independentReproductions: [],
|
||||
},
|
||||
{
|
||||
packetId: "repro-widgets-02",
|
||||
findings: [],
|
||||
independentReproductions: [
|
||||
{
|
||||
findingFingerprint: fingerprint,
|
||||
agentId: "reproducer-01",
|
||||
outcome: "reproduced",
|
||||
evidence: ["/tmp/repro-second.png"],
|
||||
notes: "Second replay.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(findings).toHaveLength(1);
|
||||
expect(findings[0]).toMatchObject({
|
||||
severity: "P1",
|
||||
title: "Confirmed release blocker",
|
||||
packets: ["widgets-01", "widgets-02"],
|
||||
caseIds: ["W01-RENDER-STATE", "W02-RENDER-STATE"],
|
||||
evidence: ["/tmp/first.png", "/tmp/second.png"],
|
||||
});
|
||||
expect(findings[0]?.reproductions).toEqual([
|
||||
{
|
||||
findingFingerprint: fingerprint,
|
||||
agentId: "reproducer-01",
|
||||
outcome: "reproduced",
|
||||
evidence: ["/tmp/repro-first.png", "/tmp/repro-second.png"],
|
||||
notes: "First replay.; Second replay.",
|
||||
},
|
||||
]);
|
||||
const severityTotals = { P0: 0, P1: findings.filter((finding) => finding.severity === "P1").length };
|
||||
expect(releaseDecision(severityTotals, [])).toBe("NO-GO");
|
||||
});
|
||||
|
||||
it("requires each human widget status to exactly match the structured status", () => {
|
||||
const passedCheck = { ...assignedWidgetCheck, status: "passed" as const };
|
||||
const content = `## Widget kind × viewport checklist
|
||||
|
||||
| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| weather | 320x568 | ${assignedWidgetCheck.sizeRequirement} | healthy | owner | reload | recovered | evidence | not-reached |
|
||||
|
||||
## Findings`;
|
||||
|
||||
expect(collectHumanWidgetStatusErrors(content, "widgets-01", [passedCheck])).toEqual([
|
||||
"widgets-01/weather/320x568: human widget status not-reached differs from structured status passed",
|
||||
]);
|
||||
expect(
|
||||
collectHumanWidgetStatusErrors(content.replace("| not-reached |", "| passed |"), "widgets-01", [passedCheck]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the release NO-GO when cases pass but an assigned widget check is not reached", () => {
|
||||
const gaps = collectCriticalCoverageGaps([
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
caseStatuses: { "W01-RENDER-STATE": "passed" },
|
||||
widgetChecks: [assignedWidgetCheck],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(gaps).toEqual([
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
kind: "widget",
|
||||
itemId: "weather@320x568",
|
||||
status: "not-reached",
|
||||
},
|
||||
]);
|
||||
expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO");
|
||||
});
|
||||
|
||||
it("reports null and malformed metadata without throwing", () => {
|
||||
const normalize = () =>
|
||||
normalizeReportMetadata(
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
status: "passed",
|
||||
caseStatuses: null,
|
||||
execution: { candidateSha: {}, runtimeFlags: null },
|
||||
findings: null,
|
||||
artifacts: null,
|
||||
widgetChecks: null,
|
||||
performance: null,
|
||||
independentReproductions: null,
|
||||
},
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
expectedCaseIds: ["W01-RENDER-STATE"],
|
||||
expectedWidgetChecks: [assignedWidgetCheck],
|
||||
},
|
||||
);
|
||||
|
||||
expect(normalize).not.toThrow();
|
||||
const result = normalize();
|
||||
expect(result.errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
"widgets-01: caseStatuses must be an object",
|
||||
"widgets-01: findings must be an array",
|
||||
"widgets-01: widgetChecks must be an array",
|
||||
"widgets-01: execution.runtimeFlags must be an array",
|
||||
"widgets-01: execution.candidateSha must be a string or null",
|
||||
]),
|
||||
);
|
||||
expect(result.metadata.caseStatuses).toEqual({ "W01-RENDER-STATE": "not-reached" });
|
||||
expect(result.metadata.findings).toEqual([]);
|
||||
expect(result.metadata.widgetChecks).toEqual([assignedWidgetCheck]);
|
||||
|
||||
const gaps = collectCriticalCoverageGaps([
|
||||
{
|
||||
packetId: "widgets-01",
|
||||
caseStatuses: result.metadata.caseStatuses,
|
||||
widgetChecks: result.metadata.widgetChecks,
|
||||
metadataErrors: result.errors,
|
||||
},
|
||||
]);
|
||||
expect(gaps.some((gap) => gap.kind === "metadata")).toBe(true);
|
||||
expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO");
|
||||
});
|
||||
|
||||
it("turns a null metadata root into deterministic validation errors", () => {
|
||||
const result = normalizeReportMetadata(null, {
|
||||
packetId: "widgets-01",
|
||||
expectedCaseIds: ["W01-RENDER-STATE"],
|
||||
expectedWidgetChecks: [assignedWidgetCheck],
|
||||
});
|
||||
|
||||
expect(result.errors[0]).toBe("widgets-01: report metadata must be an object");
|
||||
expect(result.metadata.caseStatuses["W01-RENDER-STATE"]).toBe("not-reached");
|
||||
expect(result.metadata.widgetChecks[0]?.status).toBe("not-reached");
|
||||
});
|
||||
|
||||
it("forces a metadata gap and NO-GO for an executed report from the wrong candidate", () => {
|
||||
const metadata = validMetadata();
|
||||
metadata.execution.candidateSha = "0000000000000000000000000000000000000000";
|
||||
const metadataErrors = collectReportIntegrityErrors(metadata, integrityOptions);
|
||||
const gaps = collectCriticalCoverageGaps([
|
||||
{ packetId: metadata.packetId, caseStatuses: metadata.caseStatuses, widgetChecks: [], metadataErrors },
|
||||
]);
|
||||
|
||||
expect(metadataErrors).toContain(
|
||||
`preflight-01: candidateSha must equal the campaign candidate ${campaignCandidateSha}`,
|
||||
);
|
||||
expect(gaps.some((gap) => gap.kind === "metadata")).toBe(true);
|
||||
expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO");
|
||||
});
|
||||
|
||||
it("forces a metadata gap and NO-GO when packet status disagrees with passed cases", () => {
|
||||
const metadata = validMetadata();
|
||||
metadata.status = "failed";
|
||||
const metadataErrors = collectReportIntegrityErrors(metadata, integrityOptions);
|
||||
const gaps = collectCriticalCoverageGaps([
|
||||
{ packetId: metadata.packetId, caseStatuses: metadata.caseStatuses, widgetChecks: [], metadataErrors },
|
||||
]);
|
||||
|
||||
expect(metadataErrors).toContain("preflight-01: report status failed does not match case rollup passed");
|
||||
expect(releaseDecision({ P0: 0, P1: 0 }, gaps)).toBe("NO-GO");
|
||||
});
|
||||
|
||||
it("reports missing execution metadata after any assigned case was reached", () => {
|
||||
const metadata = validMetadata();
|
||||
metadata.execution.url = null;
|
||||
metadata.execution.actualPort = null;
|
||||
|
||||
expect(collectReportIntegrityErrors(metadata, integrityOptions)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"preflight-01: execution.url is required after execution",
|
||||
"preflight-01: execution.actualPort must be a positive integer after execution",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves valid structured report metadata unchanged", () => {
|
||||
const input = validMetadata();
|
||||
const normalized = normalizeReportMetadata(input, {
|
||||
packetId: "preflight-01",
|
||||
expectedCaseIds: ["PF01-FIXTURE-STATE"],
|
||||
expectedWidgetChecks: [],
|
||||
});
|
||||
|
||||
expect(normalized.errors).toEqual([]);
|
||||
expect(normalized.metadata).toEqual(input);
|
||||
expect(collectReportIntegrityErrors(normalized.metadata, integrityOptions)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, open, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { assertSafeContainedPath, isPathWithin } from "./safety.mts";
|
||||
|
||||
const errorCode = (error: unknown): string | undefined => (error as NodeJS.ErrnoException).code;
|
||||
|
||||
export const assertSafeReportPath = async (qaRoot: string, target: string, label: string): Promise<string> =>
|
||||
assertSafeContainedPath(qaRoot, target, label);
|
||||
|
||||
export const readSafeReportFile = async (qaRoot: string, target: string, label: string): Promise<string> => {
|
||||
await assertSafeReportPath(qaRoot, target, label);
|
||||
const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
try {
|
||||
return await handle.readFile("utf8");
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
|
||||
export const writeSafeReportFile = async (
|
||||
qaRoot: string,
|
||||
target: string,
|
||||
content: string,
|
||||
label: string,
|
||||
): Promise<void> => {
|
||||
await assertSafeReportPath(qaRoot, target, label);
|
||||
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW;
|
||||
const handle = await open(target, flags, 0o666);
|
||||
try {
|
||||
await handle.writeFile(content, "utf8");
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
|
||||
interface ArtifactDirectoryMessages {
|
||||
absolutePathRequirement: string;
|
||||
directoryName: string;
|
||||
}
|
||||
|
||||
const packetArtifactDirectoryMessages: ArtifactDirectoryMessages = {
|
||||
absolutePathRequirement: "the packet's absolute artifact directory",
|
||||
directoryName: "packet artifact directory",
|
||||
};
|
||||
|
||||
const reproductionArtifactDirectoryMessages: ArtifactDirectoryMessages = {
|
||||
absolutePathRequirement: "the absolute release-v2 artifact directory",
|
||||
directoryName: "release-v2 artifact directory",
|
||||
};
|
||||
|
||||
const validateResolvedArtifactPathWithin = async (
|
||||
expectedDirectory: string,
|
||||
artifactPath: string,
|
||||
label: string,
|
||||
messages: ArtifactDirectoryMessages,
|
||||
): Promise<string | null> => {
|
||||
if (!path.isAbsolute(artifactPath) || !isPathWithin(expectedDirectory, artifactPath)) {
|
||||
return `${label} must use ${messages.absolutePathRequirement}`;
|
||||
}
|
||||
|
||||
let resolvedExpectedDirectory: string;
|
||||
try {
|
||||
resolvedExpectedDirectory = await realpath(expectedDirectory);
|
||||
} catch (error) {
|
||||
return `${label} ${messages.directoryName} does not resolve (${errorCode(error) ?? "unknown error"})`;
|
||||
}
|
||||
if (resolvedExpectedDirectory !== path.resolve(expectedDirectory)) {
|
||||
return `${label} ${messages.directoryName} must not contain symlinks`;
|
||||
}
|
||||
|
||||
let resolvedArtifact: string;
|
||||
try {
|
||||
resolvedArtifact = await realpath(artifactPath);
|
||||
} catch (error) {
|
||||
return `${label} does not resolve (${errorCode(error) ?? "unknown error"})`;
|
||||
}
|
||||
if (!isPathWithin(resolvedExpectedDirectory, resolvedArtifact)) {
|
||||
return `${label} resolves outside the ${messages.directoryName}`;
|
||||
}
|
||||
try {
|
||||
await assertSafeContainedPath(resolvedExpectedDirectory, artifactPath, label);
|
||||
const artifactStats = await lstat(artifactPath);
|
||||
if (!artifactStats.isFile()) return `${label} must resolve to an existing regular file`;
|
||||
} catch (error) {
|
||||
return `${label} is not a safe regular file (${error instanceof Error ? error.message : "unknown error"})`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateResolvedArtifactPath = async (
|
||||
expectedPacketDirectory: string,
|
||||
artifactPath: string,
|
||||
label: string,
|
||||
): Promise<string | null> =>
|
||||
validateResolvedArtifactPathWithin(expectedPacketDirectory, artifactPath, label, packetArtifactDirectoryMessages);
|
||||
|
||||
export const validateResolvedReproductionEvidencePath = async (
|
||||
releaseArtifactDirectory: string,
|
||||
artifactPath: string,
|
||||
label: string,
|
||||
): Promise<string | null> =>
|
||||
validateResolvedArtifactPathWithin(
|
||||
releaseArtifactDirectory,
|
||||
artifactPath,
|
||||
label,
|
||||
reproductionArtifactDirectoryMessages,
|
||||
);
|
||||
@@ -0,0 +1,198 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { createServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
assertSafeReportPath,
|
||||
readSafeReportFile,
|
||||
validateResolvedArtifactPath,
|
||||
validateResolvedReproductionEvidencePath,
|
||||
writeSafeReportFile,
|
||||
} from "./report-path-integrity.mts";
|
||||
|
||||
const temporaryPaths: string[] = [];
|
||||
|
||||
const temporaryDirectory = async (suffix: string): Promise<string> => {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), `homarr-release-v2-qa-${suffix}-`));
|
||||
temporaryPaths.push(directory);
|
||||
return directory;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryPaths.splice(0).map((target) => rm(target, { force: true, recursive: true })));
|
||||
});
|
||||
|
||||
describe("release-v2 QA report path integrity", () => {
|
||||
it.each(["reports/preflight-01/report.md", "ledger.json", "master-report.md"])(
|
||||
"rejects a symlinked aggregate input or output path: %s",
|
||||
async (relativePath) => {
|
||||
const qaRoot = await temporaryDirectory("report-path");
|
||||
const target = path.join(qaRoot, "outside-target");
|
||||
const linkedPath = path.join(qaRoot, relativePath);
|
||||
await mkdir(path.dirname(linkedPath), { recursive: true });
|
||||
await writeFile(target, "outside");
|
||||
await symlink(target, linkedPath);
|
||||
|
||||
await expect(assertSafeReportPath(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink");
|
||||
await expect(readSafeReportFile(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink");
|
||||
await expect(writeSafeReportFile(qaRoot, linkedPath, "replacement", "QA report path")).rejects.toThrow("symlink");
|
||||
await expect(readFile(target, "utf8")).resolves.toBe("outside");
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an artifact symlink that ultimately escapes its packet directory", async () => {
|
||||
const root = await temporaryDirectory("artifact-escape");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const packetDirectory = path.join(releaseArtifactDirectory, "preflight-01");
|
||||
const outsideArtifact = path.join(root, "outside.png");
|
||||
const linkedArtifact = path.join(packetDirectory, "escape.png");
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
await writeFile(outsideArtifact, "outside");
|
||||
await symlink(outsideArtifact, linkedArtifact);
|
||||
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"),
|
||||
).resolves.toContain("resolves outside");
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
linkedArtifact,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.toContain("resolves outside");
|
||||
});
|
||||
|
||||
it("accepts a resolved regular artifact inside the packet directory", async () => {
|
||||
const root = await temporaryDirectory("artifact-valid");
|
||||
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
|
||||
const artifact = path.join(packetDirectory, "valid.png");
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
await writeFile(artifact, "valid");
|
||||
|
||||
await expect(validateResolvedArtifactPath(packetDirectory, artifact, "preflight-01: artifact")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("accepts independent reproduction evidence from another release-v2 agent directory", async () => {
|
||||
const root = await temporaryDirectory("reproduction-artifact-valid");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const packetDirectory = path.join(releaseArtifactDirectory, "board-03");
|
||||
const reproductionDirectory = path.join(releaseArtifactDirectory, "repro-board03-remove-358d-b");
|
||||
const artifact = path.join(reproductionDirectory, "remove-confirmation.png");
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
await mkdir(reproductionDirectory, { recursive: true });
|
||||
await writeFile(artifact, "valid");
|
||||
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(releaseArtifactDirectory, artifact, "board-03: reproduction evidence"),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, artifact, "board-03: finding evidence"),
|
||||
).resolves.toContain("packet's absolute artifact directory");
|
||||
});
|
||||
|
||||
it("rejects an artifact directory and an in-directory artifact symlink", async () => {
|
||||
const root = await temporaryDirectory("artifact-not-file");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const packetDirectory = path.join(releaseArtifactDirectory, "preflight-01");
|
||||
const artifactDirectory = path.join(packetDirectory, "looks-like-evidence.png");
|
||||
const realArtifact = path.join(packetDirectory, "real.png");
|
||||
const linkedArtifact = path.join(packetDirectory, "linked.png");
|
||||
await mkdir(artifactDirectory, { recursive: true });
|
||||
await writeFile(realArtifact, "valid");
|
||||
await symlink(realArtifact, linkedArtifact);
|
||||
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, artifactDirectory, "preflight-01: artifact"),
|
||||
).resolves.toContain("regular file");
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"),
|
||||
).resolves.toContain("symlink");
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
artifactDirectory,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.toContain("regular file");
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
linkedArtifact,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.toContain("symlink");
|
||||
});
|
||||
|
||||
it("rejects relative, escaping, and missing independent reproduction evidence paths", async () => {
|
||||
const root = await temporaryDirectory("reproduction-artifact-unsafe");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const reproductionDirectory = path.join(releaseArtifactDirectory, "reproducer-01");
|
||||
const outsideArtifact = path.join(root, "outside.png");
|
||||
const missingArtifact = path.join(reproductionDirectory, "missing.png");
|
||||
await mkdir(reproductionDirectory, { recursive: true });
|
||||
await writeFile(outsideArtifact, "outside");
|
||||
|
||||
for (const artifact of ["relative.png", outsideArtifact, missingArtifact]) {
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
artifact,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")("rejects a FIFO artifact", async () => {
|
||||
const root = await temporaryDirectory("artifact-fifo");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const packetDirectory = path.join(releaseArtifactDirectory, "preflight-01");
|
||||
const fifoArtifact = path.join(packetDirectory, "capture.pipe");
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
const result = spawnSync("mkfifo", [fifoArtifact]);
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, fifoArtifact, "preflight-01: artifact"),
|
||||
).resolves.toContain("regular file");
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
fifoArtifact,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.toContain("regular file");
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")("rejects a socket artifact", async () => {
|
||||
const root = await temporaryDirectory("artifact-socket");
|
||||
const releaseArtifactDirectory = path.join(root, ".screenshots", "release-v2");
|
||||
const packetDirectory = path.join(releaseArtifactDirectory, "preflight-01");
|
||||
const socketArtifact = path.join(packetDirectory, "capture.sock");
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
const server = createServer();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketArtifact, resolve);
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
validateResolvedArtifactPath(packetDirectory, socketArtifact, "preflight-01: artifact"),
|
||||
).resolves.toContain("regular file");
|
||||
await expect(
|
||||
validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
socketArtifact,
|
||||
"preflight-01: reproduction evidence",
|
||||
),
|
||||
).resolves.toContain("regular file");
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,821 @@
|
||||
import { mkdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import {
|
||||
aggregateQaFindings,
|
||||
collectCriticalCoverageGaps,
|
||||
collectHumanWidgetStatusErrors,
|
||||
collectReportIntegrityErrors,
|
||||
normalizeReportMetadata,
|
||||
releaseDecision,
|
||||
} from "./report-integrity.mts";
|
||||
import {
|
||||
assertSafeReportPath,
|
||||
readSafeReportFile,
|
||||
validateResolvedArtifactPath,
|
||||
validateResolvedReproductionEvidencePath,
|
||||
writeSafeReportFile,
|
||||
} from "./report-path-integrity.mts";
|
||||
import { resolveCheckoutCandidateSha } from "./provenance.mts";
|
||||
import { createRuntimeExecutionContract } from "./runtime.mts";
|
||||
|
||||
type Status = "passed" | "failed" | "blocked" | "not-reached";
|
||||
type Severity = "P0" | "P1" | "P2" | "P3";
|
||||
|
||||
interface Packet {
|
||||
id: string;
|
||||
wave: string;
|
||||
title: string;
|
||||
prRefs: number[];
|
||||
personas: string[];
|
||||
boards: string[];
|
||||
profiles: string[];
|
||||
viewports: string[];
|
||||
zooms: number[];
|
||||
inputs: string[];
|
||||
widgetKinds?: string[];
|
||||
cases: string[];
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
pullRequests: { number: number; label: string }[];
|
||||
caseDimensions: Record<string, { id: string; expected: string }[]>;
|
||||
packets: Packet[];
|
||||
}
|
||||
|
||||
interface Finding {
|
||||
id?: string;
|
||||
fingerprint?: string;
|
||||
severity: Severity;
|
||||
title: string;
|
||||
area?: string;
|
||||
summary?: string;
|
||||
caseIds?: string[];
|
||||
evidence?: string[];
|
||||
}
|
||||
|
||||
interface ReportMetadata {
|
||||
packetId: string;
|
||||
status: Status;
|
||||
caseStatuses: Record<string, Status>;
|
||||
execution: {
|
||||
candidateSha: string | null;
|
||||
url: string | null;
|
||||
actualPort: number | null;
|
||||
runtimeProfile: string | null;
|
||||
runtimeFlags: string[];
|
||||
persona: string | null;
|
||||
sessionId: string | null;
|
||||
timestamp: string | null;
|
||||
viewport: string | null;
|
||||
input: string | null;
|
||||
zoom: number | null;
|
||||
};
|
||||
findings: Finding[];
|
||||
artifacts: string[];
|
||||
widgetChecks: WidgetCheck[];
|
||||
performance: {
|
||||
measurements: PerformanceMeasurement[];
|
||||
limitations: string[];
|
||||
};
|
||||
independentReproductions: IndependentReproduction[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface WidgetCheck {
|
||||
widgetKind: string;
|
||||
viewport: string;
|
||||
sizeRequirement: string;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
interface PerformanceMeasurement {
|
||||
name: string;
|
||||
value: number | null;
|
||||
unit: string;
|
||||
threshold: string;
|
||||
status: Status;
|
||||
evidence: string[];
|
||||
}
|
||||
|
||||
interface IndependentReproduction {
|
||||
findingFingerprint: string;
|
||||
agentId: string;
|
||||
outcome: "reproduced" | "not-reproduced" | "blocked" | "not-reached";
|
||||
evidence: string[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface PlaceholderRefreshOptions<T> {
|
||||
read: (entry: T) => Promise<string>;
|
||||
isSafePlaceholder: (content: string, entry: T) => boolean;
|
||||
render: (entry: T) => string;
|
||||
write: (entry: T, content: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface PlaceholderRefreshCounts {
|
||||
refreshed: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
interface ReportSnapshot {
|
||||
content: string;
|
||||
mtimeMs: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface ResolvedBlockerRolloverOptions<T, TSnapshot extends { content: string }> {
|
||||
read: (entry: T) => Promise<TSnapshot>;
|
||||
isEligible: (content: string, entry: T) => boolean;
|
||||
isUnchanged: (entry: T, snapshot: TSnapshot) => Promise<boolean>;
|
||||
render: (entry: T) => string;
|
||||
write: (entry: T, content: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ResolvedBlockerRolloverCounts {
|
||||
reset: number;
|
||||
skipped: number;
|
||||
concurrentlyChanged: number;
|
||||
}
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const qaRoot = join(repoRoot, "qa/release-v2");
|
||||
const reportsRoot = join(qaRoot, "reports");
|
||||
const campaignCandidateSha = resolveCheckoutCandidateSha(repoRoot);
|
||||
const runtimeExecutionContract = createRuntimeExecutionContract();
|
||||
export const hostRuntimeLimitation =
|
||||
`The host's fs.inotify.max_user_instances=128 is too low for concurrent development watchers. ` +
|
||||
`QA apps use ${runtimeExecutionContract.runtimeMode} from a single candidate-pinned build produced by the ${runtimeExecutionContract.bundler} bundler. ` +
|
||||
"Source changes require a new committed candidate build before browser evidence is valid.";
|
||||
const expectedProfileFlags: Record<string, string[]> = {
|
||||
"main-writable": ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"main-readonly": ["DEMO_MODE=true", "DEMO_READ_ONLY=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
"onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
};
|
||||
const exhaustiveSizeWidgetKinds = new Set([
|
||||
"downloads",
|
||||
"calendar",
|
||||
"mediaServer",
|
||||
"mediaRequests-requestList",
|
||||
"mediaRequests-requestStats",
|
||||
"systemResources",
|
||||
"systemDisks",
|
||||
"dockerContainers",
|
||||
"networkControllerSummary",
|
||||
"networkControllerStatus",
|
||||
"beszelSystemStats",
|
||||
"uptimeKuma",
|
||||
"bookmarks",
|
||||
"notebook",
|
||||
"iframe",
|
||||
"customApi",
|
||||
"assistant",
|
||||
]);
|
||||
const manifest = JSON.parse(await readFile(join(qaRoot, "coverage-manifest.json"), "utf8")) as Manifest;
|
||||
const init = process.argv.includes("--init");
|
||||
const refresh = process.argv.includes("--refresh-placeholders");
|
||||
const rolloverFlag = "--rollover-resolved-blocker";
|
||||
const rolloverFlagIndex = process.argv.indexOf(rolloverFlag);
|
||||
const rolloverResolvedBlocker = rolloverFlagIndex === -1 ? null : (process.argv[rolloverFlagIndex + 1] ?? null);
|
||||
|
||||
if (rolloverFlagIndex !== -1 && (!rolloverResolvedBlocker || rolloverResolvedBlocker.startsWith("--"))) {
|
||||
throw new Error(`${rolloverFlag} requires a finding fingerprint`);
|
||||
}
|
||||
if ([init, refresh, rolloverResolvedBlocker !== null].filter(Boolean).length > 1) {
|
||||
throw new Error("Use only one of --init, --refresh-placeholders, or --rollover-resolved-blocker");
|
||||
}
|
||||
|
||||
const escapeCell = (value: string): string => value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
||||
const list = (values: (string | number)[]): string => (values.length === 0 ? "None" : values.join(", "));
|
||||
const isBlockedOrNotReached = (status: Status): boolean => status === "blocked" || status === "not-reached";
|
||||
|
||||
const expandedCases = (packet: Packet): { id: string; expected: string }[] => {
|
||||
const dimensions = manifest.caseDimensions[packet.wave] ?? [];
|
||||
return packet.cases.flatMap((baseId) =>
|
||||
dimensions.map((dimension) => ({ id: `${baseId}-${dimension.id}`, expected: dimension.expected })),
|
||||
);
|
||||
};
|
||||
|
||||
const sizeRequirement = (widgetKind: string) => {
|
||||
if (exhaustiveSizeWidgetKinds.has(widgetKind)) {
|
||||
return "every width 1-24 × every height 1-6";
|
||||
}
|
||||
return "minimum / canonical / wide / tall / maximum / overflow / behavior-changing thresholds";
|
||||
};
|
||||
|
||||
const widgetChecks = (packet: Packet): WidgetCheck[] =>
|
||||
(packet.widgetKinds ?? []).flatMap((widgetKind) =>
|
||||
packet.viewports.map((viewport) => ({
|
||||
widgetKind,
|
||||
viewport,
|
||||
sizeRequirement: sizeRequirement(widgetKind),
|
||||
status: "not-reached",
|
||||
})),
|
||||
);
|
||||
|
||||
const reportMetadata = (packet: Packet): ReportMetadata => ({
|
||||
packetId: packet.id,
|
||||
status: "not-reached",
|
||||
caseStatuses: Object.fromEntries(expandedCases(packet).map(({ id }) => [id, "not-reached" as const])),
|
||||
execution: {
|
||||
candidateSha: campaignCandidateSha,
|
||||
url: null,
|
||||
actualPort: null,
|
||||
runtimeProfile: null,
|
||||
runtimeFlags: [],
|
||||
persona: null,
|
||||
sessionId: null,
|
||||
timestamp: null,
|
||||
viewport: null,
|
||||
input: null,
|
||||
zoom: null,
|
||||
},
|
||||
findings: [],
|
||||
artifacts: [],
|
||||
widgetChecks: widgetChecks(packet),
|
||||
performance: { measurements: [], limitations: [] },
|
||||
independentReproductions: [],
|
||||
notes: "Not executed.",
|
||||
});
|
||||
|
||||
const renderPacketReportBase = (packet: Packet): string => {
|
||||
const artifactDirectory = resolve(repoRoot, ".screenshots/release-v2", packet.id);
|
||||
const metadata = reportMetadata(packet);
|
||||
const cases = expandedCases(packet)
|
||||
.map(
|
||||
({ id, expected }) =>
|
||||
`| ${id} | not-reached | [planned screenshot](${artifactDirectory}/${id}.png) | Not executed | ${expected} |`,
|
||||
)
|
||||
.join("\n");
|
||||
const widgetMatrix = metadata.widgetChecks
|
||||
.map(
|
||||
(check) =>
|
||||
`| ${check.widgetKind} | ${check.viewport} | ${check.sizeRequirement} | loading / populated / empty / error | Owner / Editor / Viewer / read-only | mutate / save / reload / reset | fail / retry / healthy | [planned artifact](${artifactDirectory}/${check.widgetKind}-${check.viewport}.png) | ${check.status} |`,
|
||||
)
|
||||
.join("\n");
|
||||
const widgetSection =
|
||||
widgetMatrix.length === 0
|
||||
? ""
|
||||
: `\n## Widget kind × viewport checklist\n\n| Widget kind | Viewport | Size | States | Permission/read-only | Options/persistence | Recovery | Evidence | Status |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n${widgetMatrix}\n`;
|
||||
|
||||
return `<!-- release-v2-qa-report\n${JSON.stringify(metadata, null, 2)}\n-->\n\n# ${packet.id}: ${packet.title}\n\n| Metadata | Value |\n| --- | --- |\n| Status | not-reached |\n| Wave | ${packet.wave} |\n| PR refs | ${packet.prRefs.map((number) => `#${number}`).join(", ")} |\n| Personas | ${list(packet.personas)} |\n| Boards | ${list(packet.boards)} |\n| Profiles | ${list(packet.profiles)} |\n| Chromium viewports | ${list(packet.viewports)} |\n| Zoom | ${list(packet.zooms.map((zoom) => `${zoom}%`))} |\n| Input | ${list(packet.inputs)} |\n| Widget kinds | ${list(packet.widgetKinds ?? [])} |\n| Artifact directory | [${artifactDirectory}](${artifactDirectory}) |\n| Candidate SHA | not-recorded |\n| URL / actual port | not-recorded |\n| Runtime profile / flags | not-recorded |\n| Executed persona | not-recorded |\n| Browser session ID | not-recorded |\n| Timestamp | not-recorded |\n| Executed viewport / input / zoom | not-recorded |\n\n## Dogfood evidence\n\n| Case ID | Status | Evidence | Observed | Expected |\n| --- | --- | --- | --- | --- |\n${cases}\n${widgetSection}\n## Findings\n\nNo findings recorded. Add structured findings to the metadata block and mirror them here.\n\n## Blockers and gaps\n\nNot executed.\n\n## Cleanup\n\nRecord created resources, cleanup outcome, console errors, and network failures. Never include secret values.\n`;
|
||||
};
|
||||
|
||||
export const renderPacketReport = (packet: Packet): string =>
|
||||
renderPacketReportBase(packet)
|
||||
.replace("| Candidate SHA | not-recorded |", `| Candidate SHA | ${campaignCandidateSha} |`)
|
||||
.replace(
|
||||
"\n## Findings",
|
||||
"\n## Performance measurements and limitations\n\nRecord structured measurements in `performance.measurements` with units, thresholds, status, and evidence. Record every environmental or tooling limitation in `performance.limitations`; leave measurements empty instead of inventing a result.\n\n## Independent reproductions\n\nRecord a second agent's result in `independentReproductions` with the finding fingerprint, agent ID, outcome, evidence, and notes. An unattempted reproduction remains `not-reached`.\n\n## Findings",
|
||||
);
|
||||
|
||||
const parseMetadata = (content: string, packet: Packet): { metadata: ReportMetadata; errors: string[] } => {
|
||||
const metadata = content.match(/<!-- release-v2-qa-report\n([\s\S]*?)\n-->/)?.[1];
|
||||
let rawMetadata: unknown = null;
|
||||
const parseErrors: string[] = [];
|
||||
if (!metadata) {
|
||||
parseErrors.push(`${packet.id}: report metadata block is missing`);
|
||||
} else {
|
||||
try {
|
||||
rawMetadata = JSON.parse(metadata) as unknown;
|
||||
} catch (error) {
|
||||
parseErrors.push(`${packet.id}: report metadata is invalid JSON (${String(error)})`);
|
||||
}
|
||||
}
|
||||
const normalized = normalizeReportMetadata(rawMetadata, {
|
||||
packetId: packet.id,
|
||||
expectedCaseIds: expandedCases(packet).map(({ id }) => id),
|
||||
expectedWidgetChecks: widgetChecks(packet),
|
||||
});
|
||||
return {
|
||||
metadata: normalized.metadata as ReportMetadata,
|
||||
errors: [...parseErrors, ...normalized.errors],
|
||||
};
|
||||
};
|
||||
|
||||
export const reportMetadataHasEvidence = (metadata: ReportMetadata): boolean => {
|
||||
const execution = metadata.execution;
|
||||
const hasExecutionMetadata =
|
||||
execution.url !== null ||
|
||||
execution.actualPort !== null ||
|
||||
execution.runtimeProfile !== null ||
|
||||
execution.runtimeFlags.length > 0 ||
|
||||
execution.persona !== null ||
|
||||
execution.sessionId !== null ||
|
||||
execution.timestamp !== null ||
|
||||
execution.viewport !== null ||
|
||||
execution.input !== null ||
|
||||
execution.zoom !== null;
|
||||
|
||||
return (
|
||||
metadata.status !== "not-reached" ||
|
||||
Object.values(metadata.caseStatuses).some((status) => status !== "not-reached") ||
|
||||
metadata.widgetChecks.some((check) => check.status !== "not-reached") ||
|
||||
metadata.findings.length > 0 ||
|
||||
metadata.artifacts.length > 0 ||
|
||||
metadata.performance.measurements.length > 0 ||
|
||||
metadata.performance.limitations.length > 0 ||
|
||||
metadata.independentReproductions.length > 0 ||
|
||||
hasExecutionMetadata
|
||||
);
|
||||
};
|
||||
|
||||
export const reportIsOnlyBlockedBy = (metadata: ReportMetadata, findingFingerprint: string): boolean => {
|
||||
if (!isBlockedOrNotReached(metadata.status)) return false;
|
||||
if (!Object.values(metadata.caseStatuses).every(isBlockedOrNotReached)) return false;
|
||||
if (!metadata.widgetChecks.every((check) => isBlockedOrNotReached(check.status))) return false;
|
||||
if (metadata.performance.measurements.length > 0) return false;
|
||||
if (metadata.findings.length === 0) return false;
|
||||
if (metadata.findings.some((finding) => finding.fingerprint !== findingFingerprint)) return false;
|
||||
if (metadata.independentReproductions.some((reproduction) => reproduction.findingFingerprint !== findingFingerprint))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const reportContentIsOnlyBlockedBy = (content: string, packet: Packet, findingFingerprint: string): boolean => {
|
||||
const parsed = parseMetadata(content, packet);
|
||||
return parsed.errors.length === 0 && reportIsOnlyBlockedBy(parsed.metadata, findingFingerprint);
|
||||
};
|
||||
|
||||
export const rolloverResolvedBlockerReports = async <T, TSnapshot extends { content: string }>(
|
||||
entries: T[],
|
||||
options: ResolvedBlockerRolloverOptions<T, TSnapshot>,
|
||||
): Promise<ResolvedBlockerRolloverCounts> => {
|
||||
const counts: ResolvedBlockerRolloverCounts = { reset: 0, skipped: 0, concurrentlyChanged: 0 };
|
||||
for (const entry of entries) {
|
||||
let snapshot: TSnapshot;
|
||||
try {
|
||||
snapshot = await options.read(entry);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
counts.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.isEligible(snapshot.content, entry)) {
|
||||
counts.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
if (!(await options.isUnchanged(entry, snapshot))) {
|
||||
counts.concurrentlyChanged += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await options.write(entry, options.render(entry));
|
||||
counts.reset += 1;
|
||||
}
|
||||
return counts;
|
||||
};
|
||||
|
||||
export const refreshPlaceholderReports = async <T,>(
|
||||
entries: T[],
|
||||
options: PlaceholderRefreshOptions<T>,
|
||||
): Promise<PlaceholderRefreshCounts> => {
|
||||
const counts: PlaceholderRefreshCounts = { refreshed: 0, skipped: 0 };
|
||||
for (const entry of entries) {
|
||||
let existingContent: string | null = null;
|
||||
try {
|
||||
existingContent = await options.read(entry);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
|
||||
if (existingContent !== null && !options.isSafePlaceholder(existingContent, entry)) {
|
||||
counts.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await options.write(entry, options.render(entry));
|
||||
counts.refreshed += 1;
|
||||
}
|
||||
return counts;
|
||||
};
|
||||
|
||||
const collectHumanReportErrors = (content: string, packet: Packet, metadata: ReportMetadata): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!content.includes(`| Status | ${metadata.status} |`)) {
|
||||
errors.push(`${packet.id}: human metadata status differs from structured metadata`);
|
||||
}
|
||||
if (!content.includes(`| Candidate SHA | ${campaignCandidateSha} |`)) {
|
||||
errors.push(`${packet.id}: human metadata does not show the campaign candidate`);
|
||||
}
|
||||
for (const [caseId, status] of Object.entries(metadata.caseStatuses)) {
|
||||
if (!content.includes(`| ${caseId} | ${status} |`)) {
|
||||
errors.push(`${packet.id}/${caseId}: human evidence row/status is missing`);
|
||||
}
|
||||
}
|
||||
errors.push(...collectHumanWidgetStatusErrors(content, packet.id, metadata.widgetChecks));
|
||||
|
||||
const hasExecution =
|
||||
metadata.status !== "not-reached" ||
|
||||
Object.values(metadata.caseStatuses).some((status) => status !== "not-reached") ||
|
||||
metadata.widgetChecks.some((check) => check.status !== "not-reached") ||
|
||||
metadata.findings.length > 0 ||
|
||||
metadata.artifacts.length > 0;
|
||||
if (!hasExecution) return errors;
|
||||
|
||||
const requiredValues = [
|
||||
metadata.execution.candidateSha,
|
||||
metadata.execution.url,
|
||||
metadata.execution.actualPort,
|
||||
metadata.execution.runtimeProfile,
|
||||
metadata.execution.persona,
|
||||
metadata.execution.sessionId,
|
||||
metadata.execution.timestamp,
|
||||
metadata.execution.viewport,
|
||||
metadata.execution.input,
|
||||
metadata.execution.zoom,
|
||||
];
|
||||
for (const value of requiredValues) {
|
||||
if (value !== null && !content.includes(String(value))) {
|
||||
errors.push(`${packet.id}: human metadata does not include execution value ${String(value)}`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const collectArtifactErrors = async (packet: Packet, metadata: ReportMetadata): Promise<string[]> => {
|
||||
const releaseArtifactDirectory = resolve(repoRoot, ".screenshots/release-v2");
|
||||
const packetArtifactDirectory = resolve(releaseArtifactDirectory, packet.id);
|
||||
const packetArtifacts = [
|
||||
...metadata.artifacts.map((artifact) => ({ artifact, label: `${packet.id}: artifact` })),
|
||||
...metadata.findings.flatMap((finding, findingIndex) =>
|
||||
(finding.evidence ?? []).map((artifact) => ({
|
||||
artifact,
|
||||
label: `${packet.id}: findings[${findingIndex}] evidence`,
|
||||
})),
|
||||
),
|
||||
...metadata.performance.measurements.flatMap((measurement, measurementIndex) =>
|
||||
measurement.evidence.map((artifact) => ({
|
||||
artifact,
|
||||
label: `${packet.id}: performance.measurements[${measurementIndex}] evidence`,
|
||||
})),
|
||||
),
|
||||
];
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const { artifact, label } of packetArtifacts) {
|
||||
const error = await validateResolvedArtifactPath(packetArtifactDirectory, artifact, label);
|
||||
if (error) errors.push(error);
|
||||
}
|
||||
for (const [reproductionIndex, reproduction] of metadata.independentReproductions.entries()) {
|
||||
for (const artifact of reproduction.evidence) {
|
||||
const error = await validateResolvedReproductionEvidencePath(
|
||||
releaseArtifactDirectory,
|
||||
artifact,
|
||||
`${packet.id}: independentReproductions[${reproductionIndex}] evidence`,
|
||||
);
|
||||
if (error) errors.push(error);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const ensureReports = async (): Promise<void> => {
|
||||
if (refresh || rolloverResolvedBlocker) {
|
||||
for (const packet of manifest.packets) {
|
||||
const packetDirectory = join(reportsRoot, packet.id);
|
||||
const reportPath = join(packetDirectory, "report.md");
|
||||
await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`);
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rolloverResolvedBlocker) {
|
||||
const reportSnapshot = async (packet: Packet): Promise<ReportSnapshot> => {
|
||||
const reportPath = join(reportsRoot, packet.id, "report.md");
|
||||
const content = await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`);
|
||||
const metadata = await stat(reportPath);
|
||||
return { content, mtimeMs: metadata.mtimeMs, size: metadata.size };
|
||||
};
|
||||
const counts = await rolloverResolvedBlockerReports(manifest.packets, {
|
||||
read: reportSnapshot,
|
||||
isEligible: (content, packet) => reportContentIsOnlyBlockedBy(content, packet, rolloverResolvedBlocker),
|
||||
isUnchanged: async (packet, snapshot) => {
|
||||
const current = await reportSnapshot(packet);
|
||||
return (
|
||||
current.content === snapshot.content && current.mtimeMs === snapshot.mtimeMs && current.size === snapshot.size
|
||||
);
|
||||
},
|
||||
render: renderPacketReport,
|
||||
write: async (packet, content) => {
|
||||
const reportPath = join(reportsRoot, packet.id, "report.md");
|
||||
await writeSafeReportFile(qaRoot, reportPath, content, `${packet.id} report path`);
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`release-v2 QA resolved-blocker rollover (${rolloverResolvedBlocker}): ${counts.reset} reset; ${counts.skipped} protected report(s) skipped; ${counts.concurrentlyChanged} concurrently changed report(s) skipped`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
const counts = await refreshPlaceholderReports(manifest.packets, {
|
||||
read: async (packet) => {
|
||||
const reportPath = join(reportsRoot, packet.id, "report.md");
|
||||
return readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`);
|
||||
},
|
||||
isSafePlaceholder: (content, packet) => {
|
||||
const parsed = parseMetadata(content, packet);
|
||||
return parsed.errors.length === 0 && !reportMetadataHasEvidence(parsed.metadata);
|
||||
},
|
||||
render: renderPacketReport,
|
||||
write: async (packet, content) => {
|
||||
const reportPath = join(reportsRoot, packet.id, "report.md");
|
||||
await writeSafeReportFile(qaRoot, reportPath, content, `${packet.id} report path`);
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`release-v2 QA placeholder refresh: ${counts.refreshed} refreshed; ${counts.skipped} protected report(s) skipped`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const packet of manifest.packets) {
|
||||
const packetDirectory = join(reportsRoot, packet.id);
|
||||
const reportPath = join(packetDirectory, "report.md");
|
||||
await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`);
|
||||
await mkdir(packetDirectory, { recursive: true });
|
||||
await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`);
|
||||
try {
|
||||
await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
await writeSafeReportFile(qaRoot, reportPath, renderPacketReport(packet), `${packet.id} report path`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface CoverageAssignment {
|
||||
label: string;
|
||||
detail?: string;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
const rollupStatuses = (statuses: Status[]): Status => {
|
||||
if (statuses.includes("failed")) return "failed";
|
||||
if (statuses.includes("blocked")) return "blocked";
|
||||
if (statuses.includes("not-reached")) return "not-reached";
|
||||
return "passed";
|
||||
};
|
||||
|
||||
const matchingCaseStatus = (metadata: ReportMetadata, fragments: string[]) => {
|
||||
const statuses = Object.entries(metadata.caseStatuses)
|
||||
.filter(([caseId]) => fragments.some((fragment) => caseId.includes(fragment)))
|
||||
.map(([, status]) => status);
|
||||
if (statuses.length === 0) return metadata.status;
|
||||
return rollupStatuses(statuses);
|
||||
};
|
||||
|
||||
const coverageTable = (assignments: CoverageAssignment[], labelHeading: string) => {
|
||||
const grouped = new Map<string, { detail: string; statuses: Status[] }>();
|
||||
for (const assignment of assignments) {
|
||||
const existing = grouped.get(assignment.label);
|
||||
if (existing) {
|
||||
existing.statuses.push(assignment.status);
|
||||
continue;
|
||||
}
|
||||
grouped.set(assignment.label, { detail: assignment.detail ?? "—", statuses: [assignment.status] });
|
||||
}
|
||||
if (grouped.size === 0)
|
||||
return `| ${labelHeading} | Detail | Passed | Failed | Blocked | Not reached | Total |\n| --- | --- | ---: | ---: | ---: | ---: | ---: |\n| — | Not recorded | 0 | 0 | 0 | 0 | 0 |`;
|
||||
const rows = [...grouped.entries()]
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([label, value]) => {
|
||||
const count = (status: Status) => value.statuses.filter((current) => current === status).length;
|
||||
return `| ${escapeCell(label)} | ${escapeCell(value.detail)} | ${count("passed")} | ${count("failed")} | ${count("blocked")} | ${count("not-reached")} | ${value.statuses.length} |`;
|
||||
});
|
||||
return `| ${labelHeading} | Detail | Passed | Failed | Blocked | Not reached | Total |\n| --- | --- | ---: | ---: | ---: | ---: | ---: |\n${rows.join("\n")}`;
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
if (init || refresh || rolloverResolvedBlocker) await ensureReports();
|
||||
|
||||
const reports: { packet: Packet; metadata: ReportMetadata; metadataErrors: string[] }[] = [];
|
||||
for (const packet of manifest.packets) {
|
||||
const reportPath = join(reportsRoot, packet.id, "report.md");
|
||||
await assertSafeReportPath(qaRoot, reportPath, `${packet.id} report path`);
|
||||
let content = "";
|
||||
const readErrors: string[] = [];
|
||||
try {
|
||||
content = await readSafeReportFile(qaRoot, reportPath, `${packet.id} report path`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
readErrors.push(`${packet.id}: report.md is missing`);
|
||||
}
|
||||
const parsed = parseMetadata(content, packet);
|
||||
const integrityErrors = collectReportIntegrityErrors(parsed.metadata, {
|
||||
campaignCandidateSha,
|
||||
packetId: packet.id,
|
||||
profiles: packet.profiles,
|
||||
personas: packet.personas,
|
||||
viewports: packet.viewports,
|
||||
zooms: packet.zooms,
|
||||
inputs: packet.inputs,
|
||||
expectedProfileFlags,
|
||||
});
|
||||
const humanReportErrors = collectHumanReportErrors(content, packet, parsed.metadata);
|
||||
const artifactErrors = await collectArtifactErrors(packet, parsed.metadata);
|
||||
reports.push({
|
||||
packet,
|
||||
metadata: parsed.metadata,
|
||||
metadataErrors: [...readErrors, ...parsed.errors, ...integrityErrors, ...humanReportErrors, ...artifactErrors],
|
||||
});
|
||||
}
|
||||
|
||||
const packetTotals: Record<Status, number> = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 };
|
||||
const caseTotals: Record<Status, number> = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 };
|
||||
for (const { metadata } of reports) {
|
||||
packetTotals[metadata.status] += 1;
|
||||
for (const status of Object.values(metadata.caseStatuses)) caseTotals[status] += 1;
|
||||
}
|
||||
|
||||
const findings = aggregateQaFindings(
|
||||
reports.map(({ packet, metadata }) => ({
|
||||
packetId: packet.id,
|
||||
findings: metadata.findings,
|
||||
independentReproductions: metadata.independentReproductions,
|
||||
})),
|
||||
);
|
||||
const severityTotals: Record<Severity, number> = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
||||
for (const finding of findings) severityTotals[finding.severity] += 1;
|
||||
|
||||
const criticalGaps = collectCriticalCoverageGaps(
|
||||
reports.map(({ packet, metadata, metadataErrors }) => ({
|
||||
packetId: packet.id,
|
||||
caseStatuses: metadata.caseStatuses,
|
||||
widgetChecks: metadata.widgetChecks,
|
||||
metadataErrors,
|
||||
})),
|
||||
);
|
||||
const decision = releaseDecision(severityTotals, criticalGaps);
|
||||
const widgetTotals: Record<Status, number> = { passed: 0, failed: 0, blocked: 0, "not-reached": 0 };
|
||||
for (const { metadata } of reports) {
|
||||
for (const check of metadata.widgetChecks) widgetTotals[check.status] += 1;
|
||||
}
|
||||
|
||||
const ledger = {
|
||||
schemaVersion: 1,
|
||||
updatedAt: new Date().toISOString(),
|
||||
packetStatuses: Object.fromEntries(
|
||||
reports.map(({ packet, metadata }) => [packet.id, { status: metadata.status, cases: metadata.caseStatuses }]),
|
||||
),
|
||||
};
|
||||
const ledgerPath = join(qaRoot, "ledger.json");
|
||||
const masterReportPath = join(qaRoot, "master-report.md");
|
||||
await assertSafeReportPath(qaRoot, ledgerPath, "release-v2 QA ledger path");
|
||||
await assertSafeReportPath(qaRoot, masterReportPath, "release-v2 QA master report path");
|
||||
await writeSafeReportFile(qaRoot, ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "release-v2 QA ledger path");
|
||||
|
||||
const waveRows = Object.entries(Object.groupBy(reports, ({ packet }) => packet.wave)).map(([wave, entries]) => {
|
||||
const waveReports = entries ?? [];
|
||||
const passed = waveReports.filter(({ metadata }) => metadata.status === "passed").length;
|
||||
return `| ${wave} | ${passed} | ${waveReports.length} |`;
|
||||
});
|
||||
const findingRows =
|
||||
findings.length === 0
|
||||
? "| — | — | No findings recorded | — | — |"
|
||||
: findings
|
||||
.map(
|
||||
(finding) =>
|
||||
`| ${finding.severity} | ${escapeCell(finding.area ?? "general")} | ${escapeCell(finding.title)} | ${list(finding.packets)} | ${list(finding.caseIds)} |`,
|
||||
)
|
||||
.join("\n");
|
||||
const gapRows =
|
||||
criticalGaps.length === 0
|
||||
? "| — | — | — | — |"
|
||||
: criticalGaps
|
||||
.map(
|
||||
(gap) =>
|
||||
`| ${gap.packetId} | ${gap.kind} | ${escapeCell(gap.itemId)} | ${gap.status}${gap.detail ? ` — ${escapeCell(gap.detail)}` : ""} |`,
|
||||
)
|
||||
.join("\n");
|
||||
const prRows = manifest.pullRequests
|
||||
.map(
|
||||
(pullRequest) =>
|
||||
`| #${pullRequest.number} | ${pullRequest.label} | ${manifest.packets.filter((packet) => packet.prRefs.includes(pullRequest.number)).length} |`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const prCoverage = coverageTable(
|
||||
manifest.pullRequests.flatMap((pullRequest) =>
|
||||
reports
|
||||
.filter(({ packet }) => packet.prRefs.includes(pullRequest.number))
|
||||
.map(({ metadata }) => ({
|
||||
label: `#${pullRequest.number}`,
|
||||
detail: pullRequest.label,
|
||||
status: metadata.status,
|
||||
})),
|
||||
),
|
||||
"PR",
|
||||
);
|
||||
const featureCoverage = coverageTable(
|
||||
reports.map(({ packet, metadata }) => ({ label: packet.id, detail: packet.title, status: metadata.status })),
|
||||
"Feature / agent",
|
||||
);
|
||||
const widgetAssignments = reports.flatMap(({ packet, metadata }) =>
|
||||
(metadata.widgetChecks ?? widgetChecks(packet)).map((check) => ({
|
||||
label: check.widgetKind,
|
||||
detail: packet.id,
|
||||
status: check.status,
|
||||
})),
|
||||
);
|
||||
const widgetCoverage = coverageTable(widgetAssignments, "Widget kind");
|
||||
const sizeCoverage = coverageTable(
|
||||
reports.flatMap(({ packet, metadata }) =>
|
||||
(metadata.widgetChecks ?? widgetChecks(packet)).map((check) => ({
|
||||
label: check.widgetKind,
|
||||
detail: check.sizeRequirement,
|
||||
status: check.status,
|
||||
})),
|
||||
),
|
||||
"Widget size threshold",
|
||||
);
|
||||
const viewportCoverage = coverageTable(
|
||||
reports.flatMap(({ packet, metadata }) =>
|
||||
packet.viewports.map((viewport) => ({ label: viewport, detail: packet.id, status: metadata.status })),
|
||||
),
|
||||
"Viewport",
|
||||
);
|
||||
const personaCoverage = coverageTable(
|
||||
reports.flatMap(({ packet, metadata }) =>
|
||||
packet.personas.map((persona) => ({ label: persona, detail: packet.id, status: metadata.status })),
|
||||
),
|
||||
"Persona",
|
||||
);
|
||||
const permissionCoverage = coverageTable(
|
||||
reports.flatMap(({ packet, metadata }) => {
|
||||
const status = matchingCaseStatus(metadata, ["ACCESS", "PERMISSION", "READONLY"]);
|
||||
return [
|
||||
...packet.personas.map((persona) => ({ label: persona, detail: packet.id, status })),
|
||||
...packet.profiles.map((profile) => ({ label: `profile:${profile}`, detail: packet.id, status })),
|
||||
];
|
||||
}),
|
||||
"Permission boundary",
|
||||
);
|
||||
const mutationCoverage = coverageTable(
|
||||
reports.map(({ packet, metadata }) => ({
|
||||
label: packet.id,
|
||||
detail: packet.title,
|
||||
status: matchingCaseStatus(metadata, ["MUTATION", "PERSISTENCE", "OPTIONS"]),
|
||||
})),
|
||||
"Mutation area",
|
||||
);
|
||||
const stateCoverage = coverageTable(
|
||||
reports.map(({ packet, metadata }) => ({
|
||||
label: packet.id,
|
||||
detail: packet.title,
|
||||
status: matchingCaseStatus(metadata, ["STATE", "DEGRADED", "RECOVERY", "FAILURE", "EVIDENCE"]),
|
||||
})),
|
||||
"State / recovery area",
|
||||
);
|
||||
const performanceMeasurements = reports.flatMap(({ packet, metadata }) =>
|
||||
(metadata.performance?.measurements ?? []).map((measurement) => ({ packet, measurement })),
|
||||
);
|
||||
const performanceRows =
|
||||
performanceMeasurements.length === 0
|
||||
? "| — | No measurement recorded | — | — | not-reached | — |"
|
||||
: performanceMeasurements
|
||||
.map(({ packet, measurement }) => {
|
||||
const value = measurement.value === null ? "not-recorded" : `${measurement.value} ${measurement.unit}`;
|
||||
return `| ${packet.id} | ${escapeCell(measurement.name)} | ${escapeCell(value)} | ${escapeCell(measurement.threshold)} | ${measurement.status} | ${list(measurement.evidence)} |`;
|
||||
})
|
||||
.join("\n");
|
||||
const limitationRows = [
|
||||
`| host-runtime | ${escapeCell(hostRuntimeLimitation)} |`,
|
||||
...reports.flatMap(({ packet, metadata }) =>
|
||||
(metadata.performance?.limitations ?? []).map((limitation) => `| ${packet.id} | ${escapeCell(limitation)} |`),
|
||||
),
|
||||
];
|
||||
const reproductionRows = findings.flatMap((finding) =>
|
||||
finding.reproductions.map(
|
||||
(reproduction) =>
|
||||
`| ${list(finding.packets)} | ${escapeCell(reproduction.findingFingerprint)} | ${escapeCell(reproduction.agentId)} | ${reproduction.outcome} | ${list(reproduction.evidence)} | ${escapeCell(reproduction.notes)} |`,
|
||||
),
|
||||
);
|
||||
|
||||
const coverageAppendix = `## Coverage by required axis\n\n### Pull request\n\n${prCoverage}\n\n### Feature\n\n${featureCoverage}\n\n### Widget\n\n${widgetCoverage}\n\n### Size threshold\n\nHigh-risk widgets require every width 1–24 × every height 1–6 at all assigned mobile, breakpoint-edge, and desktop viewports. Other widgets require minimum, canonical, wide, tall, maximum, overflow, and behavior-changing threshold checks.\n\n${sizeCoverage}\n\n### Viewport\n\n${viewportCoverage}\n\n### Persona\n\n${personaCoverage}\n\n### Permission\n\n${permissionCoverage}\n\n### Mutation\n\n${mutationCoverage}\n\n### State and recovery\n\n${stateCoverage}\n\n## Performance measurements\n\n| Agent | Measurement | Value | Threshold | Status | Evidence |\n| --- | --- | --- | --- | --- | --- |\n${performanceRows}\n\n### Performance limitations\n\n| Agent | Limitation |\n| --- | --- |\n${limitationRows.length > 0 ? limitationRows.join("\n") : "| — | Not recorded |"}\n\n## Independent reproduction results\n\n| Agent | Finding fingerprint | Reproducing agent | Outcome | Evidence | Notes |\n| --- | --- | --- | --- | --- | --- |\n${reproductionRows.length > 0 ? reproductionRows.join("\n") : "| — | — | — | not-reached | — | Not recorded |"}\n\n`;
|
||||
|
||||
const master = `# Release-v2 QA master report\n\nGenerated: ${new Date().toISOString()}\n\n## Decision: ${decision}\n\nGO requires zero P0/P1 findings and zero critical gaps across assigned cases, widget checks, and report metadata. Current critical gaps: **${criticalGaps.length}**.\n\n## Coverage\n\n| Measure | Passed | Failed | Blocked | Not reached | Total |\n| --- | ---: | ---: | ---: | ---: | ---: |\n| Packets | ${packetTotals.passed} | ${packetTotals.failed} | ${packetTotals.blocked} | ${packetTotals["not-reached"]} | ${reports.length} |\n| Cases | ${caseTotals.passed} | ${caseTotals.failed} | ${caseTotals.blocked} | ${caseTotals["not-reached"]} | ${Object.values(caseTotals).reduce((sum, count) => sum + count, 0)} |\n| Widget checks | ${widgetTotals.passed} | ${widgetTotals.failed} | ${widgetTotals.blocked} | ${widgetTotals["not-reached"]} | ${Object.values(widgetTotals).reduce((sum, count) => sum + count, 0)} |\n\n| Wave | Passed | Total |\n| --- | ---: | ---: |\n${waveRows.join("\n")}\n\n## Severity totals\n\n| P0 | P1 | P2 | P3 |\n| ---: | ---: | ---: | ---: |\n| ${severityTotals.P0} | ${severityTotals.P1} | ${severityTotals.P2} | ${severityTotals.P3} |\n\nDeduplicated by explicit fingerprint, or by severity + area + title when no fingerprint is supplied.\n\n| Severity | Area | Finding | Packets | Cases |\n| --- | --- | --- | --- | --- |\n${findingRows}\n\n## Critical gaps\n\n| Packet | Kind | Coverage item | Status / detail |\n| --- | --- | --- | --- |\n${gapRows}\n\n## PR coverage\n\n| PR | Area | Packets |\n| --- | --- | ---: |\n${prRows}\n\n## Packet reports\n\n${reports.map(({ packet, metadata }) => `- [${packet.id}: ${packet.title}](packets/${packet.id}/report.md) — ${metadata.status}`).join("\n")}\n`;
|
||||
const enrichedMaster = master
|
||||
.replace("## Severity totals", `${coverageAppendix}## Severity totals`)
|
||||
.replaceAll("(packets/", "(reports/")
|
||||
.replace("## Packet reports", "## Agent reports");
|
||||
await writeSafeReportFile(qaRoot, masterReportPath, enrichedMaster, "release-v2 QA master report path");
|
||||
|
||||
console.log(
|
||||
`release-v2 QA report: ${decision}; ${reports.length} packets; ${findings.length} deduplicated findings; ${criticalGaps.length} critical gaps`,
|
||||
);
|
||||
const metadataErrors = reports.flatMap((report) => report.metadataErrors);
|
||||
if (metadataErrors.length > 0) {
|
||||
console.error(`release-v2 QA report found ${metadataErrors.length} metadata error(s):`);
|
||||
for (const error of metadataErrors) console.error(`- ${error}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const entrypoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : null;
|
||||
if (entrypoint === import.meta.url) await main();
|
||||
@@ -0,0 +1,281 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveCheckoutCandidateSha } from "./provenance.mts";
|
||||
import {
|
||||
hostRuntimeLimitation,
|
||||
refreshPlaceholderReports,
|
||||
renderPacketReport,
|
||||
reportContentIsOnlyBlockedBy,
|
||||
reportMetadataHasEvidence,
|
||||
rolloverResolvedBlockerReports,
|
||||
} from "./report.mts";
|
||||
import { createRuntimeExecutionContract } from "./runtime.mts";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const candidateSha = resolveCheckoutCandidateSha(repoRoot);
|
||||
const blockerFingerprint = "release-v2-auth-locale-redirect-loop";
|
||||
|
||||
describe("release-v2 QA report runtime limitation", () => {
|
||||
it("describes the shared standalone runtime contract", () => {
|
||||
const runtimeExecutionContract = createRuntimeExecutionContract();
|
||||
|
||||
expect(hostRuntimeLimitation).toContain(`the ${runtimeExecutionContract.bundler} bundler`);
|
||||
expect(hostRuntimeLimitation).toContain(runtimeExecutionContract.runtimeMode);
|
||||
expect(hostRuntimeLimitation).toContain("single candidate-pinned build");
|
||||
expect(hostRuntimeLimitation).not.toContain("Watchpack polling");
|
||||
expect(hostRuntimeLimitation).not.toContain("Turbopack");
|
||||
});
|
||||
});
|
||||
|
||||
interface TestPacket {
|
||||
id: string;
|
||||
wave: string;
|
||||
title: string;
|
||||
prRefs: number[];
|
||||
personas: string[];
|
||||
boards: string[];
|
||||
profiles: string[];
|
||||
viewports: string[];
|
||||
zooms: number[];
|
||||
inputs: string[];
|
||||
widgetKinds?: string[];
|
||||
cases: string[];
|
||||
}
|
||||
|
||||
const coverageManifest = JSON.parse(
|
||||
await readFile(path.join(repoRoot, "qa/release-v2/coverage-manifest.json"), "utf8"),
|
||||
) as { packets: TestPacket[] };
|
||||
const packet = (packetId: string): TestPacket => {
|
||||
const result = coverageManifest.packets.find((entry) => entry.id === packetId);
|
||||
if (!result) throw new Error(`Missing test packet ${packetId}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
const blockedByResolvedFinding = (testPacket: TestPacket): string => {
|
||||
const placeholder = renderPacketReport(testPacket);
|
||||
const rawMetadata = placeholder.match(/<!-- release-v2-qa-report\n([\s\S]*?)\n-->/)?.[1];
|
||||
if (!rawMetadata) throw new Error(`Missing generated metadata for ${testPacket.id}`);
|
||||
const metadata = JSON.parse(rawMetadata) as Record<string, unknown> & {
|
||||
status: string;
|
||||
caseStatuses: Record<string, string>;
|
||||
execution: Record<string, unknown>;
|
||||
findings: unknown[];
|
||||
artifacts: string[];
|
||||
widgetChecks: { status: string }[];
|
||||
performance: { measurements: unknown[]; limitations: string[] };
|
||||
independentReproductions: unknown[];
|
||||
};
|
||||
metadata.status = "blocked";
|
||||
metadata.caseStatuses = Object.fromEntries(Object.keys(metadata.caseStatuses).map((caseId) => [caseId, "blocked"]));
|
||||
metadata.widgetChecks = metadata.widgetChecks.map((check) => ({ ...check, status: "blocked" }));
|
||||
metadata.execution = {
|
||||
...metadata.execution,
|
||||
url: "http://127.0.0.1:34401",
|
||||
actualPort: 34401,
|
||||
runtimeProfile: "main-writable",
|
||||
runtimeFlags: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
|
||||
persona: testPacket.personas[0] ?? "Avery Admin",
|
||||
sessionId: `qa-v2-${testPacket.id}`,
|
||||
timestamp: "2026-08-31T15:00:00.000Z",
|
||||
viewport: testPacket.viewports[0] ?? "desktop-1440",
|
||||
input: testPacket.inputs[0] ?? "mouse",
|
||||
zoom: testPacket.zooms[0] ?? 100,
|
||||
};
|
||||
metadata.findings = [
|
||||
{
|
||||
fingerprint: blockerFingerprint,
|
||||
severity: "P1",
|
||||
title: "Locale rewrite redirect loop",
|
||||
evidence: [`/tmp/${testPacket.id}/auth-loop.png`],
|
||||
},
|
||||
];
|
||||
metadata.artifacts = [`/tmp/${testPacket.id}/auth-loop.png`];
|
||||
metadata.performance.limitations = ["The application never rendered because of the resolved auth blocker."];
|
||||
metadata.independentReproductions = [
|
||||
{
|
||||
findingFingerprint: blockerFingerprint,
|
||||
agentId: "preflight-01",
|
||||
outcome: "reproduced",
|
||||
evidence: [],
|
||||
notes: "Auth-only reproduction.",
|
||||
},
|
||||
];
|
||||
return placeholder.replace(rawMetadata, JSON.stringify(metadata, null, 2));
|
||||
};
|
||||
|
||||
const untouchedMetadata = () => ({
|
||||
packetId: "preflight-01",
|
||||
status: "not-reached" as const,
|
||||
caseStatuses: { "PF01-FIXTURE-STATE": "not-reached" as const },
|
||||
execution: {
|
||||
candidateSha: "f57a660088d6777c86aca22977354fa8b810e2be",
|
||||
url: null,
|
||||
actualPort: null,
|
||||
runtimeProfile: null,
|
||||
runtimeFlags: [],
|
||||
persona: null,
|
||||
sessionId: null,
|
||||
timestamp: null,
|
||||
viewport: null,
|
||||
input: null,
|
||||
zoom: null,
|
||||
},
|
||||
findings: [],
|
||||
artifacts: [],
|
||||
widgetChecks: [],
|
||||
performance: { measurements: [], limitations: [] },
|
||||
independentReproductions: [],
|
||||
notes: "Not executed.",
|
||||
});
|
||||
|
||||
describe("release-v2 QA report placeholder refresh", () => {
|
||||
it("preserves an executed report before refreshing a later untouched placeholder", async () => {
|
||||
const reports = new Map([
|
||||
["preflight-01", "executed report evidence"],
|
||||
["board-01", "untouched placeholder from the previous candidate"],
|
||||
]);
|
||||
|
||||
const counts = await refreshPlaceholderReports(["preflight-01", "board-01"], {
|
||||
read: async (packetId) => reports.get(packetId) ?? "",
|
||||
isSafePlaceholder: (content) => content.startsWith("untouched placeholder"),
|
||||
render: (packetId) => `refreshed placeholder for ${packetId}`,
|
||||
write: async (packetId, content) => {
|
||||
reports.set(packetId, content);
|
||||
},
|
||||
});
|
||||
|
||||
expect(counts).toEqual({ refreshed: 1, skipped: 1 });
|
||||
expect(reports.get("preflight-01")).toBe("executed report evidence");
|
||||
expect(reports.get("board-01")).toBe("refreshed placeholder for board-01");
|
||||
});
|
||||
|
||||
it("treats execution metadata as evidence even before a case status changes", () => {
|
||||
const metadata = untouchedMetadata();
|
||||
metadata.execution.url = "http://127.0.0.1:34401";
|
||||
|
||||
expect(reportMetadataHasEvidence(metadata)).toBe(true);
|
||||
});
|
||||
|
||||
it("protects every evidence-bearing report while allowing a candidate-only placeholder", () => {
|
||||
expect(reportMetadataHasEvidence(untouchedMetadata())).toBe(false);
|
||||
|
||||
const evidenceReports = [
|
||||
() => ({ ...untouchedMetadata(), status: "passed" as const }),
|
||||
() => ({ ...untouchedMetadata(), caseStatuses: { "PF01-FIXTURE-STATE": "blocked" as const } }),
|
||||
() => ({
|
||||
...untouchedMetadata(),
|
||||
widgetChecks: [
|
||||
{ widgetKind: "weather", viewport: "320x568", sizeRequirement: "minimum", status: "passed" as const },
|
||||
],
|
||||
}),
|
||||
() => ({
|
||||
...untouchedMetadata(),
|
||||
findings: [{ severity: "P3" as const, title: "Observed defect" }],
|
||||
}),
|
||||
() => ({ ...untouchedMetadata(), artifacts: ["/tmp/evidence.png"] }),
|
||||
() => ({
|
||||
...untouchedMetadata(),
|
||||
performance: {
|
||||
measurements: [
|
||||
{
|
||||
name: "render",
|
||||
value: 1,
|
||||
unit: "ms",
|
||||
threshold: "under 10 ms",
|
||||
status: "passed" as const,
|
||||
evidence: [],
|
||||
},
|
||||
],
|
||||
limitations: [],
|
||||
},
|
||||
}),
|
||||
() => ({ ...untouchedMetadata(), performance: { measurements: [], limitations: ["Host limitation"] } }),
|
||||
() => ({
|
||||
...untouchedMetadata(),
|
||||
independentReproductions: [
|
||||
{
|
||||
findingFingerprint: "finding",
|
||||
agentId: "reproducer",
|
||||
outcome: "reproduced" as const,
|
||||
evidence: [],
|
||||
notes: "Confirmed.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
for (const evidenceReport of evidenceReports) expect(reportMetadataHasEvidence(evidenceReport())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA resolved-blocker rollover", () => {
|
||||
it("resets actual board and widget packet shapes while preserving an executed preflight", async () => {
|
||||
const preflightPacket = packet("preflight-01");
|
||||
const boardPacket = packet("board-01");
|
||||
const widgetPacket = packet("widgets-01");
|
||||
const executedPreflight = renderPacketReport(preflightPacket).replace(
|
||||
'"status": "not-reached"',
|
||||
'"status": "passed"',
|
||||
);
|
||||
const reports = new Map<string, { content: string; revision: number }>([
|
||||
[preflightPacket.id, { content: executedPreflight, revision: 1 }],
|
||||
[boardPacket.id, { content: blockedByResolvedFinding(boardPacket), revision: 1 }],
|
||||
[widgetPacket.id, { content: blockedByResolvedFinding(widgetPacket), revision: 1 }],
|
||||
]);
|
||||
|
||||
const counts = await rolloverResolvedBlockerReports([preflightPacket, boardPacket, widgetPacket], {
|
||||
read: async (entry) => {
|
||||
const report = reports.get(entry.id);
|
||||
if (!report) throw new Error(`Missing report ${entry.id}`);
|
||||
return { ...report };
|
||||
},
|
||||
isEligible: (content, entry) => reportContentIsOnlyBlockedBy(content, entry, blockerFingerprint),
|
||||
isUnchanged: async (entry, snapshot) => {
|
||||
const current = reports.get(entry.id);
|
||||
return current?.content === snapshot.content && current.revision === snapshot.revision;
|
||||
},
|
||||
render: renderPacketReport,
|
||||
write: async (entry, content) => {
|
||||
reports.set(entry.id, { content, revision: 2 });
|
||||
},
|
||||
});
|
||||
|
||||
expect(counts).toEqual({ reset: 2, skipped: 1, concurrentlyChanged: 0 });
|
||||
expect(reports.get(preflightPacket.id)?.content).toBe(executedPreflight);
|
||||
for (const packetId of [boardPacket.id, widgetPacket.id]) {
|
||||
const refreshed = reports.get(packetId)?.content ?? "";
|
||||
expect(refreshed).toContain(`"candidateSha": "${candidateSha}"`);
|
||||
expect(refreshed).toContain(`| Candidate SHA | ${candidateSha} |`);
|
||||
expect(refreshed).toContain("| Status | not-reached |");
|
||||
expect(refreshed).not.toContain(blockerFingerprint);
|
||||
}
|
||||
});
|
||||
|
||||
it("skips a report changed by an agent after eligibility was checked", async () => {
|
||||
const boardPacket = packet("board-01");
|
||||
const initial = blockedByResolvedFinding(boardPacket);
|
||||
const agentUpdate = `${initial}\nAgent completed new evidence.\n`;
|
||||
const reports = new Map([[boardPacket.id, { content: initial, revision: 1 }]]);
|
||||
|
||||
const counts = await rolloverResolvedBlockerReports([boardPacket], {
|
||||
read: async () => ({ content: initial, revision: 1 }),
|
||||
isEligible: (content, entry) => reportContentIsOnlyBlockedBy(content, entry, blockerFingerprint),
|
||||
isUnchanged: async () => {
|
||||
reports.set(boardPacket.id, { content: agentUpdate, revision: 2 });
|
||||
return false;
|
||||
},
|
||||
render: renderPacketReport,
|
||||
write: async (entry, content) => {
|
||||
reports.set(entry.id, { content, revision: 3 });
|
||||
},
|
||||
});
|
||||
|
||||
expect(counts).toEqual({ reset: 0, skipped: 0, concurrentlyChanged: 1 });
|
||||
expect(reports.get(boardPacket.id)?.content).toBe(agentUpdate);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,926 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { closeSync, constants, existsSync, openSync } from "node:fs";
|
||||
import { chmod, cp, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { createServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import {
|
||||
acquireBuildLease,
|
||||
acquireSlotLease,
|
||||
assertSafeContainedPath,
|
||||
assertSafeRunRoot,
|
||||
assertSupportedRuntimePlatform,
|
||||
createFixtureEnvironment,
|
||||
sanitizeAppEnvironment,
|
||||
} from "./safety.mts";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const defaultRunRoot = path.join(tmpdir(), "homarr-release-v2-qa");
|
||||
const markerName = ".homarr-release-v2-qa-root.json";
|
||||
const runtimeMode = "production-standalone" as const;
|
||||
const runtimeBundler = "webpack" as const;
|
||||
const candidateBuildHeapLimitMb = 16_384;
|
||||
const candidateBuildManifestSchemaVersion = 2;
|
||||
const profiles = ["main-writable", "main-readonly", "onboarding-fresh", "degraded"] as const;
|
||||
|
||||
type Profile = (typeof profiles)[number];
|
||||
export type RuntimeCommand = "start" | "status" | "stop";
|
||||
|
||||
export const assertRuntimeCommandPlatform = (command: RuntimeCommand, platform: NodeJS.Platform = process.platform) => {
|
||||
if (command === "status") return;
|
||||
assertSupportedRuntimePlatform(platform);
|
||||
};
|
||||
|
||||
interface RuntimeExecutionContract {
|
||||
runtimeMode: typeof runtimeMode;
|
||||
bundler: typeof runtimeBundler;
|
||||
watcher: null;
|
||||
}
|
||||
|
||||
export const createRuntimeExecutionContract = (): RuntimeExecutionContract => ({
|
||||
runtimeMode,
|
||||
bundler: runtimeBundler,
|
||||
watcher: null,
|
||||
});
|
||||
|
||||
export const validateRuntimeExecutionContract = (value: {
|
||||
runtimeMode?: unknown;
|
||||
bundler?: unknown;
|
||||
watcher?: unknown;
|
||||
}): string[] => {
|
||||
const contractErrors: string[] = [];
|
||||
if (value.runtimeMode !== runtimeMode) contractErrors.push("runtime mode must be production-standalone");
|
||||
if (value.bundler !== runtimeBundler) contractErrors.push("bundler must be webpack");
|
||||
if (value.watcher !== null && value.watcher !== undefined) {
|
||||
contractErrors.push("standalone runtimes must not use a filesystem watcher");
|
||||
}
|
||||
return contractErrors;
|
||||
};
|
||||
|
||||
export const createAppLaunchCommand = (
|
||||
serverPath: string,
|
||||
options: { platform?: NodeJS.Platform; prlimitAvailable?: boolean; nodeExecutable?: string } = {},
|
||||
) => {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const prlimitAvailable = options.prlimitAvailable ?? existsSync("/usr/bin/prlimit");
|
||||
const nodeExecutable = options.nodeExecutable ?? process.execPath;
|
||||
if (platform === "linux" && prlimitAvailable) {
|
||||
return {
|
||||
command: "/usr/bin/prlimit",
|
||||
arguments: ["--nofile=65536:1048576", "--", nodeExecutable, serverPath],
|
||||
};
|
||||
}
|
||||
return { command: nodeExecutable, arguments: [serverPath] };
|
||||
};
|
||||
|
||||
export const createCandidateBuildPaths = (candidateSha: string) => {
|
||||
if (!/^[0-9a-f]{40}$/u.test(candidateSha)) {
|
||||
throw new Error("Candidate build requires a 40-character lowercase commit SHA");
|
||||
}
|
||||
const appRoot = path.join(repoRoot, "apps/nextjs");
|
||||
const distDirName = `.next-qa/release-v2-${candidateSha}`;
|
||||
const buildDir = path.join(appRoot, distDirName);
|
||||
const standaloneRoot = path.join(buildDir, "standalone");
|
||||
const standaloneAppDir = path.join(standaloneRoot, "apps/nextjs");
|
||||
const standaloneStaticDir = path.join(standaloneAppDir, distDirName, "static");
|
||||
return {
|
||||
appRoot,
|
||||
distDirName,
|
||||
buildDir,
|
||||
standaloneRoot,
|
||||
standaloneAppDir,
|
||||
standaloneStaticDir,
|
||||
standaloneStaticChunksDir: path.join(standaloneStaticDir, "chunks"),
|
||||
generatedServerPath: path.join(standaloneAppDir, "server.js"),
|
||||
serverPath: path.join(standaloneAppDir, "server.cjs"),
|
||||
markerPath: path.join(buildDir, "qa-build-manifest.json"),
|
||||
};
|
||||
};
|
||||
|
||||
interface Options {
|
||||
command: RuntimeCommand;
|
||||
slot: number;
|
||||
profile: Profile;
|
||||
runRoot: string;
|
||||
candidate: string;
|
||||
reset: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeManifest {
|
||||
schemaVersion: 1;
|
||||
runId: string;
|
||||
status: "running" | "stopped";
|
||||
candidateSha: string;
|
||||
branch: string | null;
|
||||
slot: number;
|
||||
profile: Profile;
|
||||
startedAt: string;
|
||||
stoppedAt?: string;
|
||||
repoRoot: string;
|
||||
runRoot: string;
|
||||
slotDir: string;
|
||||
dbPath: string;
|
||||
trustedCertificatePath: string;
|
||||
fixtureManifestPath: string;
|
||||
nextDistDir: string;
|
||||
url: string;
|
||||
fixtureUrl: string;
|
||||
ports: { app: number; fixture: number; redis: number };
|
||||
flags: { demoMode: boolean; demoReadOnly: boolean; unsafeMockIntegration: boolean };
|
||||
runtimeMode: typeof runtimeMode;
|
||||
bundler: typeof runtimeBundler;
|
||||
watcher: null;
|
||||
build: { candidateSha: string; serverPath: string };
|
||||
processes: {
|
||||
app: { pid: number; logPath: string };
|
||||
fixture: { pid: number; logPath: string };
|
||||
};
|
||||
docker: { redisContainer: string };
|
||||
}
|
||||
|
||||
export const createSlotTrustedCertificateDirectory = async (runRoot: string, slotDir: string) => {
|
||||
await assertSafeContainedPath(runRoot, slotDir, "QA slot directory");
|
||||
const trustedCertificatePath = path.join(slotDir, "trusted-certificates");
|
||||
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
|
||||
await mkdir(trustedCertificatePath, { mode: 0o700 });
|
||||
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
|
||||
await chmod(trustedCertificatePath, 0o700);
|
||||
return trustedCertificatePath;
|
||||
};
|
||||
|
||||
const usage = () => {
|
||||
console.log(
|
||||
"Usage: runtime.mts <start|status|stop> --slot <1|2|3> [--profile <main-writable|main-readonly|onboarding-fresh|degraded>] [--run-root <path>] [--candidate <ref>] [--reset]",
|
||||
);
|
||||
};
|
||||
|
||||
const parseArgs = (): Options => {
|
||||
const args = process.argv.slice(2).filter((argument) => argument !== "--");
|
||||
if (args[0] === "--help" || args[0] === "-h") {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
const command = args.shift();
|
||||
if (command !== "start" && command !== "status" && command !== "stop") {
|
||||
usage();
|
||||
throw new Error("A runtime command is required");
|
||||
}
|
||||
|
||||
let slot = 0;
|
||||
let profile: Profile = "main-writable";
|
||||
let runRoot = defaultRunRoot;
|
||||
let candidate = "HEAD";
|
||||
let reset = false;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const argument = args[index];
|
||||
const value = args[index + 1];
|
||||
if (argument === "--slot" && value) {
|
||||
slot = Number(value);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--profile" && value && profiles.includes(value as Profile)) {
|
||||
profile = value as Profile;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--run-root" && value) {
|
||||
runRoot = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--candidate" && value) {
|
||||
candidate = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--reset") {
|
||||
reset = true;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--help" || argument === "-h") {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`Unknown or incomplete argument: ${argument ?? "<missing>"}`);
|
||||
}
|
||||
|
||||
if (![1, 2, 3].includes(slot)) throw new Error("--slot must be 1, 2, or 3");
|
||||
return { command, slot, profile, runRoot: path.resolve(runRoot), candidate, reset };
|
||||
};
|
||||
|
||||
const commandOutput = (command: string, args: string[]) => {
|
||||
const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8" });
|
||||
if (result.status !== 0) {
|
||||
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status ?? "unknown"}`;
|
||||
throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
};
|
||||
|
||||
const resolveCandidate = (candidate: string) =>
|
||||
commandOutput("git", ["rev-parse", "--verify", `${candidate}^{commit}`]).toLowerCase();
|
||||
|
||||
const currentBranch = () => {
|
||||
const result = spawnSync("git", ["branch", "--show-current"], { cwd: repoRoot, encoding: "utf8" });
|
||||
const branch = result.stdout.trim();
|
||||
return branch.length > 0 ? branch : null;
|
||||
};
|
||||
|
||||
const writeJsonAtomic = async (filePath: string, value: unknown) => {
|
||||
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
||||
const temporaryPath = `${filePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
||||
const handle = await open(
|
||||
temporaryPath,
|
||||
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
try {
|
||||
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
try {
|
||||
await rename(temporaryPath, filePath);
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { force: true });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const assertRunRootMarker = async (runRoot: string) => {
|
||||
const markerPath = path.join(runRoot, markerName);
|
||||
await assertSafeContainedPath(runRoot, markerPath, "QA run-root marker");
|
||||
const marker = JSON.parse(await readFile(markerPath, "utf8")) as {
|
||||
schemaVersion?: number;
|
||||
repoRoot?: string;
|
||||
runRoot?: string;
|
||||
};
|
||||
if (marker.schemaVersion !== 1 || path.resolve(marker.repoRoot ?? "") !== repoRoot) {
|
||||
throw new Error(`QA run-root marker does not belong to this checkout: ${markerPath}`);
|
||||
}
|
||||
if (marker.runRoot && path.resolve(marker.runRoot) !== runRoot) {
|
||||
throw new Error(`QA run-root marker records a different root: ${markerPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const readManifest = async (manifestPath: string) => {
|
||||
const value = JSON.parse(await readFile(manifestPath, "utf8")) as RuntimeManifest;
|
||||
if (value.schemaVersion !== 1) throw new Error(`Unsupported runtime manifest: ${manifestPath}`);
|
||||
return value;
|
||||
};
|
||||
|
||||
export interface PortReservation {
|
||||
port: number;
|
||||
release: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const reserveLoopbackPort = async (): Promise<PortReservation> =>
|
||||
await new Promise<PortReservation>((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Unable to allocate a TCP port"));
|
||||
return;
|
||||
}
|
||||
let released = false;
|
||||
resolve({
|
||||
port: address.port,
|
||||
release: async () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
await new Promise<void>((closeResolve, closeReject) => {
|
||||
server.close((error) => {
|
||||
if (error) closeReject(error);
|
||||
else closeResolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type PortLaunchResult<T> = { status: "ready"; value: T } | { status: "address-in-use" };
|
||||
|
||||
export const launchWithPortRetry = async <T,>(
|
||||
launch: (port: number) => Promise<PortLaunchResult<T>>,
|
||||
options: { attempts?: number; initialReservation?: PortReservation } = {},
|
||||
): Promise<T> => {
|
||||
const attempts = options.attempts ?? 3;
|
||||
let reservation = options.initialReservation;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
reservation ??= await reserveLoopbackPort();
|
||||
const port = reservation.port;
|
||||
await reservation.release();
|
||||
reservation = undefined;
|
||||
const result = await launch(port);
|
||||
if (result.status === "ready") return result.value;
|
||||
}
|
||||
throw new Error(`Unable to launch the QA app after ${attempts} address-in-use retries`);
|
||||
};
|
||||
|
||||
const waitForJsonFile = async <T,>(filePath: string, timeoutMs: number) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
return JSON.parse(await readFile(filePath, "utf8")) as T;
|
||||
} catch {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${filePath}`);
|
||||
};
|
||||
|
||||
const waitForUrl = async (url: string, timeoutMs: number) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastStatus: number | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5_000) });
|
||||
lastStatus = response.status;
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
// Startup failures are reported from the process log after the deadline.
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${url}; last status=${lastStatus ?? "unreachable"}`);
|
||||
};
|
||||
|
||||
interface SpawnedProcess {
|
||||
pid: number;
|
||||
exited: Promise<number | null>;
|
||||
}
|
||||
|
||||
const spawnLogged = (
|
||||
command: string,
|
||||
args: string[],
|
||||
logPath: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: { cwd?: string } = {},
|
||||
): SpawnedProcess => {
|
||||
const output = openSync(
|
||||
logPath,
|
||||
constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY | constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
detached: true,
|
||||
env,
|
||||
stdio: ["ignore", output, output],
|
||||
});
|
||||
closeSync(output);
|
||||
if (!child.pid) throw new Error(`Failed to launch ${command}`);
|
||||
const exited = new Promise<number | null>((resolve) => {
|
||||
child.once("exit", (code) => resolve(code));
|
||||
});
|
||||
child.unref();
|
||||
return { pid: child.pid, exited };
|
||||
};
|
||||
|
||||
const runForeground = (command: string, args: string[], env: NodeJS.ProcessEnv) => {
|
||||
const result = spawnSync(command, args, { cwd: repoRoot, env, stdio: "inherit" });
|
||||
if (result.status !== 0)
|
||||
throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status ?? "unknown"}`);
|
||||
};
|
||||
|
||||
const candidateBuildIsReady = async (candidateSha: string) => {
|
||||
const buildPaths = createCandidateBuildPaths(candidateSha);
|
||||
try {
|
||||
const marker = JSON.parse(await readFile(buildPaths.markerPath, "utf8")) as {
|
||||
schemaVersion?: number;
|
||||
candidateSha?: string;
|
||||
runtimeMode?: string;
|
||||
bundler?: string;
|
||||
};
|
||||
const serverStats = await stat(buildPaths.serverPath);
|
||||
const staticStats = await stat(buildPaths.standaloneStaticDir);
|
||||
const staticChunksStats = await stat(buildPaths.standaloneStaticChunksDir);
|
||||
const publicStats = await stat(path.join(buildPaths.standaloneAppDir, "public"));
|
||||
return (
|
||||
marker.schemaVersion === candidateBuildManifestSchemaVersion &&
|
||||
marker.candidateSha === candidateSha &&
|
||||
marker.runtimeMode === runtimeMode &&
|
||||
marker.bundler === runtimeBundler &&
|
||||
serverStats.isFile() &&
|
||||
staticStats.isDirectory() &&
|
||||
staticChunksStats.isDirectory() &&
|
||||
publicStats.isDirectory()
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureCandidateBuild = async (runRoot: string, candidateSha: string) => {
|
||||
const buildPaths = createCandidateBuildPaths(candidateSha);
|
||||
if (await candidateBuildIsReady(candidateSha)) return buildPaths;
|
||||
|
||||
const buildLease = await acquireBuildLease(runRoot, `build:${candidateSha}`);
|
||||
try {
|
||||
if (await candidateBuildIsReady(candidateSha)) return buildPaths;
|
||||
await assertSafeContainedPath(buildPaths.appRoot, buildPaths.buildDir, "QA candidate build directory");
|
||||
await rm(buildPaths.buildDir, { recursive: true, force: true });
|
||||
await assertSafeContainedPath(buildPaths.appRoot, buildPaths.buildDir, "QA candidate build directory");
|
||||
|
||||
runForeground("pnpm", ["--filter", "@homarr/nextjs", "with-env", "next", "build", "--webpack"], {
|
||||
...sanitizeAppEnvironment(process.env),
|
||||
CI: "true",
|
||||
DISABLE_REDIS_LOGS: "true",
|
||||
HOMARR_QA_NEXT_DIST_DIR: buildPaths.distDirName,
|
||||
HOMARR_QA_STANDALONE_BUILD: "true",
|
||||
HOMARR_VERSION: candidateSha.slice(0, 12),
|
||||
NEXT_TELEMETRY_DISABLED: "1",
|
||||
NODE_OPTIONS: `--max-old-space-size=${candidateBuildHeapLimitMb}`,
|
||||
SKIP_ENV_VALIDATION: "true",
|
||||
});
|
||||
|
||||
const publicTarget = path.join(buildPaths.standaloneAppDir, "public");
|
||||
await assertSafeContainedPath(
|
||||
buildPaths.standaloneAppDir,
|
||||
buildPaths.standaloneStaticDir,
|
||||
"QA standalone static directory",
|
||||
);
|
||||
await mkdir(path.dirname(buildPaths.standaloneStaticDir), { recursive: true });
|
||||
await cp(path.join(buildPaths.buildDir, "static"), buildPaths.standaloneStaticDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
await cp(path.join(buildPaths.appRoot, "public"), publicTarget, { recursive: true, force: true });
|
||||
// The app package is ESM, while Next emits a CommonJS standalone launcher.
|
||||
// Give the generated launcher an explicit CommonJS extension before executing it.
|
||||
await rename(buildPaths.generatedServerPath, buildPaths.serverPath);
|
||||
const serverStats = await stat(buildPaths.serverPath);
|
||||
if (!serverStats.isFile()) throw new Error(`Standalone server is missing after build: ${buildPaths.serverPath}`);
|
||||
const staticChunksStats = await stat(buildPaths.standaloneStaticChunksDir);
|
||||
if (!staticChunksStats.isDirectory()) {
|
||||
throw new Error(`Standalone static chunks are missing after build: ${buildPaths.standaloneStaticChunksDir}`);
|
||||
}
|
||||
await writeJsonAtomic(buildPaths.markerPath, {
|
||||
schemaVersion: candidateBuildManifestSchemaVersion,
|
||||
candidateSha,
|
||||
runtimeMode,
|
||||
bundler: runtimeBundler,
|
||||
builtAt: new Date().toISOString(),
|
||||
});
|
||||
return buildPaths;
|
||||
} finally {
|
||||
await buildLease.release();
|
||||
}
|
||||
};
|
||||
|
||||
const isProcessRunning = (pid: number) => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const waitForAppAttempt = async (
|
||||
spawned: SpawnedProcess,
|
||||
url: string,
|
||||
logPath: string,
|
||||
logOffset: number,
|
||||
timeoutMs: number,
|
||||
): Promise<"ready" | "address-in-use"> => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const exitCode = await Promise.race([
|
||||
spawned.exited,
|
||||
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), 250)),
|
||||
]);
|
||||
if (exitCode !== undefined) {
|
||||
const log = await readFile(logPath, "utf8").catch(() => "");
|
||||
const attemptLog = log.slice(logOffset);
|
||||
if (/EADDRINUSE|address already in use/iu.test(attemptLog)) return "address-in-use";
|
||||
throw new Error(`QA app process exited during startup; inspect ${logPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${url}/api/health/ready`, {
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
if (response.ok && isProcessRunning(spawned.pid)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
if (isProcessRunning(spawned.pid)) return "ready";
|
||||
}
|
||||
} catch {
|
||||
// Continue while the selected process owns its startup attempt.
|
||||
}
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${url}; inspect ${logPath}`);
|
||||
};
|
||||
|
||||
const processBelongsToRun = async (pid: number, runId: string) => {
|
||||
if (!isProcessRunning(pid)) return false;
|
||||
try {
|
||||
const environment = await readFile(`/proc/${pid}/environ`);
|
||||
return environment.includes(Buffer.from(`QA_RUN_ID=${runId}\0`));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopProcess = async (pid: number, runId: string) => {
|
||||
if (!isProcessRunning(pid)) return;
|
||||
if (!(await processBelongsToRun(pid, runId))) {
|
||||
throw new Error(`Refusing to signal PID ${pid}: it is not owned by QA run ${runId}`);
|
||||
}
|
||||
try {
|
||||
process.kill(-pid, "SIGTERM");
|
||||
} catch {
|
||||
process.kill(pid, "SIGTERM");
|
||||
}
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (Date.now() < deadline && isProcessRunning(pid)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
if (isProcessRunning(pid)) {
|
||||
try {
|
||||
process.kill(-pid, "SIGKILL");
|
||||
} catch {
|
||||
process.kill(pid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeRedisContainer = (container: string, runId: string) => {
|
||||
const inspect = spawnSync(
|
||||
"docker",
|
||||
["inspect", "--format", '{{ index .Config.Labels "com.homarr.release-v2-qa.run" }}', container],
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (inspect.status !== 0) return;
|
||||
if (inspect.stdout.trim() !== runId)
|
||||
throw new Error(`Refusing to remove Docker container without matching QA run label: ${container}`);
|
||||
const result = spawnSync("docker", ["rm", "--force", container], { stdio: "inherit" });
|
||||
if (result.status !== 0) throw new Error(`Failed to remove QA Redis container ${container}`);
|
||||
};
|
||||
|
||||
const stopFromManifest = async (
|
||||
manifestPath: string,
|
||||
manifest: RuntimeManifest,
|
||||
expectedRunRoot: string,
|
||||
expectedSlot: number,
|
||||
) => {
|
||||
if (manifest.status === "stopped") return;
|
||||
const expectedSlotDir = path.join(expectedRunRoot, "slots", String(expectedSlot));
|
||||
const expectedManifestPath = path.join(expectedSlotDir, "runtime-manifest.json");
|
||||
if (path.resolve(manifest.runRoot) !== expectedRunRoot || manifest.slot !== expectedSlot) {
|
||||
throw new Error(`Refusing runtime manifest for a different QA run-root or slot: ${manifestPath}`);
|
||||
}
|
||||
if (path.resolve(manifestPath) !== expectedManifestPath) {
|
||||
throw new Error(`Refusing runtime manifest outside the selected slot: ${manifestPath}`);
|
||||
}
|
||||
if (path.resolve(manifest.slotDir) !== path.resolve(expectedSlotDir)) {
|
||||
throw new Error(`Refusing runtime manifest with an unsafe slot directory: ${manifest.slotDir}`);
|
||||
}
|
||||
await assertSafeContainedPath(expectedRunRoot, expectedSlotDir, "QA slot directory");
|
||||
await stopProcess(manifest.processes.app.pid, manifest.runId);
|
||||
await stopProcess(manifest.processes.fixture.pid, manifest.runId);
|
||||
removeRedisContainer(manifest.docker.redisContainer, manifest.runId);
|
||||
const stopped: RuntimeManifest = { ...manifest, status: "stopped", stoppedAt: new Date().toISOString() };
|
||||
await assertSafeContainedPath(expectedRunRoot, manifestPath, "QA runtime manifest");
|
||||
await writeJsonAtomic(manifestPath, stopped);
|
||||
};
|
||||
|
||||
const profileFlags = (profile: Profile) => ({
|
||||
demoMode: profile !== "onboarding-fresh",
|
||||
demoReadOnly: profile === "main-readonly",
|
||||
unsafeMockIntegration: true,
|
||||
});
|
||||
|
||||
const startWithLease = async (options: Options) => {
|
||||
const candidateSha = resolveCandidate(options.candidate);
|
||||
const checkoutSha = resolveCandidate("HEAD");
|
||||
if (checkoutSha !== candidateSha) {
|
||||
throw new Error(`The requested candidate ${candidateSha} does not match the checked-out HEAD ${checkoutSha}`);
|
||||
}
|
||||
const branch = currentBranch();
|
||||
|
||||
await mkdir(options.runRoot, { recursive: true, mode: 0o700 });
|
||||
await assertSafeRunRoot(options.runRoot);
|
||||
await chmod(options.runRoot, 0o700);
|
||||
await assertSafeRunRoot(options.runRoot);
|
||||
const markerPath = path.join(options.runRoot, markerName);
|
||||
await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker");
|
||||
if (!existsSync(markerPath)) {
|
||||
await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker");
|
||||
await writeJsonAtomic(markerPath, {
|
||||
schemaVersion: 1,
|
||||
repoRoot,
|
||||
runRoot: options.runRoot,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
await assertRunRootMarker(options.runRoot);
|
||||
}
|
||||
const candidateBuild = await ensureCandidateBuild(options.runRoot, candidateSha);
|
||||
|
||||
const slotsRoot = path.join(options.runRoot, "slots");
|
||||
await assertSafeContainedPath(options.runRoot, slotsRoot, "QA slots directory");
|
||||
await mkdir(slotsRoot, { recursive: true, mode: 0o700 });
|
||||
await assertSafeContainedPath(options.runRoot, slotsRoot, "QA slots directory");
|
||||
const slotDir = path.join(slotsRoot, String(options.slot));
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory");
|
||||
const manifestPath = path.join(slotDir, "runtime-manifest.json");
|
||||
if (existsSync(manifestPath)) {
|
||||
await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest");
|
||||
const existing = await readManifest(manifestPath);
|
||||
if (!options.reset) throw new Error(`Slot ${options.slot} already has a manifest; pass --reset to replace it`);
|
||||
await stopFromManifest(manifestPath, existing, options.runRoot, options.slot);
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target");
|
||||
await rm(slotDir, { recursive: true });
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target");
|
||||
} else if (existsSync(slotDir)) {
|
||||
if (!options.reset) throw new Error(`Slot ${options.slot} has incomplete state; pass --reset to replace it`);
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target");
|
||||
await rm(slotDir, { recursive: true });
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot reset target");
|
||||
}
|
||||
const legacyNextDistDir = path.join(repoRoot, "apps/nextjs/.next-qa", `slot-${options.slot}`);
|
||||
if (options.reset) {
|
||||
await assertSafeContainedPath(
|
||||
path.join(repoRoot, "apps/nextjs"),
|
||||
legacyNextDistDir,
|
||||
"legacy QA Next cache reset target",
|
||||
);
|
||||
await rm(legacyNextDistDir, { recursive: true, force: true });
|
||||
await assertSafeContainedPath(
|
||||
path.join(repoRoot, "apps/nextjs"),
|
||||
legacyNextDistDir,
|
||||
"legacy QA Next cache reset target",
|
||||
);
|
||||
}
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot creation target");
|
||||
await mkdir(slotDir, { mode: 0o700 });
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory");
|
||||
await chmod(slotDir, 0o700);
|
||||
await assertSafeContainedPath(options.runRoot, slotDir, "QA slot directory");
|
||||
|
||||
const runId = `release-v2-${candidateSha.slice(0, 8)}-s${options.slot}-${Date.now()}`;
|
||||
const redisContainer = `homarr-release-v2-qa-redis-s${options.slot}-${candidateSha.slice(0, 8)}-${Date.now().toString(36)}`;
|
||||
const dbPath = path.join(slotDir, "db.sqlite");
|
||||
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(options.runRoot, slotDir);
|
||||
const fixtureReadyPath = path.join(slotDir, "fixture-ready.json");
|
||||
const fixtureManifestPath = path.join(slotDir, "fixture-manifest.json");
|
||||
const fixtureLogPath = path.join(slotDir, "fixture.log");
|
||||
const appLogPath = path.join(slotDir, "app.log");
|
||||
const flags = profileFlags(options.profile);
|
||||
let fixturePid: number | undefined;
|
||||
let appPid: number | undefined;
|
||||
let appPortReservation: PortReservation | undefined;
|
||||
let redisCreated = false;
|
||||
|
||||
try {
|
||||
const redisRun = spawnSync(
|
||||
"docker",
|
||||
[
|
||||
"run",
|
||||
"--detach",
|
||||
"--name",
|
||||
redisContainer,
|
||||
"--label",
|
||||
`com.homarr.release-v2-qa.run=${runId}`,
|
||||
"--publish",
|
||||
"127.0.0.1::6379",
|
||||
"redis:latest",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (redisRun.status !== 0) throw new Error(`Unable to start QA Redis: ${redisRun.stderr.trim()}`);
|
||||
redisCreated = true;
|
||||
const redisPortOutput = commandOutput("docker", ["port", redisContainer, "6379/tcp"]);
|
||||
const redisPort = Number(redisPortOutput.split(":").at(-1));
|
||||
if (!Number.isInteger(redisPort)) throw new Error(`Unable to parse QA Redis port: ${redisPortOutput}`);
|
||||
|
||||
await assertSafeContainedPath(options.runRoot, fixtureReadyPath, "QA fixture readiness file");
|
||||
await assertSafeContainedPath(options.runRoot, fixtureLogPath, "QA fixture log");
|
||||
const fixtureProcess = spawnLogged(
|
||||
process.execPath,
|
||||
["--import", "tsx", "scripts/release-v2-qa/fixture-server.mts", "--port", "0", "--ready-file", fixtureReadyPath],
|
||||
fixtureLogPath,
|
||||
createFixtureEnvironment(runId),
|
||||
);
|
||||
fixturePid = fixtureProcess.pid;
|
||||
const fixtureReady = await waitForJsonFile<{ url: string; port: number }>(fixtureReadyPath, 30_000);
|
||||
await waitForUrl(`${fixtureReady.url}/health`, 30_000);
|
||||
appPortReservation = await reserveLoopbackPort();
|
||||
let appPort = appPortReservation.port;
|
||||
let url = `http://127.0.0.1:${appPort}`;
|
||||
let qaPassword = process.env.QA_PASSWORD;
|
||||
if (!qaPassword && process.env.QA_PASSWORD_FILE) {
|
||||
qaPassword = (await readFile(path.resolve(process.env.QA_PASSWORD_FILE), "utf8")).trim();
|
||||
}
|
||||
if (flags.demoMode && !qaPassword) {
|
||||
throw new Error("QA_PASSWORD or QA_PASSWORD_FILE is required for populated QA profiles");
|
||||
}
|
||||
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
AUTH_SECRET: randomBytes(32).toString("hex"),
|
||||
AUTH_PROVIDERS: "credentials",
|
||||
AUTH_COOKIE_PREFIX: `homarr_qa_s${options.slot}`,
|
||||
DB_DRIVER: "better-sqlite3",
|
||||
DB_URL: dbPath,
|
||||
LOCAL_CERTIFICATE_PATH: trustedCertificatePath,
|
||||
DEMO_MODE: String(flags.demoMode),
|
||||
DEMO_READ_ONLY: String(flags.demoReadOnly),
|
||||
UNSAFE_ENABLE_MOCK_INTEGRATION: String(flags.unsafeMockIntegration),
|
||||
SECRET_ENCRYPTION_KEY: randomBytes(32).toString("hex"),
|
||||
REDIS_IS_EXTERNAL: "true",
|
||||
REDIS_HOST: "127.0.0.1",
|
||||
REDIS_PORT: String(redisPort),
|
||||
REDIS_DATABASE_INDEX: String(options.slot),
|
||||
QA_CANDIDATE_SHA: candidateSha,
|
||||
QA_FIXTURE_URL: fixtureReady.url,
|
||||
QA_PROFILE: options.profile,
|
||||
QA_RUN_ID: runId,
|
||||
QA_RUN_ROOT: options.runRoot,
|
||||
QA_SLOT: String(options.slot),
|
||||
QA_PASSWORD: qaPassword,
|
||||
HOMARR_WEBSITE_URL: url,
|
||||
WORKSHOP_API_URL: fixtureReady.url,
|
||||
WORKSHOP_WEB_URL: `${fixtureReady.url}/iframe`,
|
||||
HOMARR_QA_NEXT_DIST_DIR: candidateBuild.distDirName,
|
||||
HOMARR_VERSION: candidateSha.slice(0, 12),
|
||||
NEXT_TELEMETRY_DISABLED: "1",
|
||||
};
|
||||
|
||||
await assertSafeContainedPath(options.runRoot, dbPath, "QA SQLite database");
|
||||
runForeground("pnpm", ["db:migration:sqlite:run"], environment);
|
||||
await assertSafeContainedPath(options.runRoot, dbPath, "QA SQLite database");
|
||||
await assertSafeContainedPath(options.runRoot, fixtureManifestPath, "QA fixture manifest");
|
||||
runForeground("pnpm", ["qa:release-v2:seed", "--", "--profile", options.profile, "--output", slotDir], environment);
|
||||
await assertSafeContainedPath(options.runRoot, fixtureManifestPath, "QA fixture manifest");
|
||||
|
||||
const appEnvironment = sanitizeAppEnvironment(environment);
|
||||
await assertSafeContainedPath(options.runRoot, appLogPath, "QA app log");
|
||||
const launchedApp = await launchWithPortRetry<{ pid: number; port: number; url: string }>(
|
||||
async (attemptPort) => {
|
||||
appPort = attemptPort;
|
||||
url = `http://127.0.0.1:${appPort}`;
|
||||
const appLaunch = createAppLaunchCommand(candidateBuild.serverPath);
|
||||
const logOffset = await stat(appLogPath)
|
||||
.then((stats) => stats.size)
|
||||
.catch(() => 0);
|
||||
await assertSafeContainedPath(options.runRoot, appLogPath, "QA app log");
|
||||
const spawned = spawnLogged(
|
||||
appLaunch.command,
|
||||
appLaunch.arguments,
|
||||
appLogPath,
|
||||
{
|
||||
...appEnvironment,
|
||||
HOSTNAME: "127.0.0.1",
|
||||
HOMARR_WEBSITE_URL: url,
|
||||
NODE_ENV: "production",
|
||||
PORT: String(appPort),
|
||||
},
|
||||
{ cwd: candidateBuild.standaloneRoot },
|
||||
);
|
||||
appPid = spawned.pid;
|
||||
const outcome = await waitForAppAttempt(spawned, url, appLogPath, logOffset, 300_000);
|
||||
if (outcome === "address-in-use") {
|
||||
appPid = undefined;
|
||||
return { status: "address-in-use" } as const;
|
||||
}
|
||||
return { status: "ready", value: { pid: spawned.pid, port: appPort, url } } as const;
|
||||
},
|
||||
{ initialReservation: appPortReservation },
|
||||
);
|
||||
appPortReservation = undefined;
|
||||
appPid = launchedApp.pid;
|
||||
appPort = launchedApp.port;
|
||||
url = launchedApp.url;
|
||||
|
||||
const manifest: RuntimeManifest = {
|
||||
schemaVersion: 1,
|
||||
runId,
|
||||
status: "running",
|
||||
candidateSha,
|
||||
branch,
|
||||
slot: options.slot,
|
||||
profile: options.profile,
|
||||
startedAt: new Date().toISOString(),
|
||||
repoRoot,
|
||||
runRoot: options.runRoot,
|
||||
slotDir,
|
||||
dbPath,
|
||||
trustedCertificatePath,
|
||||
fixtureManifestPath,
|
||||
nextDistDir: candidateBuild.buildDir,
|
||||
url,
|
||||
fixtureUrl: fixtureReady.url,
|
||||
ports: { app: appPort, fixture: fixtureReady.port, redis: redisPort },
|
||||
flags,
|
||||
...createRuntimeExecutionContract(),
|
||||
build: { candidateSha, serverPath: candidateBuild.serverPath },
|
||||
processes: {
|
||||
app: { pid: appPid, logPath: appLogPath },
|
||||
fixture: { pid: fixturePid, logPath: fixtureLogPath },
|
||||
},
|
||||
docker: { redisContainer },
|
||||
};
|
||||
await writeJsonAtomic(manifestPath, manifest);
|
||||
console.log(JSON.stringify(manifest, null, 2));
|
||||
} catch (error) {
|
||||
if (appPortReservation) await appPortReservation.release();
|
||||
if (appPid) await stopProcess(appPid, runId);
|
||||
if (fixturePid) await stopProcess(fixturePid, runId);
|
||||
if (redisCreated) removeRedisContainer(redisContainer, runId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const start = async (options: Options) => {
|
||||
options.runRoot = await assertSafeRunRoot(options.runRoot, { allowMissing: true });
|
||||
const lease = await acquireSlotLease(options.runRoot, options.slot, `start:${options.profile}`);
|
||||
try {
|
||||
await startWithLease(options);
|
||||
} finally {
|
||||
await lease.release();
|
||||
}
|
||||
};
|
||||
|
||||
const status = async (options: Options) => {
|
||||
options.runRoot = await assertSafeRunRoot(options.runRoot);
|
||||
await assertRunRootMarker(options.runRoot);
|
||||
const manifestPath = path.join(options.runRoot, "slots", String(options.slot), "runtime-manifest.json");
|
||||
await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest");
|
||||
const manifest = await readManifest(manifestPath);
|
||||
const appReachable = await fetch(`${manifest.url}/api/health/live`, { signal: AbortSignal.timeout(5_000) })
|
||||
.then((response) => response.ok)
|
||||
.catch(() => false);
|
||||
const fixtureReachable = await fetch(`${manifest.fixtureUrl}/health`, { signal: AbortSignal.timeout(5_000) })
|
||||
.then((response) => response.ok)
|
||||
.catch(() => false);
|
||||
const redisRunning =
|
||||
spawnSync("docker", ["inspect", "--format", "{{.State.Running}}", manifest.docker.redisContainer], {
|
||||
encoding: "utf8",
|
||||
}).stdout.trim() === "true";
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...manifest,
|
||||
observed: {
|
||||
appProcess: await processBelongsToRun(manifest.processes.app.pid, manifest.runId),
|
||||
fixtureProcess: await processBelongsToRun(manifest.processes.fixture.pid, manifest.runId),
|
||||
appReachable,
|
||||
fixtureReachable,
|
||||
redisRunning,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const stopWithLease = async (options: Options) => {
|
||||
const markerPath = path.join(options.runRoot, markerName);
|
||||
await assertSafeContainedPath(options.runRoot, markerPath, "QA run-root marker");
|
||||
if (!existsSync(markerPath)) throw new Error(`QA run-root marker is missing: ${markerPath}`);
|
||||
await assertRunRootMarker(options.runRoot);
|
||||
const manifestPath = path.join(options.runRoot, "slots", String(options.slot), "runtime-manifest.json");
|
||||
await assertSafeContainedPath(options.runRoot, manifestPath, "QA runtime manifest");
|
||||
const manifest = await readManifest(manifestPath);
|
||||
await stopFromManifest(manifestPath, manifest, options.runRoot, options.slot);
|
||||
console.log(`Stopped release-v2 QA slot ${options.slot}`);
|
||||
};
|
||||
|
||||
const stop = async (options: Options) => {
|
||||
options.runRoot = await assertSafeRunRoot(options.runRoot);
|
||||
const lease = await acquireSlotLease(options.runRoot, options.slot, "stop");
|
||||
try {
|
||||
await stopWithLease(options);
|
||||
} finally {
|
||||
await lease.release();
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const options = parseArgs();
|
||||
assertRuntimeCommandPlatform(options.command);
|
||||
if (options.command === "start") await start(options);
|
||||
else if (options.command === "status") await status(options);
|
||||
else await stop(options);
|
||||
};
|
||||
|
||||
const isDirectExecution = process.argv[1]
|
||||
? pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(fileURLToPath(import.meta.url)).href
|
||||
: false;
|
||||
if (isDirectExecution) await main();
|
||||
@@ -0,0 +1,178 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, mkdir, rm, stat } from "node:fs/promises";
|
||||
import { createServer } from "node:net";
|
||||
import type { Server } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
assertRuntimeCommandPlatform,
|
||||
createAppLaunchCommand,
|
||||
createCandidateBuildPaths,
|
||||
createRuntimeExecutionContract,
|
||||
createSlotTrustedCertificateDirectory,
|
||||
launchWithPortRetry,
|
||||
reserveLoopbackPort,
|
||||
validateRuntimeExecutionContract,
|
||||
} from "./runtime.mts";
|
||||
|
||||
const openServers: Server[] = [];
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
const listen = async (server: Server, port: number) =>
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(port, "127.0.0.1", resolve);
|
||||
});
|
||||
|
||||
const close = async (server: Server) =>
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
openServers.splice(0).map(async (server) => {
|
||||
if (server.listening) await close(server);
|
||||
}),
|
||||
);
|
||||
await Promise.all(temporaryDirectories.splice(0).map(async (directory) => await rm(directory, { recursive: true })));
|
||||
});
|
||||
|
||||
describe("release-v2 QA slot storage", () => {
|
||||
test("creates a restrictive slot-local trusted certificate directory", async () => {
|
||||
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
|
||||
temporaryDirectories.push(runRoot);
|
||||
const slotDir = path.join(runRoot, "slots", "1");
|
||||
await mkdir(slotDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(runRoot, slotDir);
|
||||
const directoryStats = await stat(trustedCertificatePath);
|
||||
|
||||
expect(trustedCertificatePath).toBe(path.join(slotDir, "trusted-certificates"));
|
||||
expect(directoryStats.isDirectory()).toBe(true);
|
||||
expect(directoryStats.mode & 0o777).toBe(0o700);
|
||||
});
|
||||
|
||||
test("refuses a trusted certificate directory outside the approved run root", async () => {
|
||||
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
|
||||
const outsideRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-outside-"));
|
||||
temporaryDirectories.push(runRoot, outsideRoot);
|
||||
|
||||
await expect(createSlotTrustedCertificateDirectory(runRoot, outsideRoot)).rejects.toThrow(/escapes/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA runtime port startup", () => {
|
||||
test("keeps an allocated loopback port reserved until startup is ready to bind", async () => {
|
||||
const reservation = await reserveLoopbackPort();
|
||||
const contender = createServer();
|
||||
openServers.push(contender);
|
||||
|
||||
await expect(listen(contender, reservation.port)).rejects.toMatchObject({ code: "EADDRINUSE" });
|
||||
await reservation.release();
|
||||
await listen(contender, reservation.port);
|
||||
});
|
||||
|
||||
test("retries with a newly reserved port after an address-in-use collision", async () => {
|
||||
let attempts = 0;
|
||||
const selectedPort = await launchWithPortRetry(async (port) => {
|
||||
attempts += 1;
|
||||
const server = createServer();
|
||||
openServers.push(server);
|
||||
await listen(server, port);
|
||||
|
||||
if (attempts === 1) {
|
||||
const collidedProcess = createServer();
|
||||
openServers.push(collidedProcess);
|
||||
await expect(listen(collidedProcess, port)).rejects.toMatchObject({ code: "EADDRINUSE" });
|
||||
await close(server);
|
||||
return { status: "address-in-use" } as const;
|
||||
}
|
||||
|
||||
await close(server);
|
||||
return { status: "ready", value: port } as const;
|
||||
});
|
||||
|
||||
expect(attempts).toBe(2);
|
||||
expect(selectedPort).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA runtime platform boundary", () => {
|
||||
test.each(["start", "stop"] as const)("rejects %s before dispatch on an unsupported platform", (command) => {
|
||||
expect(() => assertRuntimeCommandPlatform(command, "darwin")).toThrow(/Linux/u);
|
||||
});
|
||||
|
||||
test("allows read-only status on an unsupported platform", () => {
|
||||
expect(() => assertRuntimeCommandPlatform("status", "win32")).not.toThrow();
|
||||
});
|
||||
|
||||
test.each(["start", "status", "stop"] as const)("allows %s on Linux", (command) => {
|
||||
expect(() => assertRuntimeCommandPlatform(command, "linux")).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA app execution contract", () => {
|
||||
test("uses the candidate standalone server while preserving the Linux file-descriptor wrapper", () => {
|
||||
const serverPath = "/tmp/release-v2-build/standalone/apps/nextjs/server.js";
|
||||
const launch = createAppLaunchCommand(serverPath, {
|
||||
platform: "linux",
|
||||
prlimitAvailable: true,
|
||||
nodeExecutable: "/usr/bin/node",
|
||||
});
|
||||
const launchWithoutPrlimit = createAppLaunchCommand(serverPath, {
|
||||
platform: "linux",
|
||||
prlimitAvailable: false,
|
||||
nodeExecutable: "/usr/bin/node",
|
||||
});
|
||||
|
||||
expect(launch).toEqual({
|
||||
command: "/usr/bin/prlimit",
|
||||
arguments: ["--nofile=65536:1048576", "--", "/usr/bin/node", serverPath],
|
||||
});
|
||||
expect(launchWithoutPrlimit).toEqual({ command: "/usr/bin/node", arguments: [serverPath] });
|
||||
});
|
||||
|
||||
test("records and validates the deterministic standalone build settings", () => {
|
||||
const execution = createRuntimeExecutionContract();
|
||||
|
||||
expect(execution).toEqual({
|
||||
runtimeMode: "production-standalone",
|
||||
bundler: "webpack",
|
||||
watcher: null,
|
||||
});
|
||||
expect(validateRuntimeExecutionContract(execution)).toEqual([]);
|
||||
expect(
|
||||
validateRuntimeExecutionContract({
|
||||
runtimeMode: "development",
|
||||
bundler: "turbopack",
|
||||
watcher: { watchpackPollingIntervalMs: 500 },
|
||||
}),
|
||||
).toEqual([
|
||||
"runtime mode must be production-standalone",
|
||||
"bundler must be webpack",
|
||||
"standalone runtimes must not use a filesystem watcher",
|
||||
]);
|
||||
expect(validateRuntimeExecutionContract({ watcher: execution.watcher })).toEqual([
|
||||
"runtime mode must be production-standalone",
|
||||
"bundler must be webpack",
|
||||
]);
|
||||
});
|
||||
|
||||
test("derives one candidate-pinned build shared by every runtime slot", () => {
|
||||
const paths = createCandidateBuildPaths("0123456789abcdef0123456789abcdef01234567");
|
||||
|
||||
expect(paths.distDirName).toBe(".next-qa/release-v2-0123456789abcdef0123456789abcdef01234567");
|
||||
expect(paths.generatedServerPath).toBe(`${paths.buildDir}/standalone/apps/nextjs/server.js`);
|
||||
expect(paths.serverPath).toBe(`${paths.buildDir}/standalone/apps/nextjs/server.cjs`);
|
||||
expect(paths.standaloneRoot).toBe(`${paths.buildDir}/standalone`);
|
||||
expect(paths.standaloneStaticDir).toBe(`${paths.buildDir}/standalone/apps/nextjs/${paths.distDirName}/static`);
|
||||
expect(() => createCandidateBuildPaths("not-a-commit")).toThrow(/40-character lowercase commit SHA/u);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { lstat, open, readFile, realpath, unlink } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const runRootNamePattern = /^homarr-release-v2-qa(?:$|-[A-Za-z0-9][A-Za-z0-9._-]*)$/u;
|
||||
const zoomOutputNamePattern = /^homarr-release-v2-qa-zoom-[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
||||
const ipv6LoopbackHosts = new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
||||
const leaseSchemaVersion = 1;
|
||||
const incompleteLeaseGraceMs = 30_000;
|
||||
|
||||
const missingPath = (error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT";
|
||||
const existingPath = (error: unknown) => (error as NodeJS.ErrnoException).code === "EEXIST";
|
||||
|
||||
export const assertSupportedRuntimePlatform = (platform: NodeJS.Platform = process.platform) => {
|
||||
if (platform !== "linux") {
|
||||
throw new Error(
|
||||
"Release-v2 QA runtime mutations are supported only on Linux because safe process ownership and cleanup require procfs and POSIX process groups",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const isPathWithin = (parent: string, child: string) => {
|
||||
if (!path.isAbsolute(parent) || !path.isAbsolute(child)) return false;
|
||||
const relativePath = path.relative(path.resolve(parent), path.resolve(child));
|
||||
return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
||||
};
|
||||
|
||||
const canonicalTemporaryParent = async () => await realpath(path.resolve(tmpdir()));
|
||||
|
||||
const assertSafeTemporaryChild = async (
|
||||
target: string,
|
||||
options: {
|
||||
allowMissing?: boolean;
|
||||
label: string;
|
||||
namePattern: RegExp;
|
||||
nameRequirement: string;
|
||||
},
|
||||
) => {
|
||||
const resolvedTarget = path.resolve(target);
|
||||
const canonicalParent = await canonicalTemporaryParent();
|
||||
const targetParent = path.dirname(resolvedTarget);
|
||||
let realTargetParent: string;
|
||||
try {
|
||||
realTargetParent = await realpath(targetParent);
|
||||
} catch (error) {
|
||||
if (missingPath(error)) {
|
||||
throw new Error(`${options.label} must be a direct child of the operating system temporary directory`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (realTargetParent !== canonicalParent) {
|
||||
throw new Error(`${options.label} must be a direct child of the operating system temporary directory`);
|
||||
}
|
||||
|
||||
const targetName = path.basename(resolvedTarget);
|
||||
if (!options.namePattern.test(targetName)) {
|
||||
throw new Error(options.nameRequirement);
|
||||
}
|
||||
|
||||
const canonicalTarget = path.join(canonicalParent, targetName);
|
||||
try {
|
||||
const stats = await lstat(canonicalTarget);
|
||||
if (stats.isSymbolicLink()) throw new Error(`${options.label} must not be a symlink`);
|
||||
if (!stats.isDirectory()) throw new Error(`${options.label} must be a directory`);
|
||||
if ((await realpath(canonicalTarget)) !== canonicalTarget) {
|
||||
throw new Error(`${options.label} must not contain symlink components`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (missingPath(error) && options.allowMissing) return canonicalTarget;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return canonicalTarget;
|
||||
};
|
||||
|
||||
export const assertSafeContainedPath = async (parent: string, child: string, label: string) => {
|
||||
const resolvedParent = path.resolve(parent);
|
||||
const resolvedChild = path.resolve(child);
|
||||
if (!isPathWithin(resolvedParent, resolvedChild)) {
|
||||
throw new Error(`${label} escapes its approved parent`);
|
||||
}
|
||||
|
||||
const realParent = await realpath(resolvedParent);
|
||||
if (realParent !== resolvedParent) {
|
||||
throw new Error(`${label} parent contains a symlink`);
|
||||
}
|
||||
|
||||
const relativePath = path.relative(resolvedParent, resolvedChild);
|
||||
let currentPath = resolvedParent;
|
||||
for (const segment of relativePath.split(path.sep).filter(Boolean)) {
|
||||
currentPath = path.join(currentPath, segment);
|
||||
try {
|
||||
const stats = await lstat(currentPath);
|
||||
if (stats.isSymbolicLink()) throw new Error(`${label} contains a symlink`);
|
||||
} catch (error) {
|
||||
if (missingPath(error)) break;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedChild;
|
||||
};
|
||||
|
||||
export const assertSafeRunRoot = async (runRoot: string, options: { allowMissing?: boolean } = {}) =>
|
||||
await assertSafeTemporaryChild(runRoot, {
|
||||
allowMissing: options.allowMissing,
|
||||
label: "The QA run-root",
|
||||
namePattern: runRootNamePattern,
|
||||
nameRequirement:
|
||||
'The QA run-root basename must start with "homarr-release-v2-qa" and use only safe suffix characters',
|
||||
});
|
||||
|
||||
export const assertSafeZoomOutput = async (outputDirectory: string, options: { allowMissing?: boolean } = {}) =>
|
||||
await assertSafeTemporaryChild(outputDirectory, {
|
||||
allowMissing: options.allowMissing,
|
||||
label: "The zoom extension output",
|
||||
namePattern: zoomOutputNamePattern,
|
||||
nameRequirement:
|
||||
'The zoom extension output basename must start with "homarr-release-v2-qa-zoom-" and use only safe suffix characters',
|
||||
});
|
||||
|
||||
const readLinuxProcessStartMarker = async (pid: number) => {
|
||||
if (process.platform !== "linux") return null;
|
||||
try {
|
||||
const processStat = await readFile(`/proc/${pid}/stat`, "utf8");
|
||||
const commandEnd = processStat.lastIndexOf(")");
|
||||
if (commandEnd < 0) return null;
|
||||
const fieldsAfterCommand = processStat
|
||||
.slice(commandEnd + 2)
|
||||
.trim()
|
||||
.split(/\s+/u);
|
||||
const startTime = fieldsAfterCommand[19];
|
||||
return startTime ? `linux:${startTime}` : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isProcessRunning = (pid: number) => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
interface SlotLeaseOwner {
|
||||
schemaVersion: 1;
|
||||
pid: number;
|
||||
processStartMarker: string | null;
|
||||
token: string;
|
||||
runRoot: string;
|
||||
slot: number;
|
||||
operation: string;
|
||||
acquiredAt: string;
|
||||
}
|
||||
|
||||
interface LeaseSnapshot {
|
||||
device: bigint;
|
||||
inode: bigint;
|
||||
modifiedMs: number;
|
||||
content: string;
|
||||
owner: SlotLeaseOwner | null;
|
||||
}
|
||||
|
||||
export interface SlotLease {
|
||||
path: string;
|
||||
owner: SlotLeaseOwner;
|
||||
release: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const getSlotLeasePath = async (runRoot: string, slot: number) => {
|
||||
const canonicalRunRoot = await assertSafeRunRoot(runRoot, { allowMissing: true });
|
||||
const canonicalParent = await canonicalTemporaryParent();
|
||||
return path.join(canonicalParent, `.${path.basename(canonicalRunRoot)}.slot-${slot}.lease`);
|
||||
};
|
||||
|
||||
export const getBuildLeasePath = async (runRoot: string) => {
|
||||
const canonicalRunRoot = await assertSafeRunRoot(runRoot, { allowMissing: true });
|
||||
const canonicalParent = await canonicalTemporaryParent();
|
||||
return path.join(canonicalParent, `.${path.basename(canonicalRunRoot)}.candidate-build.lease`);
|
||||
};
|
||||
|
||||
const readLeaseSnapshot = async (leasePath: string): Promise<LeaseSnapshot> => {
|
||||
const stats = await lstat(leasePath, { bigint: true });
|
||||
if (stats.isSymbolicLink() || !stats.isFile()) {
|
||||
throw new Error(`Refusing unsafe QA slot lease path: ${leasePath}`);
|
||||
}
|
||||
const content = await readFile(leasePath, "utf8");
|
||||
let owner: SlotLeaseOwner | null = null;
|
||||
try {
|
||||
owner = JSON.parse(content) as SlotLeaseOwner;
|
||||
} catch {
|
||||
// An acquiring process may have created the file but not completed its marker write yet.
|
||||
}
|
||||
return {
|
||||
device: stats.dev,
|
||||
inode: stats.ino,
|
||||
modifiedMs: Number(stats.mtimeMs),
|
||||
content,
|
||||
owner,
|
||||
};
|
||||
};
|
||||
|
||||
const isValidLeaseOwner = (owner: SlotLeaseOwner | null, runRoot: string, slot: number): owner is SlotLeaseOwner =>
|
||||
owner?.schemaVersion === leaseSchemaVersion &&
|
||||
Number.isInteger(owner.pid) &&
|
||||
owner.pid > 0 &&
|
||||
typeof owner.token === "string" &&
|
||||
owner.token.length > 0 &&
|
||||
path.resolve(owner.runRoot) === runRoot &&
|
||||
owner.slot === slot;
|
||||
|
||||
const processMatchesLeaseOwner = async (owner: SlotLeaseOwner) => {
|
||||
if (!isProcessRunning(owner.pid)) return false;
|
||||
const observedStartMarker = await readLinuxProcessStartMarker(owner.pid);
|
||||
if (owner.processStartMarker && observedStartMarker) {
|
||||
return owner.processStartMarker === observedStartMarker;
|
||||
}
|
||||
// If the platform cannot verify process start identity, preserve the live lock rather than
|
||||
// risking recovery over an active owner.
|
||||
return true;
|
||||
};
|
||||
|
||||
const sameLeaseSnapshot = (left: LeaseSnapshot, right: LeaseSnapshot) =>
|
||||
left.device === right.device &&
|
||||
left.inode === right.inode &&
|
||||
left.modifiedMs === right.modifiedMs &&
|
||||
left.content === right.content;
|
||||
|
||||
const recoverStaleLease = async (leasePath: string, snapshot: LeaseSnapshot) => {
|
||||
const confirmation = await readLeaseSnapshot(leasePath);
|
||||
if (!sameLeaseSnapshot(snapshot, confirmation)) {
|
||||
throw new Error(`QA slot lease changed during stale recovery: ${leasePath}`);
|
||||
}
|
||||
await unlink(leasePath);
|
||||
};
|
||||
|
||||
const acquireLease = async (
|
||||
runRoot: string,
|
||||
slot: number,
|
||||
operation: string,
|
||||
leasePath: string,
|
||||
resourceLabel: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<SlotLease> => {
|
||||
assertSupportedRuntimePlatform(platform);
|
||||
const canonicalRunRoot = await assertSafeRunRoot(runRoot, { allowMissing: true });
|
||||
const owner: SlotLeaseOwner = {
|
||||
schemaVersion: leaseSchemaVersion,
|
||||
pid: process.pid,
|
||||
processStartMarker: await readLinuxProcessStartMarker(process.pid),
|
||||
token: randomUUID(),
|
||||
runRoot: canonicalRunRoot,
|
||||
slot,
|
||||
operation,
|
||||
acquiredAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
const handle = await open(leasePath, "wx", 0o600);
|
||||
try {
|
||||
await handle.writeFile(`${JSON.stringify(owner)}\n`, "utf8");
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
|
||||
return {
|
||||
path: leasePath,
|
||||
owner,
|
||||
release: async () => {
|
||||
const snapshot = await readLeaseSnapshot(leasePath);
|
||||
if (!isValidLeaseOwner(snapshot.owner, canonicalRunRoot, slot) || snapshot.owner?.token !== owner.token) {
|
||||
throw new Error(`Refusing to release a QA ${resourceLabel} lease owned by another process: ${leasePath}`);
|
||||
}
|
||||
await recoverStaleLease(leasePath, snapshot);
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (!existingPath(error)) throw error;
|
||||
const snapshot = await readLeaseSnapshot(leasePath);
|
||||
const existingOwner = snapshot.owner;
|
||||
if (!isValidLeaseOwner(existingOwner, canonicalRunRoot, slot)) {
|
||||
if (Date.now() - snapshot.modifiedMs < incompleteLeaseGraceMs) {
|
||||
throw new Error(`QA ${resourceLabel} lease is being initialized by another operation`, { cause: error });
|
||||
}
|
||||
await recoverStaleLease(leasePath, snapshot);
|
||||
continue;
|
||||
}
|
||||
if (await processMatchesLeaseOwner(existingOwner)) {
|
||||
throw new Error(
|
||||
`QA ${resourceLabel} is busy with ${existingOwner.operation} owned by PID ${existingOwner.pid}`,
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
);
|
||||
}
|
||||
await recoverStaleLease(leasePath, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to acquire QA ${resourceLabel} lease after stale recovery`);
|
||||
};
|
||||
|
||||
export const acquireSlotLease = async (
|
||||
runRoot: string,
|
||||
slot: number,
|
||||
operation: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
) => {
|
||||
if (![1, 2, 3].includes(slot)) throw new Error("QA slot lease requires slot 1, 2, or 3");
|
||||
const leasePath = await getSlotLeasePath(runRoot, slot);
|
||||
return await acquireLease(runRoot, slot, operation, leasePath, `slot ${slot}`, platform);
|
||||
};
|
||||
|
||||
export const acquireBuildLease = async (
|
||||
runRoot: string,
|
||||
operation: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
) => {
|
||||
const leasePath = await getBuildLeasePath(runRoot);
|
||||
return await acquireLease(runRoot, 0, operation, leasePath, "candidate build", platform);
|
||||
};
|
||||
|
||||
const normalizedHost = (host: string) => {
|
||||
const lowerHost = host.toLowerCase();
|
||||
if (lowerHost.startsWith("[") && lowerHost.endsWith("]")) return lowerHost.slice(1, -1);
|
||||
return lowerHost;
|
||||
};
|
||||
|
||||
export const validateLoopbackHost = (host: string) => {
|
||||
const normalized = normalizedHost(host);
|
||||
if (normalized !== "127.0.0.1" && normalized !== "localhost" && !ipv6LoopbackHosts.has(normalized)) {
|
||||
throw new Error("The fixture host must be an explicit loopback address");
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const validateFixtureUrl = (value: string) => {
|
||||
let fixtureUrl: URL;
|
||||
try {
|
||||
fixtureUrl = new URL(value);
|
||||
} catch {
|
||||
throw new Error("The fixture URL must be a valid loopback HTTP origin");
|
||||
}
|
||||
|
||||
if (
|
||||
fixtureUrl.protocol !== "http:" ||
|
||||
fixtureUrl.username ||
|
||||
fixtureUrl.password ||
|
||||
fixtureUrl.pathname !== "/" ||
|
||||
fixtureUrl.search ||
|
||||
fixtureUrl.hash
|
||||
) {
|
||||
throw new Error("The fixture URL must be a credential-free loopback HTTP origin");
|
||||
}
|
||||
try {
|
||||
validateLoopbackHost(fixtureUrl.hostname);
|
||||
} catch {
|
||||
throw new Error("The fixture URL must be a credential-free loopback HTTP origin");
|
||||
}
|
||||
return fixtureUrl.origin;
|
||||
};
|
||||
|
||||
export const sanitizeAppEnvironment = (environment: NodeJS.ProcessEnv) => {
|
||||
const sanitized = { ...environment };
|
||||
delete sanitized.QA_PASSWORD;
|
||||
delete sanitized.QA_PASSWORD_FILE;
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
export const createFixtureEnvironment = (runId: string): NodeJS.ProcessEnv => ({ QA_RUN_ID: runId });
|
||||
@@ -0,0 +1,245 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdir, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
acquireBuildLease,
|
||||
acquireSlotLease,
|
||||
assertSafeContainedPath,
|
||||
assertSafeRunRoot,
|
||||
assertSupportedRuntimePlatform,
|
||||
createFixtureEnvironment,
|
||||
getSlotLeasePath,
|
||||
isPathWithin,
|
||||
sanitizeAppEnvironment,
|
||||
validateFixtureUrl,
|
||||
validateLoopbackHost,
|
||||
} from "./safety.mts";
|
||||
|
||||
const temporaryPaths: string[] = [];
|
||||
const uniqueName = (suffix: string) => `homarr-release-v2-qa-test-${process.pid}-${Date.now()}-${suffix}`;
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryPaths.splice(0).map((target) => rm(target, { force: true, recursive: true })));
|
||||
});
|
||||
|
||||
describe("release-v2 QA harness safety", () => {
|
||||
test("supports runtime mutations only on Linux", () => {
|
||||
expect(() => assertSupportedRuntimePlatform("linux")).not.toThrow();
|
||||
expect(() => assertSupportedRuntimePlatform("darwin")).toThrow(/Linux/u);
|
||||
expect(() => assertSupportedRuntimePlatform("win32")).toThrow(/Linux/u);
|
||||
});
|
||||
|
||||
test("rejects an unsupported platform before creating a slot lease", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("unsupported-platform"));
|
||||
const leasePath = await getSlotLeasePath(runRoot, 1);
|
||||
temporaryPaths.push(leasePath);
|
||||
|
||||
await expect(acquireSlotLease(runRoot, 1, "unsupported-start", "darwin")).rejects.toThrow(/Linux/u);
|
||||
await expect(stat(leasePath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("accepts only direct, predictably named children of the temporary directory", async () => {
|
||||
const safeRoot = path.join(tmpdir(), uniqueName("safe"));
|
||||
temporaryPaths.push(safeRoot);
|
||||
|
||||
await expect(assertSafeRunRoot(safeRoot, { allowMissing: true })).resolves.toBe(path.resolve(safeRoot));
|
||||
await expect(
|
||||
assertSafeRunRoot(path.join(tmpdir(), "unrelated-homarr-release-v2-qa"), { allowMissing: true }),
|
||||
).rejects.toThrow("must start with");
|
||||
await expect(
|
||||
assertSafeRunRoot(path.join(tmpdir(), "homarr-release-v2-qaevil"), { allowMissing: true }),
|
||||
).rejects.toThrow("must start with");
|
||||
await expect(
|
||||
assertSafeRunRoot(path.join(tmpdir(), "nested", uniqueName("nested")), { allowMissing: true }),
|
||||
).rejects.toThrow("direct child");
|
||||
await expect(
|
||||
assertSafeRunRoot(path.join(tmpdir(), "..", "var", "tmp", uniqueName("traversal")), { allowMissing: true }),
|
||||
).rejects.toThrow("direct child");
|
||||
});
|
||||
|
||||
test("canonicalizes a temporary-directory alias without widening containment", async () => {
|
||||
const temporaryAlias = path.join(tmpdir(), uniqueName("temporary-alias"));
|
||||
const runRootName = uniqueName("canonical-root");
|
||||
temporaryPaths.push(temporaryAlias);
|
||||
await symlink(tmpdir(), temporaryAlias, "dir");
|
||||
|
||||
await expect(assertSafeRunRoot(path.join(temporaryAlias, runRootName), { allowMissing: true })).resolves.toBe(
|
||||
path.join(await realpath(tmpdir()), runRootName),
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects a symlink run-root", async () => {
|
||||
const target = path.join(tmpdir(), uniqueName("target"));
|
||||
const linkedRoot = path.join(tmpdir(), uniqueName("linked"));
|
||||
temporaryPaths.push(linkedRoot, target);
|
||||
await mkdir(target);
|
||||
await symlink(target, linkedRoot);
|
||||
|
||||
await expect(assertSafeRunRoot(linkedRoot)).rejects.toThrow("symlink");
|
||||
});
|
||||
|
||||
test("rejects symlink components in a contained reset target", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("component-root"));
|
||||
const externalTarget = path.join(tmpdir(), uniqueName("component-target"));
|
||||
temporaryPaths.push(runRoot, externalTarget);
|
||||
await mkdir(runRoot);
|
||||
await mkdir(externalTarget);
|
||||
await symlink(externalTarget, path.join(runRoot, "slots"));
|
||||
|
||||
await expect(
|
||||
assertSafeContainedPath(runRoot, path.join(runRoot, "slots", "1"), "QA slot reset target"),
|
||||
).rejects.toThrow("symlink");
|
||||
});
|
||||
|
||||
test("catches a symlink swapped in after an earlier missing-target check", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("swap-root"));
|
||||
const externalTarget = path.join(tmpdir(), uniqueName("swap-target"));
|
||||
const slotPath = path.join(runRoot, "slots", "1");
|
||||
temporaryPaths.push(runRoot, externalTarget);
|
||||
await mkdir(path.dirname(slotPath), { recursive: true });
|
||||
await mkdir(externalTarget);
|
||||
|
||||
await expect(assertSafeContainedPath(runRoot, slotPath, "QA slot mutation target")).resolves.toBe(slotPath);
|
||||
await symlink(externalTarget, slotPath);
|
||||
await expect(assertSafeContainedPath(runRoot, slotPath, "QA slot mutation target")).rejects.toThrow("symlink");
|
||||
});
|
||||
|
||||
test("rejects a concurrent operation while a live slot lease is held", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("live-lease"));
|
||||
const lease = await acquireSlotLease(runRoot, 1, "first-start");
|
||||
temporaryPaths.push(lease.path);
|
||||
|
||||
await expect(acquireSlotLease(runRoot, 1, "second-start")).rejects.toThrow(/slot 1 is busy.*first-start/u);
|
||||
await lease.release();
|
||||
});
|
||||
|
||||
test("serializes the shared candidate build independently of slot leases", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("build-lease"));
|
||||
const buildLease = await acquireBuildLease(runRoot, "first-build");
|
||||
const slotLease = await acquireSlotLease(runRoot, 1, "slot-start");
|
||||
temporaryPaths.push(buildLease.path, slotLease.path);
|
||||
|
||||
await expect(acquireBuildLease(runRoot, "second-build")).rejects.toThrow(/candidate build is busy.*first-build/u);
|
||||
await buildLease.release();
|
||||
await slotLease.release();
|
||||
});
|
||||
|
||||
test("recovers a stale lease with a dead owner PID", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("stale-lease"));
|
||||
const leasePath = await getSlotLeasePath(runRoot, 2);
|
||||
temporaryPaths.push(leasePath);
|
||||
await writeFile(
|
||||
leasePath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
pid: 2_147_483_647,
|
||||
processStartMarker: "linux:stale",
|
||||
token: "stale-token",
|
||||
runRoot: path.resolve(runRoot),
|
||||
slot: 2,
|
||||
operation: "stale-start",
|
||||
acquiredAt: "2026-01-01T00:00:00.000Z",
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
const lease = await acquireSlotLease(runRoot, 2, "replacement-start");
|
||||
expect(lease.owner.operation).toBe("replacement-start");
|
||||
await lease.release();
|
||||
});
|
||||
|
||||
test("recovers a PID-reuse lease marker without signaling the unrelated process", async () => {
|
||||
const runRoot = path.join(tmpdir(), uniqueName("pid-reuse-lease"));
|
||||
const leasePath = await getSlotLeasePath(runRoot, 3);
|
||||
temporaryPaths.push(leasePath);
|
||||
await writeFile(
|
||||
leasePath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
pid: process.pid,
|
||||
processStartMarker: "linux:not-this-process",
|
||||
token: "reused-pid-token",
|
||||
runRoot: path.resolve(runRoot),
|
||||
slot: 3,
|
||||
operation: "abandoned-start",
|
||||
acquiredAt: "2026-01-01T00:00:00.000Z",
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
const lease = await acquireSlotLease(runRoot, 3, "safe-recovery");
|
||||
expect(process.pid).toBeGreaterThan(0);
|
||||
await lease.release();
|
||||
});
|
||||
|
||||
test("uses path segments rather than string prefixes for containment", () => {
|
||||
const parent = path.join(tmpdir(), "qa-artifacts", "packet-1");
|
||||
expect(isPathWithin(parent, path.join(parent, "screen.png"))).toBe(true);
|
||||
expect(isPathWithin(parent, `${parent}-spoof/screen.png`)).toBe(false);
|
||||
expect(isPathWithin(parent, path.join(parent, "..", "packet-2", "screen.png"))).toBe(false);
|
||||
expect(isPathWithin(parent, ".screenshots/release-v2/packet-1/screen.png")).toBe(false);
|
||||
});
|
||||
|
||||
test.each(["http://127.0.0.1:3210", "http://localhost:3210", "http://[::1]:3210", "http://[0:0:0:0:0:0:0:1]:3210"])(
|
||||
"accepts a credential-free loopback fixture origin: %s",
|
||||
(fixtureUrl) => {
|
||||
expect(validateFixtureUrl(fixtureUrl)).toBe(new URL(fixtureUrl).origin);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
"https://127.0.0.1:3210",
|
||||
"http://user:secret@127.0.0.1:3210",
|
||||
"http://0.0.0.0:3210",
|
||||
"http://192.0.2.1:3210",
|
||||
"http://localhost.example:3210",
|
||||
"http://127.0.0.1:3210/path",
|
||||
])("rejects an unsafe fixture URL without echoing it: %s", (fixtureUrl) => {
|
||||
expect(() => validateFixtureUrl(fixtureUrl)).toThrow("fixture URL");
|
||||
});
|
||||
|
||||
test.each(["127.0.0.1", "localhost", "::1", "[::1]", "0:0:0:0:0:0:0:1"])(
|
||||
"accepts the loopback fixture bind host %s",
|
||||
(host) => {
|
||||
expect(validateLoopbackHost(host)).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
test.each(["0.0.0.0", "::", "192.0.2.1", "example.test"])("rejects the non-loopback bind host %s", (host) => {
|
||||
expect(() => validateLoopbackHost(host)).toThrow("loopback");
|
||||
});
|
||||
|
||||
test("removes both QA password inputs from the spawned app environment", () => {
|
||||
const environment = sanitizeAppEnvironment({
|
||||
QA_PASSWORD: "do-not-forward",
|
||||
QA_PASSWORD_FILE: "/tmp/do-not-forward",
|
||||
QA_RUN_ID: "qa-run",
|
||||
});
|
||||
|
||||
expect(environment).toEqual({ QA_RUN_ID: "qa-run" });
|
||||
expect("QA_PASSWORD" in environment).toBe(false);
|
||||
expect("QA_PASSWORD_FILE" in environment).toBe(false);
|
||||
});
|
||||
|
||||
test("uses an explicit fixture environment allowlist", () => {
|
||||
const environment = createFixtureEnvironment("qa-run");
|
||||
|
||||
expect(environment).toEqual({ QA_RUN_ID: "qa-run" });
|
||||
for (const forbiddenName of [
|
||||
"QA_PASSWORD",
|
||||
"QA_PASSWORD_FILE",
|
||||
"DB_URL",
|
||||
"AUTH_SECRET",
|
||||
"API_KEY",
|
||||
"SECRET_ENCRYPTION_KEY",
|
||||
"HOME",
|
||||
"PATH",
|
||||
]) {
|
||||
expect(forbiddenName in environment).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,172 @@
|
||||
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { assertSafeZoomOutput } from "./safety.mts";
|
||||
|
||||
const allowedFactorStrings = new Set(["0.8", "1", "1.25", "2"]);
|
||||
|
||||
interface ZoomOptions {
|
||||
origin: string;
|
||||
factor: number;
|
||||
outputDirectory: string;
|
||||
}
|
||||
|
||||
const usage = () => {
|
||||
console.log("Usage: zoom.mts --origin <http(s)-origin> --factor <0.8|1|1.25|2> --output <new-absolute-directory>");
|
||||
};
|
||||
|
||||
const validateOrigin = (value: string) => {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new Error("--origin must be a valid HTTP(S) origin");
|
||||
}
|
||||
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
throw new Error("--origin must use http: or https:");
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
throw new Error("--origin must not contain credentials");
|
||||
}
|
||||
if (url.pathname !== "/" || url.search || url.hash) {
|
||||
throw new Error("--origin must not contain a path, query, or fragment");
|
||||
}
|
||||
|
||||
return url.origin;
|
||||
};
|
||||
|
||||
const validateOutputDirectory = async (value: string) => {
|
||||
if (!path.isAbsolute(value)) {
|
||||
throw new Error("--output must be an absolute path to a new directory");
|
||||
}
|
||||
|
||||
const outputDirectory = await assertSafeZoomOutput(value, { allowMissing: true });
|
||||
|
||||
let outputExists = true;
|
||||
try {
|
||||
await lstat(outputDirectory);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
outputExists = false;
|
||||
}
|
||||
if (outputExists) throw new Error(`--output must not already exist: ${outputDirectory}`);
|
||||
return outputDirectory;
|
||||
};
|
||||
|
||||
export const parseZoomOptions = async (rawArguments: string[]): Promise<ZoomOptions> => {
|
||||
const args = rawArguments.filter((argument) => argument !== "--");
|
||||
let origin: string | undefined;
|
||||
let factorString: string | undefined;
|
||||
let output: string | undefined;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const argument = args[index];
|
||||
const value = args[index + 1];
|
||||
if (argument === "--origin" && value) {
|
||||
origin = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--factor" && value) {
|
||||
factorString = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--output" && value) {
|
||||
output = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown or incomplete argument: ${argument ?? "<missing>"}`);
|
||||
}
|
||||
|
||||
if (!origin) throw new Error("--origin is required");
|
||||
if (!factorString) throw new Error("--factor is required");
|
||||
if (!allowedFactorStrings.has(factorString)) {
|
||||
throw new Error("--factor must be one of 0.8, 1, 1.25, or 2");
|
||||
}
|
||||
if (!output) throw new Error("--output is required");
|
||||
|
||||
return {
|
||||
origin: validateOrigin(origin),
|
||||
factor: Number(factorString),
|
||||
outputDirectory: await validateOutputDirectory(output),
|
||||
};
|
||||
};
|
||||
|
||||
export const createZoomExtensionContent = (origin: string, factor: number) => {
|
||||
const manifest = {
|
||||
manifest_version: 3,
|
||||
name: "Homarr release v2 native zoom",
|
||||
version: "1.0.0",
|
||||
permissions: ["tabs"],
|
||||
host_permissions: [`${origin}/*`],
|
||||
background: { service_worker: "background.js" },
|
||||
};
|
||||
const background = `const TARGET_ORIGIN = ${JSON.stringify(origin)};
|
||||
const ZOOM_FACTOR = ${JSON.stringify(factor)};
|
||||
|
||||
function applyZoom(tab) {
|
||||
if (tab?.id == null || !tab.url) return;
|
||||
|
||||
let origin;
|
||||
try {
|
||||
origin = new URL(tab.url).origin;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (origin !== TARGET_ORIGIN) return;
|
||||
|
||||
chrome.tabs.setZoom(tab.id, ZOOM_FACTOR, () => {
|
||||
void chrome.runtime.lastError;
|
||||
});
|
||||
}
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status !== "complete") return;
|
||||
applyZoom({ ...tab, id: tabId });
|
||||
});
|
||||
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach(applyZoom);
|
||||
});
|
||||
`;
|
||||
|
||||
return {
|
||||
manifest: `${JSON.stringify(manifest, null, 2)}\n`,
|
||||
background,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateZoomExtension = async (options: ZoomOptions) => {
|
||||
const content = createZoomExtensionContent(options.origin, options.factor);
|
||||
await assertSafeZoomOutput(options.outputDirectory, { allowMissing: true });
|
||||
await mkdir(options.outputDirectory, { mode: 0o700 });
|
||||
await assertSafeZoomOutput(options.outputDirectory);
|
||||
await Promise.all([
|
||||
writeFile(path.join(options.outputDirectory, "manifest.json"), content.manifest, { flag: "wx", mode: 0o600 }),
|
||||
writeFile(path.join(options.outputDirectory, "background.js"), content.background, { flag: "wx", mode: 0o600 }),
|
||||
]);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const rawArguments = process.argv.slice(2);
|
||||
if (rawArguments.includes("--help") || rawArguments.includes("-h")) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = await parseZoomOptions(rawArguments);
|
||||
await generateZoomExtension(options);
|
||||
console.log(`Generated release-v2 native zoom extension`);
|
||||
console.log(`Origin: ${options.origin}`);
|
||||
console.log(`Factor: ${options.factor}`);
|
||||
console.log(`Output: ${options.outputDirectory}`);
|
||||
};
|
||||
|
||||
const isDirectExecution = process.argv[1]
|
||||
? pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(fileURLToPath(import.meta.url)).href
|
||||
: false;
|
||||
if (isDirectExecution) await main();
|
||||
@@ -0,0 +1,155 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdir, readFile, rm, symlink } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { createZoomExtensionContent, generateZoomExtension, parseZoomOptions } from "./zoom.mts";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
const createOutputPath = (suffix: string) => {
|
||||
const directory = path.join(
|
||||
tmpdir(),
|
||||
`homarr-release-v2-qa-zoom-${process.pid}-${Date.now()}-${suffix}-${temporaryDirectories.length}`,
|
||||
);
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
||||
});
|
||||
|
||||
describe("release-v2 native zoom helper", () => {
|
||||
test("generates deterministic, exact-origin MV3 extension files", async () => {
|
||||
const firstOutput = createOutputPath("first");
|
||||
const secondOutput = createOutputPath("second");
|
||||
const firstOptions = await parseZoomOptions([
|
||||
"--origin",
|
||||
"http://127.0.0.1:34401/",
|
||||
"--factor",
|
||||
"1.25",
|
||||
"--output",
|
||||
firstOutput,
|
||||
]);
|
||||
const secondOptions = await parseZoomOptions([
|
||||
"--origin",
|
||||
"http://127.0.0.1:34401",
|
||||
"--factor",
|
||||
"1.25",
|
||||
"--output",
|
||||
secondOutput,
|
||||
]);
|
||||
|
||||
await generateZoomExtension(firstOptions);
|
||||
await generateZoomExtension(secondOptions);
|
||||
|
||||
const firstManifest = await readFile(path.join(firstOutput, "manifest.json"), "utf8");
|
||||
const firstBackground = await readFile(path.join(firstOutput, "background.js"), "utf8");
|
||||
expect(firstManifest).toBe(await readFile(path.join(secondOutput, "manifest.json"), "utf8"));
|
||||
expect(firstBackground).toBe(await readFile(path.join(secondOutput, "background.js"), "utf8"));
|
||||
expect(JSON.parse(firstManifest)).toMatchObject({
|
||||
manifest_version: 3,
|
||||
permissions: ["tabs"],
|
||||
host_permissions: ["http://127.0.0.1:34401/*"],
|
||||
background: { service_worker: "background.js" },
|
||||
});
|
||||
expect(firstBackground).toContain('const TARGET_ORIGIN = "http://127.0.0.1:34401";');
|
||||
expect(firstBackground).toContain("const ZOOM_FACTOR = 1.25;");
|
||||
expect(firstBackground).toContain("origin !== TARGET_ORIGIN");
|
||||
expect(firstBackground).not.toContain("password");
|
||||
});
|
||||
|
||||
test.each(["0.8", "1", "1.25", "2"])("accepts the supported factor %s", async (factor) => {
|
||||
const output = createOutputPath(`factor-${factor.replace(".", "-")}`);
|
||||
const options = await parseZoomOptions([
|
||||
"--origin",
|
||||
"https://qa.example.test:8443",
|
||||
"--factor",
|
||||
factor,
|
||||
"--output",
|
||||
output,
|
||||
]);
|
||||
|
||||
expect(options.factor).toBe(Number(factor));
|
||||
});
|
||||
|
||||
test.each(["0", "0.9", "1.0", "1.5", "2.1", "not-a-number"])("rejects unsupported factor %s", async (factor) => {
|
||||
const output = createOutputPath(`invalid-factor-${factor.replaceAll(/[^A-Za-z0-9]/gu, "-")}`);
|
||||
await expect(
|
||||
parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", factor, "--output", output]),
|
||||
).rejects.toThrow("--factor must be one of 0.8, 1, 1.25, or 2");
|
||||
});
|
||||
|
||||
test.each([
|
||||
"ftp://127.0.0.1:34401",
|
||||
"http://user:secret@127.0.0.1:34401",
|
||||
"http://127.0.0.1:34401/boards/qa-grid-24",
|
||||
"http://127.0.0.1:34401/?mode=qa",
|
||||
"not-a-url",
|
||||
])("rejects a non-origin target without echoing it: %s", async (origin) => {
|
||||
const output = createOutputPath("invalid-origin");
|
||||
await expect(parseZoomOptions(["--origin", origin, "--factor", "1", "--output", output])).rejects.toThrow(
|
||||
/--origin/u,
|
||||
);
|
||||
});
|
||||
|
||||
test("requires a new, predictably named direct child of the temporary directory", async () => {
|
||||
const existingDirectory = createOutputPath("existing");
|
||||
const nestedParent = createOutputPath("nested-parent");
|
||||
await mkdir(existingDirectory);
|
||||
await mkdir(nestedParent);
|
||||
await expect(
|
||||
parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", "1", "--output", "relative-extension"]),
|
||||
).rejects.toThrow("--output must be an absolute path");
|
||||
await expect(
|
||||
parseZoomOptions(["--origin", "http://127.0.0.1:34401", "--factor", "1", "--output", existingDirectory]),
|
||||
).rejects.toThrow("--output must not already exist");
|
||||
await expect(
|
||||
parseZoomOptions([
|
||||
"--origin",
|
||||
"http://127.0.0.1:34401",
|
||||
"--factor",
|
||||
"1",
|
||||
"--output",
|
||||
path.join(nestedParent, "homarr-release-v2-qa-zoom-nested"),
|
||||
]),
|
||||
).rejects.toThrow("direct child");
|
||||
await expect(
|
||||
parseZoomOptions([
|
||||
"--origin",
|
||||
"http://127.0.0.1:34401",
|
||||
"--factor",
|
||||
"1",
|
||||
"--output",
|
||||
path.join(tmpdir(), "uncontrolled-extension"),
|
||||
]),
|
||||
).rejects.toThrow("basename must start");
|
||||
});
|
||||
|
||||
test("canonicalizes a temporary-directory alias for extension output", async () => {
|
||||
const temporaryAlias = createOutputPath("alias-parent");
|
||||
await symlink(tmpdir(), temporaryAlias, "dir");
|
||||
const outputName = `homarr-release-v2-qa-zoom-${process.pid}-${Date.now()}-through-alias`;
|
||||
const canonicalOutput = path.join(tmpdir(), outputName);
|
||||
temporaryDirectories.push(canonicalOutput);
|
||||
|
||||
const options = await parseZoomOptions([
|
||||
"--origin",
|
||||
"http://127.0.0.1:34401",
|
||||
"--factor",
|
||||
"1",
|
||||
"--output",
|
||||
path.join(temporaryAlias, outputName),
|
||||
]);
|
||||
|
||||
expect(options.outputDirectory).toBe(canonicalOutput);
|
||||
});
|
||||
|
||||
test("renders baseline factor 1 without changing the extension shape", () => {
|
||||
const content = createZoomExtensionContent("https://qa.example.test", 1);
|
||||
expect(content.background).toContain("const ZOOM_FACTOR = 1;");
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,20 @@ export default defineConfig({
|
||||
include: ["packages/docker/**/*.spec.{ts,tsx}"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "translation-node",
|
||||
environment: "node",
|
||||
setupFiles: ["./vitest.setup.ts", "./vitest.setup.node.ts"],
|
||||
include: ["packages/translation/**/*.spec.ts"],
|
||||
server: {
|
||||
deps: {
|
||||
inline: ["next-intl"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
@@ -76,6 +90,7 @@ export default defineConfig({
|
||||
"packages/db/**",
|
||||
"packages/docker/**",
|
||||
"packages/request-handler/**",
|
||||
"packages/translation/**",
|
||||
"e2e/**",
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user