Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Before you pick a service the detail panel shows a **services dashboard** instea
Selecting a service opens the detail panel with Start, Stop, and Restart controls, status, and the correct `.env` connection values with a one-click copy button. The header leads with the same service mark the installed grid draws, larger and standing on its own without the tinted plate, inked in the brand colour the preset declares. An admin UI that ships no mark of its own draws the mark of the engine it administers instead, in that engine's colour, so pgAdmin comes up as PostgreSQL's elephant rather than a stand-in glyph; a per-site worker carries no mark of its own, so its header starts at the label. The header states how many sites use the service on a button next to the action menu, which drops down the list of them: each row is coloured by what that site is doing (serving, paused, stopped) and links through to it, and a service nothing uses shows no button at all. A per-site worker answers to one site, so its header names that site inline instead. Restart is available for every built-in and custom service and wraps `podman restart` (clears the paused flag on success); the grouped per-site workers (Queues, Horizon, Schedules, Workers, Stripe, Reverb) remain start/stop only. A **Check for updates** action sits in the service's action menu (non-worker services only); it bypasses the cached availability lookup, re-fetches the registry tag list, and shows either an "Already up to date" hint or a "Update available: {tag}" banner that becomes the live Update button moments later. Database service detail panels (mysql, postgres, mongo, and any installed alternate like `mysql-5-7`) get a few extras:

- **Databases tab**: the panel opens on it, listing what is actually inside the running engine as cards with their sizes. From a card you can create and drop a database, export it to a plain SQL dump or import one, copy a per-database connection string, and open it in an installed admin tool, with that database's snapshots to take, restore, delete or download on the same card. Cards link back to the site that owns the database, a `_testing` database folds into the card of the database it tests and is offered up alongside it when that database is dropped, and a worktree's isolated database is shown under its own branch domain. It requires dashboard-control authority, which remote sessions receive only after `lerd remote-control full-access on`. See [Databases](../usage/database.md).
- **Snapshots tab**: next to Databases, the [automatic snapshot](../usage/database.md#automatic-snapshots) schedule stated at a glance (with a button to the settings dialog) and what the engine's snapshots cost on disk, then every snapshot on the engine as a paged table filterable by site and by when it was taken, each row offering keep, download, restore and delete, with checkboxes for removing several at once behind one confirmation. Whether a given database is covered is set on that database's own snapshots dialog, or with one click on the clock beside it in the Databases grid. Same authority as the Databases tab. A site's database service card carries a camera button that opens this tab filtered to that site.
- **Snapshots tab**: next to Databases, the [automatic snapshot](../usage/database.md#automatic-snapshots) schedule stated at a glance (with a button to the settings dialog) and what the engine's snapshots cost on disk, then every snapshot on the engine as a paged table filterable by site and by when it was taken, each row offering keep, download, restore and delete, with checkboxes for removing several at once behind one confirmation. Whether a given database is covered is set on that database's own snapshots dialog, or with one click on the clock beside it in the Databases grid. Same authority as the Databases tab, and like it, opening the tab wakes a sleeping engine and lists its snapshots once it is ready, while a stopped engine has to be started first. A site's database service card carries a camera button that opens this tab filtered to that site.
- **Env tab**: the service's connection URL and every env var its preset wires into a site's `.env`, one row per key. The header's copy button takes the whole block as `KEY=value` lines ready to paste into a `.env`, each row copies its own line, and a copy the browser refuses marks the button red instead of failing silently.
- **Suggestion banner**: a sky-blue tip offering to install the paired admin UI (phpMyAdmin / pgAdmin / Mongo Express) when it isn't installed yet. Dismissable per-preset; dismissal persists in `localStorage`.
- **Open admin button**: when the paired admin UI is installed, a button on the header opens its dashboard inline as a full-width iframe overlay and auto-starts the admin service if needed. When no admin UI is installed and the service is active, a fallback **Open connection URL** anchor hands the `mysql://` / `postgresql://` / `mongodb://` URL to your registered DB client (DBeaver, TablePlus, Compass, etc.).
Expand Down
21 changes: 16 additions & 5 deletions internal/ui/web/src/tabs/services/ServiceSnapshotsTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import DetailButton from '$components/DetailButton.svelte';
import Dropdown from '$components/Dropdown.svelte';
import Icon from '$components/Icon.svelte';
import LoadingRow from '$components/LoadingRow.svelte';
import Modal from '$components/Modal.svelte';
import Pager from '$components/Pager.svelte';
import SnapshotSettingsModal from '../../modals/SnapshotSettingsModal.svelte';
Expand All @@ -21,6 +22,7 @@
} from '$stores/databases';
import { autoSnapshot, loadAutoSnapshot, nextSnapshotAt } from '$stores/autoSnapshot';
import type { Service } from '$stores/services';
import { keepServiceAwake } from '$stores/dashboard';
import { onMount } from 'svelte';
import { m } from '../../paraglide/messages.js';

Expand All @@ -38,13 +40,18 @@
siteFilter = site;
page = 0;
});
// The snapshots ride along with the engine's databases, so the tab reads the
// same listing the Databases tab does rather than fetching its own, and skips
// a service that has no databases for the same reason it does.
// Snapshots ride along with the engine's databases listing, which an asleep
// engine returns empty, so the tab wakes it and loads only once it is ready.
$effect(() => {
if (!svc.is_database) return;
if (!svc.is_database || !up) return;
void loadEngine(svc.name);
});
$effect(() => {
if (!svc.is_database) return;
return keepServiceAwake(svc.name);
});
const up = $derived(svc.status === 'active' && !svc.idle_suspended);
const stopped = $derived(!up && !svc.idle_suspended);

type Row = { snap: Snapshot; database: string; site: string; key: string; at: Date | null };

Expand Down Expand Up @@ -328,7 +335,11 @@
<p class="text-xs text-red-500 mb-2">{error}</p>
{/if}

{#if rows.length === 0}
{#if stopped}
<p class="text-sm text-gray-500 dark:text-gray-400">{m.databases_startHint()}</p>
{:else if !up}
<LoadingRow />
Comment on lines +338 to +341

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Wake behavior is undocumented

This changes what users see for sleeping and stopped engines, but no documentation changes accompany it. CLAUDE.md requires a relevant docs/ update for every behavior change. Before merging, document that opening Snapshots wakes a sleeping engine and that a stopped engine must be started.

Context Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/web/src/tabs/services/ServiceSnapshotsTab.svelte
Line: 340-343

Comment:
**Wake behavior is undocumented**

This changes what users see for sleeping and stopped engines, but no documentation changes accompany it. `CLAUDE.md` requires a relevant `docs/` update for every behavior change. Before merging, document that opening Snapshots wakes a sleeping engine and that a stopped engine must be started.

**Context Used:** CLAUDE.md ([source](https://github.com/lerd-env/lerd/blob/main/CLAUDE.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, the Snapshots tab entry in the web UI docs now says opening it wakes a sleeping engine and that a stopped one has to be started first. In e412a34.

{:else if rows.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">{m.databases_noSnapshots()}</p>
{:else if filtered.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">{m.snapshots_noMatches()}</p>
Expand Down
37 changes: 34 additions & 3 deletions internal/ui/web/src/tabs/services/ServiceSnapshotsTab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,31 @@ const engine: DatabaseEngine = {
]
};

const svc = { name: 'mysql', is_database: true } as Service;
const svc = { name: 'mysql', status: 'active', is_database: true } as Service;

const {
loadAutoSnapshot,
saveAutoSnapshot,
loadEngine,
keepSnapshot,
deleteSnapshot,
restoreSnapshot
restoreSnapshot,
keepServiceAwake
} = vi.hoisted(
() => ({
loadAutoSnapshot: vi.fn(async () => {}),
saveAutoSnapshot: vi.fn(async () => ({ ok: true })),
loadEngine: vi.fn(async () => {}),
keepSnapshot: vi.fn(async () => ({ ok: true })),
deleteSnapshot: vi.fn(async () => ({ ok: true })),
restoreSnapshot: vi.fn(async () => ({ ok: true }))
restoreSnapshot: vi.fn(async () => ({ ok: true })),
keepServiceAwake: vi.fn(() => () => {})
})
);
vi.mock('$stores/dashboard', async (orig) => {
const actual = (await orig()) as object;
return { ...actual, keepServiceAwake };
});
vi.mock('$stores/autoSnapshot', async (orig) => {
const actual = (await orig()) as object;
return { ...actual, loadAutoSnapshot, saveAutoSnapshot };
Expand Down Expand Up @@ -127,6 +133,8 @@ describe('ServiceSnapshotsTab', () => {
keepSnapshot.mockClear();
deleteSnapshot.mockClear();
restoreSnapshot.mockClear();
loadEngine.mockClear();
keepServiceAwake.mockClear();
autoSnapshot.set(structuredClone(policy));
databases.set([engine]);
});
Expand Down Expand Up @@ -349,4 +357,27 @@ describe('ServiceSnapshotsTab', () => {
expect(keepSnapshot).toHaveBeenCalledWith('mysql', 'shop_db', 'auto-20260720-100000', true)
);
});

// An asleep engine lists no databases, so the tab wakes it and only asks for
// its snapshots once it is ready, rather than reporting none.
it('wakes a sleeping engine and loads its snapshots once it is ready', async () => {
databases.set([]);
const sleeping = { ...svc, status: 'inactive', idle_suspended: true } as Service;
const { getByText, queryByText, rerender } = render(ServiceSnapshotsTab, { props: { svc: sleeping } });
expect(keepServiceAwake).toHaveBeenCalledWith('mysql');
expect(loadEngine).not.toHaveBeenCalled();
expect(getByText('Loading...')).toBeInTheDocument();
expect(queryByText('No snapshots yet.')).toBeNull();

await rerender({ svc });
await waitFor(() => expect(loadEngine).toHaveBeenCalledWith('mysql'));
});

it('asks to start a stopped engine instead of reporting no snapshots', () => {
databases.set([]);
const stopped = { ...svc, status: 'inactive' } as Service;
const { getByText } = render(ServiceSnapshotsTab, { props: { svc: stopped } });
expect(getByText('Start the engine to view its databases.')).toBeInTheDocument();
expect(loadEngine).not.toHaveBeenCalled();
});
});
Loading