Skip to content

feat: UI polish batch 2 — log search, teamless SSO block, multi-select mappings, backup fixes#68

Merged
TerrifiedBug merged 4 commits intomainfrom
worktree-ui-polish-batch-2
Mar 8, 2026
Merged

feat: UI polish batch 2 — log search, teamless SSO block, multi-select mappings, backup fixes#68
TerrifiedBug merged 4 commits intomainfrom
worktree-ui-polish-batch-2

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

  • Log search: Added client-side text search to both pipeline and node log viewers with match highlighting
  • Teamless SSO block: Users without team assignments now see a full-page "No Team Assigned" message instead of a broken UI with silent FORBIDDEN errors
  • Multi-select team mapping: Group-to-team mapping UI now supports selecting multiple teams per row (same role applies to all), reducing repetitive rows for admin groups
  • Teams pill threshold: Users table shows "N teams" badge when user belongs to >1 team (was >2) for cleaner display
  • Backup bug fix: listBackups() now validates .dump file exists before showing a backup; added failed-backup error banner in settings
  • Backup download: Super-admins can download .dump files directly from the available backups table

Test plan

  • Open pipeline logs → verify search input filters log lines and highlights matches
  • Open fleet node logs → same search behavior
  • Log in as SSO user with no team → verify full-page block appears with sign-out option
  • Settings > IdP Group Mappings → verify multi-select team dropdown, save, and reload merges correctly
  • Settings > Users table → verify "N teams" pill shows for multi-team users
  • Settings > Backups → verify failed backup banner appears when last backup failed
  • Settings > Backups → verify download button streams .dump file (super-admin only)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR delivers five UI/UX improvements across the dashboard: client-side log search with highlighting, a full-page gate for teamless SSO users, multi-select team mapping for IdP group sync, a reduced teams-pill threshold, and backup reliability fixes including a super-admin .dump download endpoint.

Key changes:

  • Log search (pipeline-logs.tsx, node-logs.tsx, log-search-utils.tsx): Client-side filtering and first-occurrence highlight added to both log viewers. The highlightMatch utility only highlights the first occurrence per message line; subsequent matches in the same message are left un-highlighted.
  • Teamless SSO block (layout.tsx): Uses trpc.team.list — which returns all teams for super-admins and only membership teams for regular users — so super-admins are correctly unaffected unless the database contains zero non-system teams.
  • Multi-select team mapping (settings/page.tsx): mergeMappings/flattenMappings round-trip correctly converts the flat {group, teamId, role} backend schema to grouped {group, teamIds[], role} UI state and back on save.
  • Backup download (download/route.ts): Super-admin-only endpoint with correct path.basename + allowlist-regex path sanitisation. However, fs.stat and createReadStream sit outside the try/catch that guards fs.access, meaning a file deleted between those calls will produce an unhandled exception and a 500 response instead of a clean 404.
  • listBackups fix (backup.ts): Orphaned .meta.json files are now skipped; the fix is correct.

Confidence Score: 4/5

  • Safe to merge after addressing the unguarded fs.stat / createReadStream calls in the download route.
  • Six of seven changed files are clean. The only correctness issue is in the new download route where a TOCTOU gap between fs.access and fs.stat can produce an unhandled 500; everything else (auth, path sanitisation, super-admin gating) is correctly implemented.
  • src/app/api/backups/[filename]/download/route.ts — unprotected fs.stat / createReadStream calls after the access check.

Important Files Changed

Filename Overview
src/app/api/backups/[filename]/download/route.ts New super-admin-only download endpoint with solid path sanitisation, but fs.stat and createReadStream are outside the error-handling block, so a deleted file after fs.access will produce an unhandled 500 instead of a 404.
src/app/(dashboard)/layout.tsx Adds teamless-user block using trpc.team.list; super-admins are correctly handled because the server-side list procedure returns all teams for them, so teams.length === 0 is only true when zero teams exist in the system.
src/app/(dashboard)/settings/page.tsx Multi-select team mapping (merge/flatten round-trip looks correct), teams pill threshold change, failed-backup banner, and download button (correctly gated behind isSuperAdmin tab) all look clean.
src/components/log-search-utils.tsx New highlight utility is correct but only highlights the first occurrence of the search term per log line; subsequent occurrences in the same message remain un-highlighted.
src/server/services/backup.ts listBackups now guards against orphaned .meta.json files by checking for the corresponding .dump before parsing metadata; the shared catch block is a minor readability point but not a bug.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Browser requests backup download] --> B{Session valid?}
    B -- No --> C[401 Unauthorized]
    B -- Yes --> D{User is super-admin?}
    D -- No --> E[403 Forbidden]
    D -- Yes --> F{Filename passes sanitizeFilename?}
    F -- No --> G[400 Bad Request]
    F -- Yes --> H{Filename ends with .dump?}
    H -- No --> I[400 Bad Request]
    H -- Yes --> J[fs.access check]
    J -- File missing --> K[404 Not Found]
    J -- File exists --> L[fs.stat + createReadStream]
    L --> M[Stream file as octet-stream response]
    style L fill:#f97316,color:#fff
Loading

Last reviewed commit: 5d1d7de

Comment on lines +56 to +58
const stat = await fs.stat(filePath);
const stream = createReadStream(filePath);
const webStream = Readable.toWeb(stream) as ReadableStream;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fs.stat and createReadStream unprotected after fs.access check

The fs.access check at line 51 is wrapped in a try/catch that returns a clean 404, but fs.stat and createReadStream (lines 56–58) are outside any error handler. If the .dump file is removed between the fs.access check and the subsequent calls (e.g., during a concurrent backup cleanup), fs.stat will throw an unhandled exception and Next.js will return a 500 error instead of a graceful 404.

The simpler fix is to drop the redundant fs.access check and use fs.stat directly inside the try/catch:

Suggested change
const stat = await fs.stat(filePath);
const stream = createReadStream(filePath);
const webStream = Readable.toWeb(stream) as ReadableStream;
let stat: Awaited<ReturnType<typeof fs.stat>>;
try {
stat = await fs.stat(filePath);
} catch {
return new Response("Backup not found", { status: 404 });
}
const stream = createReadStream(filePath);
const webStream = Readable.toWeb(stream) as ReadableStream;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/backups/[filename]/download/route.ts
Line: 56-58

Comment:
**`fs.stat` and `createReadStream` unprotected after `fs.access` check**

The `fs.access` check at line 51 is wrapped in a try/catch that returns a clean 404, but `fs.stat` and `createReadStream` (lines 56–58) are outside any error handler. If the `.dump` file is removed between the `fs.access` check and the subsequent calls (e.g., during a concurrent backup cleanup), `fs.stat` will throw an unhandled exception and Next.js will return a 500 error instead of a graceful 404.

The simpler fix is to drop the redundant `fs.access` check and use `fs.stat` directly inside the try/catch:

```suggestion
  let stat: Awaited<ReturnType<typeof fs.stat>>;
  try {
    stat = await fs.stat(filePath);
  } catch {
    return new Response("Backup not found", { status: 404 });
  }

  const stream = createReadStream(filePath);
  const webStream = Readable.toWeb(stream) as ReadableStream;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +3 to +13
export function highlightMatch(text: string, search: string): ReactNode {
if (!search) return text;
const idx = text.toLowerCase().indexOf(search.toLowerCase());
if (idx === -1) return text;
return (
<>
{text.slice(0, idx)}
<mark className="bg-yellow-500/30 text-yellow-200 rounded-sm px-0.5">{text.slice(idx, idx + search.length)}</mark>
{text.slice(idx + search.length)}
</>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

highlightMatch only highlights the first occurrence

The current implementation uses indexOf and stops after the first match. A log message like "ERROR: failed connection, error code 500" searched for "error" would highlight only the first occurrence, leaving subsequent matches unhighlighted while all of them appeared in the filtered results.

Consider iterating over all occurrences by replacing the single-indexOf approach with a loop that accumulates all match segments before returning them as a React fragment.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/log-search-utils.tsx
Line: 3-13

Comment:
**`highlightMatch` only highlights the first occurrence**

The current implementation uses `indexOf` and stops after the first match. A log message like `"ERROR: failed connection, error code 500"` searched for `"error"` would highlight only the first occurrence, leaving subsequent matches unhighlighted while all of them appeared in the filtered results.

Consider iterating over all occurrences by replacing the single-`indexOf` approach with a loop that accumulates all match segments before returning them as a React fragment.

How can I resolve this? If you propose a fix, please make it concise.

@TerrifiedBug TerrifiedBug merged commit 0c25956 into main Mar 8, 2026
12 checks passed
@TerrifiedBug TerrifiedBug deleted the worktree-ui-polish-batch-2 branch March 8, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant