fix: resolve admin issue review findings and record verification
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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[] = [],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user