fix: resolve admin issue review findings and record verification

This commit is contained in:
Codex
2026-09-12 18:24:23 +02:00
parent c7e5f295e6
commit 2d1b714ebe
10 changed files with 85 additions and 46 deletions
+8
View File
@@ -14,6 +14,14 @@ requirements as mandatory; do not replace the running server stack in place.
## Current product shape
Gitea #28–#31 follow-up is implemented in this checkout: Workspace's four tabs,
Database list-first entry without the preparation footer, full-height Pi instructions
with installation-host OS selection, and short Admin navigation labels. Core and
session behavior are retained. These follow-up changes are **not yet rebuilt into
local Docker**; see `docs/reports/2026-09-12-admin-issues-28-31.md` for test/review results.
The host CLI must regenerate its Compose projection on the destination installation
to identify the physical host OS correctly when the backend runs in Docker.
The latest context-shelf A and five Administration pages are implemented locally.
At the owner's request, local Docker project `thothii-18998cca7b0a` was rebuilt and
its core/frontend recreated on 2026-09-12. The real UI at `http://127.0.0.1:8080`
@@ -17,4 +17,11 @@ same-document integration boundary while leaving room for a future path adapter.
The accepted visual direction is A / Workbench for all five surfaces; B and C remain recoverable
prototypes. Workspace owns readiness and preprocessing, consuming the Database catalog as a
prerequisite. Database owns connection/binding, schema synchronization, descriptions and sensitivity;
its page links to the related Workspace's preparation rather than duplicating the preprocessing action.
the preprocessing action remains exclusively in Workspace preparation.
On 2026-09-12, the owner refined this direction in
[Gitea #29](https://git.tylconsulting.it/mptyl/ThothII/issues/29): Database opens at its
catalog list, with its original table space and margins, and no Workspace preparation footer.
This supersedes the earlier requirement for a preparation link in that page. Workspace
remains reachable through the shared Administration navigation; its Preparation tab still
links to Database configuration and schema when catalog work is needed.
+36 -1
View File
@@ -38,5 +38,40 @@ the existing public Pi service and host projection contracts cover OS transport.
Red tests were observed before adding tabs, restoring list entry, selecting OS tabs,
projecting the host platform, and shortening navigation labels.
Verification results and independent Standards/Spec reviews are recorded below when complete.
- Frontend full Vitest run: 82 files, 677 tests passed.
- Backend full Vitest run: 108 files passed, one skipped; 1,380 tests passed, 40 existing
optional tests skipped. Backend typecheck passed.
- After review fixes: all five affected frontend suites passed (163 tests, including
two new production-mode persisted-navigation regressions); frontend typecheck and
production build passed. The existing large-chunk advisory remains.
- The complete host CLI suite (`go test ./...`) passed across all 21 packages, including
the OS model-projection assertion.
- Strict documentation build passed.
- Read-only browser QA of actual React pages at 1280, 768 and 390 CSS pixels checked
Workspace tabs, Database table space, and Pi height/overflow. Pi's long instructions
initially exposed horizontal page overflow at 390 pixels; constrained grid children
and narrow definition lists corrected it. Database QA used an empty synthetic catalog;
the unit suites exercise populated tables, columns, edits and operation guards.
- Docker and real-stack Playwright acceptance were not run for this follow-up.
The preview is synthetic and read-only, not a deployment or provider smoke test.
## Standards
Initial independent review: one documented ADR conflict and one maintainability
judgement call. ADR 0020 now records the owner's superseding #29 footer decision.
Workspace and Pi now share keyboard tab navigation rather than duplicate its switch.
The independent follow-up review confirmed both findings resolved, with no new regressions.
## Spec
Initial independent review: two P2 findings. Fresh Database entry could still restore
Tables/Relationships from local storage, and a real-stack test retained the old Admin
button-label regex. Both were corrected; fresh entry is list-first while in-memory
navigation is retained when moving between already mounted app surfaces.
The independent follow-up review confirmed both findings resolved, with no new regressions.
Review summary: Standards 2 findings resolved, 0 outstanding; Spec 2 findings resolved,
0 outstanding. Both reviews were read-only, relative to the user-approved baseline.
Docker has not been rebuilt for this follow-up; the read-only QA preview uses the changed source.
No Gitea issue state or remote branch was changed.
+2 -2
View File
@@ -108,9 +108,9 @@ LocalEvidenceArchive(root).initialize()
async function openMemory() {
await page.getByRole("button", { name: "Administration", exact: true }).click();
expect((await page.getByRole("button", {
name: /^(Database management|Memory management|Evidence management)$/,
name: /^(Database|Memory|Evidence)$/,
}).allTextContents()).map(text => text.trim())).toEqual([
"Database management", "Memory management", "Evidence management",
"Database", "Memory", "Evidence",
]);
await page.getByRole("button", { name: "Memory", exact: true }).click();
await page.screenshot({ path: "/private/tmp/thothii-m1-memory-opening.png", fullPage: true });
@@ -0,0 +1,11 @@
/** Horizontal tab lists activate and focus the same item when navigating by keyboard. */
export function nextTabIndex(key: string, current: number, count: number): number | undefined {
if (count === 0) return undefined;
switch (key) {
case "ArrowRight": return (current + 1) % count;
case "ArrowLeft": return (current + count - 1) % count;
case "Home": return 0;
case "End": return count - 1;
default: return undefined;
}
}
-1
View File
@@ -806,7 +806,6 @@ export function AppShell({ canLogout }: AppShellProps) {
<div style={{ display: activeSurface === "database-management" ? "contents" : "none" }}>
<DatabaseManagementPage
key={selectedWorkspaceId}
initialWorkspaceId={selectedWorkspaceId}
canManage={canManageDatabase}
canManageSecrets={canManageWorkspaceSecrets}
onNavigationStateChange={updateDatabaseNavigationState}
@@ -90,14 +90,12 @@ function renderPage({
canManageSecrets = true,
onNavigationStateChange,
presentation = "legacy",
initialWorkspaceId,
}: {
rows?: CatalogDatabase[] | (() => CatalogDatabase[]);
canManage?: boolean;
canManageSecrets?: boolean;
onNavigationStateChange?: (state: { dirty: boolean; busy: boolean }) => void;
presentation?: "legacy" | "fleet";
initialWorkspaceId?: string;
} = {}) {
server.use(http.get("/api/catalog/databases", () => HttpResponse.json(
typeof rows === "function" ? rows() : rows,
@@ -106,7 +104,6 @@ function renderPage({
const view = render(
<QueryClientProvider client={client}>
<DatabaseManagementPage
initialWorkspaceId={initialWorkspaceId}
canManage={canManage}
canManageSecrets={canManageSecrets}
onNavigationStateChange={onNavigationStateChange}
@@ -119,7 +116,7 @@ function renderPage({
}
test("fleet entry opens the database list without workspace preparation or an automatic editor", async () => {
renderPage({ presentation: "fleet", initialWorkspaceId: "psd-clinical" });
renderPage({ presentation: "fleet" });
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
expect(screen.getByText("Research laboratory")).toBeVisible();
expect(screen.queryByRole("button", { name: "Back to list" })).not.toBeInTheDocument();
@@ -127,6 +124,18 @@ test("fleet entry opens the database list without workspace preparation or an au
expect(screen.queryByRole("link", { name: "Open workspace preprocessing" })).not.toBeInTheDocument();
});
test.each(["tables", "relationships"])("fresh entry ignores a previously persisted %s screen", async kind => {
vi.stubEnv("MODE", "production");
localStorage.setItem("thothii:database-management-screen", JSON.stringify({ kind, workspaceId: "psd-clinical" }));
try {
renderPage({ presentation: "fleet" });
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Back to databases" })).not.toBeInTheDocument();
} finally {
vi.unstubAllEnvs();
}
});
function makeSyncRun(
scope: CatalogSyncRun["scope"],
tableIds: string[] = [],
+1 -23
View File
@@ -92,23 +92,7 @@ const SYNC_STARTED_MESSAGES: Record<CatalogSyncScope, string> = {
relationships: "Relationship synchronization started",
all: "Full schema synchronization started",
};
const DATABASE_SCREEN_STORAGE_KEY = "thothii:database-management-screen";
function readPersistedDatabaseScreen(): DatabaseScreen {
if (import.meta.env.MODE === "test") return { kind: "list" };
try {
const parsed = JSON.parse(window.localStorage.getItem(DATABASE_SCREEN_STORAGE_KEY) ?? "null") as { kind?: unknown; workspaceId?: unknown } | null;
if ((parsed?.kind === "tables" || parsed?.kind === "relationships") && typeof parsed.workspaceId === "string") {
return { kind: parsed.kind, workspaceId: parsed.workspaceId };
}
} catch {
// Ignore malformed persisted navigation and return to the safe list view.
}
return { kind: "list" };
}
interface Props {
initialWorkspaceId?: string;
canManage: boolean;
canManageSecrets: boolean;
onNavigationStateChange?: (state: DatabaseNavigationState) => void;
@@ -214,7 +198,7 @@ export function DatabaseManagementPage({
(run) => run.status === "running",
);
const [screen, setScreen] = useState<DatabaseScreen>(readPersistedDatabaseScreen);
const [screen, setScreen] = useState<DatabaseScreen>({ kind: "list" });
const [interactionPreferences, setInteractionPreferences] = useState(() => workspacePreferences.load());
useEffect(() => workspacePreferences.subscribe(setInteractionPreferences), []);
const selectedMetadataModel = interactionPreferences.provider && interactionPreferences.model
@@ -254,12 +238,6 @@ export function DatabaseManagementPage({
suggestions: SensitivityReviewItem[];
} | null>(null);
useEffect(() => {
if (import.meta.env.MODE === "test") return;
const persisted = screen.kind === "tables" || screen.kind === "relationships" ? screen : { kind: "list" };
window.localStorage.setItem(DATABASE_SCREEN_STORAGE_KEY, JSON.stringify(persisted));
}, [screen]);
const originRef = useRef<HTMLElement | null>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const formHeadingRef = useRef<HTMLHeadingElement>(null);
+4 -11
View File
@@ -9,6 +9,7 @@ import {
} from "../api/pi-management";
import { listModels } from "../api/models";
import { Button } from "../components/ui/button";
import { nextTabIndex } from "../components/ui/tab-navigation";
import { WorkAreaPanel } from "./WorkAreaPanel";
type Feedback = { tone: "success" | "error"; message: string } | undefined;
@@ -214,18 +215,10 @@ function PiPlatformInstructions({ hostPlatform }: { hostPlatform?: PiPlatform })
function handleTabKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, platform: PiPlatform) {
const index = piPlatforms.findIndex((item) => item.id === platform);
const nextPlatform = event.key === "ArrowRight"
? piPlatforms[(index + 1) % piPlatforms.length]
: event.key === "ArrowLeft"
? piPlatforms[(index - 1 + piPlatforms.length) % piPlatforms.length]
: event.key === "Home"
? piPlatforms[0]
: event.key === "End"
? piPlatforms[piPlatforms.length - 1]
: undefined;
if (!nextPlatform) return;
const next = nextTabIndex(event.key, index, piPlatforms.length);
if (next === undefined) return;
event.preventDefault();
activateAndFocus(nextPlatform.id);
activateAndFocus(piPlatforms[next].id);
}
return (
+2 -3
View File
@@ -32,6 +32,7 @@ import {
type AuthOperationGuard,
} from "../auth/authOperation";
import { Button } from "../components/ui/button";
import { nextTabIndex } from "../components/ui/tab-navigation";
import { WorkAreaPanel } from "./WorkAreaPanel";
import { WorkspacePreparation } from "./administration/WorkspacePreparation";
@@ -556,9 +557,7 @@ export function WorkspaceManager({
tabIndex={activeArea === area ? 0 : -1}
onClick={() => setActiveArea(area)}
onKeyDown={event => {
const next = event.key === "ArrowRight" ? (index + 1) % workspaceAreas.length
: event.key === "ArrowLeft" ? (index + workspaceAreas.length - 1) % workspaceAreas.length
: event.key === "Home" ? 0 : event.key === "End" ? workspaceAreas.length - 1 : undefined;
const next = nextTabIndex(event.key, index, workspaceAreas.length);
if (next === undefined) return;
event.preventDefault();
setActiveArea(workspaceAreas[next]);