diff --git a/docs/PLAN.md b/docs/PLAN.md index f074795..a584ea9 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -47,3 +47,4 @@ Updated: 2026-04-02 30. Added a websocket heartbeat so time-based status transitions such as `live -> idle/warn` are recalculated predictably even when no new proxy events arrive. 31. Moved proxy-copy into the Users actions column, added a last-seen/online column from parsed 3proxy logs, and introduced bounded websocket/API reconnect attempts with a visible connection banner and forced logout after full recovery failure. 32. Restored proxy-link copying for plain-`http` deployments by falling back from the Clipboard API to `execCommand('copy')`, and added regression coverage for both clipboard paths. +33. Replaced stale startup mock values with explicit skeleton loading states so the shell no longer flashes fallback dashboard/users/settings data before the first live snapshot arrives. diff --git a/docs/PROJECT_INDEX.md b/docs/PROJECT_INDEX.md index 0f9a6ac..6cc539a 100644 --- a/docs/PROJECT_INDEX.md +++ b/docs/PROJECT_INDEX.md @@ -23,10 +23,10 @@ Updated: 2026-04-02 ## Frontend - `src/main.tsx`: application bootstrap -- `src/App.tsx`: authenticated panel shell with API-backed login, `sessionStorage` token persistence, hash-based tab history, websocket snapshot patch sync, bounded reconnect/API fallback policy with connection notices, log-derived last-seen user labels, icon-based user actions, HTTP-safe proxy-link copying fallback, create/edit user modal flows, runtime stop control, localized labels, early theme application, and protected panel mutations +- `src/App.tsx`: authenticated panel shell with API-backed login, `sessionStorage` token persistence, hash-based tab history, websocket snapshot patch sync, bounded reconnect/API fallback policy with connection notices, skeleton-first startup hydration, log-derived last-seen user labels, icon-based user actions, HTTP-safe proxy-link copying fallback, create/edit user modal flows, runtime stop control, localized labels, early theme application, and protected panel mutations - `src/SystemTab.tsx`: Settings tab with separate panel-settings and services cards, editable proxy endpoint, dirty-draft protection against incoming live sync, unified service type editing, remove confirmation, and generated config preview -- `src/App.test.tsx`: login-gate, preferences persistence, hash-tab restoration, websocket-sync safety, reconnect/logout fallback handling, clipboard fallback coverage, generated-credential/create-edit modal flows, runtime stop, pause/resume, delete-confirm, and settings-save UI tests -- `src/app.css`: full panel styling including fixed-width icon action buttons, busy-state treatment, and connection banner styling +- `src/App.test.tsx`: login-gate, startup skeleton, preferences persistence, hash-tab restoration, websocket-sync safety, reconnect/logout fallback handling, clipboard fallback coverage, generated-credential/create-edit modal flows, runtime stop, pause/resume, delete-confirm, and settings-save UI tests +- `src/app.css`: full panel styling including fixed-width icon action buttons, busy-state treatment, startup skeleton shimmer states, and connection banner styling - `src/data/mockDashboard.ts`: default panel state and frontend fallback snapshot - `src/lib/3proxy.ts`: formatting and status helpers - `src/lib/3proxy.test.ts`: paranoia-oriented tests for core domain rules diff --git a/src/App.test.tsx b/src/App.test.tsx index 7349189..47c9003 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -9,6 +9,12 @@ async function loginIntoPanel(user: ReturnType) { await user.type(screen.getByLabelText(/login/i), 'admin'); await user.type(screen.getByLabelText(/password/i), 'proxy-ui-demo'); await user.click(screen.getByRole('button', { name: /open panel/i })); + await waitFor(() => expect(MockWebSocket.instances.length).toBeGreaterThan(0)); + MockWebSocket.instances.at(-1)?.emitMessage({ + type: 'snapshot.init', + snapshot: fallbackDashboardSnapshot, + }); + await waitFor(() => expect(screen.getByRole('button', { name: /settings/i })).not.toBeDisabled()); } function mockClipboardWriteText(writeText: ReturnType) { @@ -64,6 +70,19 @@ describe('App login gate', () => { expect(screen.queryByRole('button', { name: /open panel/i })).not.toBeInTheDocument(); }); + it('shows loading skeletons instead of stale mock values before the first snapshot arrives', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByLabelText(/login/i), 'admin'); + await user.type(screen.getByLabelText(/password/i), 'proxy-ui-demo'); + await user.click(screen.getByRole('button', { name: /open panel/i })); + + expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument(); + expect(screen.queryByText(/edge\.example\.net/i)).not.toBeInTheDocument(); + expect(document.querySelectorAll('.skeleton-line').length).toBeGreaterThan(0); + }); + it('stores panel language in localStorage and restores it after a remount', async () => { const user = userEvent.setup(); const firstRender = render(); diff --git a/src/App.tsx b/src/App.tsx index c26d8ef..23045c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -831,6 +831,259 @@ function TrashIcon() { ); } +function SkeletonLine({ className = '' }: { className?: string }) { + return