-
@@ -393,12 +402,15 @@
-
-
Notifications
-
+
+
History
+
+
+
+
Showing notifications for your accessible clients.
@@ -408,7 +420,7 @@
@@ -420,7 +432,7 @@
id="empty-state"
class="py-6 text-center text-slate-400"
>
- No notifications yet.
+ No notifications.
diff --git a/Overlord-Server/src/ui.css b/Overlord-Server/src/ui.css
index a78ca8d..74406b5 100644
--- a/Overlord-Server/src/ui.css
+++ b/Overlord-Server/src/ui.css
@@ -4,5 +4,8 @@
@import "./web/styles/primitives/buttons.css";
@import "./web/styles/primitives/badges.css";
@import "./web/styles/primitives/popovers.css";
+@import "./web/styles/primitives/help.css";
+@import "./web/styles/primitives/fields.css";
@import "./web/styles/patterns/toolbars.css";
-
+@import "./web/styles/features/notifications.css";
+@import "./web/styles/features/file-share.css";
diff --git a/Overlord-Server/src/web/styles/base.css b/Overlord-Server/src/web/styles/base.css
index da45fba..f88c08f 100644
--- a/Overlord-Server/src/web/styles/base.css
+++ b/Overlord-Server/src/web/styles/base.css
@@ -15,4 +15,3 @@
transition-duration: 0.01ms !important;
}
}
-
diff --git a/Overlord-Server/src/web/styles/features/file-share.css b/Overlord-Server/src/web/styles/features/file-share.css
new file mode 100644
index 0000000..62e718e
--- /dev/null
+++ b/Overlord-Server/src/web/styles/features/file-share.css
@@ -0,0 +1,5 @@
+@media (max-width: 640px) {
+ .file-share-empty {
+ padding-right: 4rem;
+ }
+}
diff --git a/Overlord-Server/src/web/styles/features/notifications.css b/Overlord-Server/src/web/styles/features/notifications.css
new file mode 100644
index 0000000..53cf5fd
--- /dev/null
+++ b/Overlord-Server/src/web/styles/features/notifications.css
@@ -0,0 +1,118 @@
+.app-notifications .notifications-shell {
+ gap: 1rem;
+}
+
+.app-notifications .notification-panel,
+.app-notifications .notification-section {
+ border-color: var(--ui-border);
+ border-radius: 0.875rem;
+ background: var(--ui-surface);
+ box-shadow: var(--ui-shadow-sm);
+}
+
+.app-notifications .notification-section {
+ overflow: clip;
+}
+
+.app-notifications .notification-section__summary,
+.app-notifications .notification-advanced > summary {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ list-style: none;
+ cursor: pointer;
+ user-select: none;
+}
+
+.app-notifications .notification-section__summary {
+ justify-content: space-between;
+ min-height: 3.5rem;
+ padding: 0.75rem 1rem;
+ font-weight: 600;
+}
+
+.app-notifications .notification-section__summary::-webkit-details-marker,
+.app-notifications .notification-advanced > summary::-webkit-details-marker {
+ display: none;
+}
+
+.app-notifications .notification-section__summary:hover,
+.app-notifications .notification-section__summary:focus-visible {
+ background: rgb(30 41 59 / 0.45);
+}
+
+.app-notifications .notification-section__title {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.625rem;
+}
+
+.app-notifications .notification-section__body {
+ padding: 1rem;
+ border-top: 1px solid var(--ui-border);
+}
+
+.app-notifications .notification-chevron {
+ color: var(--ui-text-muted);
+ font-size: 0.75rem;
+ transition: transform 160ms ease;
+}
+
+.app-notifications details[open] > summary > .notification-chevron {
+ transform: rotate(180deg);
+}
+
+.app-notifications .notification-advanced {
+ border-top: 1px solid var(--ui-border);
+}
+
+.app-notifications .notification-advanced > summary {
+ min-height: 2.75rem;
+ color: var(--ui-text-secondary);
+ font-size: 0.875rem;
+ font-weight: 600;
+}
+
+.app-notifications .notification-advanced > summary .notification-chevron {
+ margin-left: auto;
+}
+
+.app-notifications .notification-advanced__body {
+ padding: 0.25rem 0 0.25rem;
+}
+
+.app-notifications .notification-choice {
+ min-height: 2.75rem;
+ padding-block: 0.625rem;
+}
+
+.app-notifications .template-variables {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ margin-top: 0.375rem;
+ color: var(--ui-text-muted);
+ font-size: 0.75rem;
+}
+
+.app-notifications .template-variables code {
+ padding: 0.125rem 0.35rem;
+ border: 1px solid var(--ui-border);
+ border-radius: 0.25rem;
+ background: var(--ui-canvas);
+}
+
+@media (max-width: 640px) {
+ .app-notifications main {
+ padding-inline: 0.875rem;
+ }
+
+ .app-notifications #notification-search {
+ width: min(100%, 18rem);
+ }
+
+ .app-notifications .notification-section__body,
+ .app-notifications .notification-panel {
+ padding: 0.875rem;
+ }
+}
diff --git a/Overlord-Server/src/web/styles/primitives/badges.css b/Overlord-Server/src/web/styles/primitives/badges.css
index 6cd2d1e..79f7cc1 100644
--- a/Overlord-Server/src/web/styles/primitives/badges.css
+++ b/Overlord-Server/src/web/styles/primitives/badges.css
@@ -50,4 +50,3 @@
background: var(--ui-text-faint);
box-shadow: 0 0 0 4px rgba(100, 116, 139, 0.1);
}
-
diff --git a/Overlord-Server/src/web/styles/primitives/cards.css b/Overlord-Server/src/web/styles/primitives/cards.css
index 73743c1..cfc4ceb 100644
--- a/Overlord-Server/src/web/styles/primitives/cards.css
+++ b/Overlord-Server/src/web/styles/primitives/cards.css
@@ -19,4 +19,3 @@
opacity: 0.52;
border-color: rgba(56, 189, 248, 0.62);
}
-
diff --git a/Overlord-Server/src/web/styles/primitives/fields.css b/Overlord-Server/src/web/styles/primitives/fields.css
new file mode 100644
index 0000000..d54f44b
--- /dev/null
+++ b/Overlord-Server/src/web/styles/primitives/fields.css
@@ -0,0 +1,48 @@
+/* Inputs nested inside a styled shell should not paint a second surface. */
+input.bg-transparent,
+.graph-field input {
+ background-color: transparent !important;
+ border-color: transparent !important;
+ box-shadow: none !important;
+}
+
+.graph-field,
+.graph-select {
+ border-color: var(--ui-border-subtle);
+ background: var(--ui-surface-1);
+ color: var(--ui-text);
+}
+
+.graph-field:focus-within,
+.graph-select:focus {
+ border-color: var(--ui-border-strong) !important;
+ box-shadow: none !important;
+}
+
+.graph-field input::placeholder {
+ color: var(--ui-text-faint);
+}
+
+#upload-file {
+ padding: 0 0.75rem 0 0;
+ overflow: hidden;
+ color: var(--ui-text-muted);
+}
+
+#upload-file::file-selector-button {
+ height: 100%;
+ margin-right: 0.75rem;
+ padding: 0 0.875rem;
+ border: 0;
+ border-right: 1px solid var(--ui-border-subtle);
+ border-radius: 0;
+ background: transparent;
+ color: var(--ui-text);
+ font: inherit;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+#upload-file:hover::file-selector-button {
+ background: var(--ui-surface-2);
+}
diff --git a/Overlord-Server/src/web/styles/primitives/help.css b/Overlord-Server/src/web/styles/primitives/help.css
new file mode 100644
index 0000000..2a81533
--- /dev/null
+++ b/Overlord-Server/src/web/styles/primitives/help.css
@@ -0,0 +1,24 @@
+.ui-help {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.25rem;
+ height: 1.25rem;
+ flex: none;
+ color: var(--ui-text-muted);
+ font-size: 0.75rem;
+ border-radius: 999px;
+ cursor: help;
+}
+
+.ui-help:hover,
+.ui-help:focus-visible {
+ color: var(--ui-accent);
+ outline: none;
+}
+
+.ui-heading-help {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
diff --git a/Overlord-Server/test/e2e/concise-ui.e2e.ts b/Overlord-Server/test/e2e/concise-ui.e2e.ts
new file mode 100644
index 0000000..11e276b
--- /dev/null
+++ b/Overlord-Server/test/e2e/concise-ui.e2e.ts
@@ -0,0 +1,68 @@
+import { expect, test } from "@playwright/test";
+import { collectBrowserIssues, login } from "./helpers";
+
+test.describe("concise UI conventions", () => {
+ test("removes redundant page copy and promotes section help", async ({ page }) => {
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+
+ await page.goto("/logs");
+ await expect(page.getByRole("heading", { name: "Client Connection Logs" })).toBeVisible();
+ await expect(page.getByText("Review connection lifecycle activity across enrolled clients.")).toHaveCount(0);
+ const filterHelp = page.locator("h2", { hasText: "Filter activity" }).locator(".ui-help");
+ await expect(filterHelp).toHaveAttribute("title", "Narrow results by client, time range, or event type.");
+ await expect(page.getByText("Narrow results by client, time range, or event type.")).toHaveClass("sr-only");
+
+ await page.goto("/plugins");
+ await expect(page.getByRole("heading", { name: "Plugins", exact: true })).toBeVisible();
+ await expect(page.getByText("Upload zip bundles and manage client plugins or server extensions")).toHaveCount(0);
+ await expect(page.locator("h2", { hasText: "Upload" }).locator(".ui-help")).toHaveAttribute(
+ "title",
+ "Drag a plugin zip here or click to browse",
+ );
+
+ await page.goto("/settings");
+ await expect(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible();
+ await expect(page.getByText("Manage your account preferences, operational tools, and admin configuration.")).toHaveCount(0);
+ await expect(page.locator("#section-account h2 .ui-help")).toHaveAttribute(
+ "title",
+ "View your account info and update your password.",
+ );
+ await expect(page.locator("#chat-retention-days")).toHaveAttribute(
+ "title",
+ "Messages older than this will be automatically deleted. Set to 0 to keep messages forever. Max 365 days.",
+ );
+ await page.waitForLoadState("networkidle");
+
+ await page.goto("/build");
+ await expect(page.getByText("Build customized Overlord client agents")).toHaveCount(0);
+ const obfuscateOption = page.locator('label:has(input[name="obfuscate"])').first();
+ await expect(obfuscateOption).toHaveAttribute("title", "Build with garble for code obfuscation");
+ await expect(obfuscateOption.locator("span.sr-only")).toContainText("Build with garble");
+ await page.waitForLoadState("networkidle");
+
+ await page.goto("/purgatory");
+ await page.locator("#auto-accept-toggle").click({ force: true });
+ await expect(page.getByText("This disables all enrollment security.")).toBeVisible();
+ await page.locator("#auto-accept-modal-cancel").click();
+
+ expect(browserIssues).toEqual([]);
+ });
+
+ test("keeps compact help and controls usable at phone width", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+ await page.goto("/settings");
+
+ await expect(page.locator("#section-account h2 .ui-help")).toBeVisible();
+ await page.locator("#section-account h2 .ui-help").focus();
+ const overflow = await page.evaluate(() => ({
+ body: document.body.scrollWidth - document.body.clientWidth,
+ root: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ }));
+ expect(overflow.body).toBeLessThanOrEqual(1);
+ expect(overflow.root).toBeLessThanOrEqual(1);
+ expect(browserIssues).toEqual([]);
+ });
+});
diff --git a/Overlord-Server/test/e2e/helpers.ts b/Overlord-Server/test/e2e/helpers.ts
index a6146e3..804da5e 100644
--- a/Overlord-Server/test/e2e/helpers.ts
+++ b/Overlord-Server/test/e2e/helpers.ts
@@ -18,7 +18,11 @@ export async function login(
export async function navigateFromMenu(page: Page, linkId: string): Promise
{
const link = page.locator(`#${linkId}`);
const wrapper = link.locator("xpath=ancestor::*[contains(@class, 'nav-dd-wrapper')][1]");
- await wrapper.locator(":scope > .nav-dd-group-btn").click();
+ const groupButton = wrapper.locator(":scope > .nav-dd-group-btn");
+ if (!(await groupButton.isVisible())) {
+ await page.locator("#topbar-nav-toggle").click();
+ }
+ await groupButton.click();
await expect(link).toBeVisible();
await link.click();
}
diff --git a/Overlord-Server/test/e2e/notifications-layout.e2e.ts b/Overlord-Server/test/e2e/notifications-layout.e2e.ts
new file mode 100644
index 0000000..863b24c
--- /dev/null
+++ b/Overlord-Server/test/e2e/notifications-layout.e2e.ts
@@ -0,0 +1,54 @@
+import { expect, test } from "@playwright/test";
+import { collectBrowserIssues, login } from "./helpers";
+
+test.describe("compact notifications layout", () => {
+ test("keeps common settings visible and advanced delivery collapsed", async ({ page }) => {
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+ await page.goto("/notifications");
+
+ await expect(page.getByRole("heading", { name: "Keywords" })).toBeVisible();
+ await expect(page.locator("#keyword-input")).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Desktop alerts" })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Event delivery" })).toBeVisible();
+
+ const deliverySections = page.locator("details.notification-section");
+ await expect(deliverySections).toHaveCount(2);
+ await expect(deliverySections.nth(0)).not.toHaveAttribute("open", "");
+ await expect(deliverySections.nth(1)).not.toHaveAttribute("open", "");
+ await expect(page.locator("#save-webhook")).not.toBeVisible();
+ await expect(page.locator("#save-telegram")).not.toBeVisible();
+
+ await deliverySections.nth(0).locator(":scope > summary").click();
+ await expect(page.locator("#save-webhook")).toBeVisible();
+ await expect(page.locator("#webhook-url")).toBeVisible();
+ await expect(page.locator("#webhook-template")).not.toBeVisible();
+ await deliverySections.nth(0).locator("details.notification-advanced > summary").click();
+ await expect(page.getByRole("textbox", { name: "Editor content" })).toBeVisible();
+
+ await expect(page.locator(".ui-help[title]")).toHaveCount(9);
+ await expect(page.getByText("Fire when a client connects")).toHaveCount(0);
+ await expect(page.getByText("Your personal webhook")).toHaveCount(0);
+ expect(browserIssues).toEqual([]);
+ });
+
+ test("fits opened settings on a phone viewport", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+ await page.goto("/notifications");
+
+ const webhook = page.locator("details.notification-section").first();
+ await webhook.locator(":scope > summary").click();
+ await webhook.locator("details.notification-advanced > summary").click();
+ await expect(page.getByRole("textbox", { name: "Editor content" })).toBeVisible();
+
+ const overflow = await page.evaluate(() => ({
+ body: document.body.scrollWidth - document.body.clientWidth,
+ root: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ }));
+ expect(overflow.body).toBeLessThanOrEqual(1);
+ expect(overflow.root).toBeLessThanOrEqual(1);
+ expect(browserIssues).toEqual([]);
+ });
+});
diff --git a/Overlord-Server/test/e2e/ui-polish.e2e.ts b/Overlord-Server/test/e2e/ui-polish.e2e.ts
new file mode 100644
index 0000000..19199ce
--- /dev/null
+++ b/Overlord-Server/test/e2e/ui-polish.e2e.ts
@@ -0,0 +1,66 @@
+import { expect, test } from "@playwright/test";
+import { collectBrowserIssues, login } from "./helpers";
+
+test.describe("cross-page UI polish", () => {
+ test("rounds notification panels and normalizes graph fields", async ({ page }) => {
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+
+ await page.goto("/notifications");
+ const notificationRadius = await page.locator("#keyword-section").evaluate((element) =>
+ Number.parseFloat(getComputedStyle(element).borderTopLeftRadius),
+ );
+ const deliveryRadius = await page.locator("details.notification-section").first().evaluate((element) =>
+ Number.parseFloat(getComputedStyle(element).borderTopLeftRadius),
+ );
+ expect(notificationRadius).toBeGreaterThanOrEqual(14);
+ expect(deliveryRadius).toBeGreaterThanOrEqual(14);
+
+ await page.goto("/graph");
+ const graphColors = await page.evaluate(() => {
+ const field = document.querySelector(".graph-field");
+ const input = document.querySelector("#graph-search");
+ const select = document.querySelector("#graph-status");
+ if (!field || !input || !select) throw new Error("Graph controls are missing");
+ return {
+ field: getComputedStyle(field).backgroundColor,
+ input: getComputedStyle(input).backgroundColor,
+ select: getComputedStyle(select).backgroundColor,
+ };
+ });
+ expect(graphColors.input).toBe("rgba(0, 0, 0, 0)");
+ expect(graphColors.field).toBe(graphColors.select);
+
+ await page.locator("#graph-search").fill("example");
+ await expect(page.locator("#graph-search")).toHaveValue("example");
+ expect(browserIssues).toEqual([]);
+ });
+
+ test("spaces the File Share empty state and fits on mobile", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const browserIssues = collectBrowserIssues(page);
+ await login(page);
+ await page.goto("/file-share");
+
+ const emptyState = page.locator(".file-share-empty");
+ await expect(emptyState).toBeVisible();
+ const filePickerColors = await page.locator("#upload-file").evaluate((element) => ({
+ field: getComputedStyle(element).backgroundColor,
+ button: getComputedStyle(element, "::file-selector-button").backgroundColor,
+ }));
+ expect(filePickerColors.button).toBe("rgba(0, 0, 0, 0)");
+ expect(filePickerColors.field).not.toBe(filePickerColors.button);
+ await page.locator("#upload-file").hover();
+ const iconBox = await emptyState.locator("i").boundingBox();
+ const labelBox = await emptyState.locator("span").boundingBox();
+ expect(iconBox).not.toBeNull();
+ expect(labelBox).not.toBeNull();
+ expect(labelBox!.y - (iconBox!.y + iconBox!.height)).toBeGreaterThanOrEqual(11);
+
+ const overflow = await page.evaluate(() =>
+ document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(1);
+ expect(browserIssues).toEqual([]);
+ });
+});