Menu

#415 feat(dash): per-project tab title and topbar label

closed
nobody
None
2026-08-24
2026-08-19
Anonymous
No

Originally created by: QY-25123

Pull Request

Slice

  • Repo: doberman-core
  • Feature / Slice: dash / per-project-title — Per-project dashboard tab title
  • Plan reference: n/a (small UX fix, not tied to a plan slice)

What this PR does

doberman dash is already scoped to one repo per run (--path, default the current
directory — see create_app(repo_root=...)), but every browser tab rendered the identical
<title>Doberman Dashboard</title>, so running several dashboards side by side (one per
project) gave no way to tell the tabs apart. The tab title and the topbar wordmark now carry
the repo folder's name, e.g. widget-service — Doberman Dashboard.

  • _project_display_name(repo_root) derives the label from the resolved folder name.
  • _render_shell(repo_root) renders the HTML shell per create_app call instead of serving
    one static string, substituting the project name into <title>, the topbar, and the JS
    unread-count title update (document.title = "(N) " + DASH_BASE_TITLE).
  • The project name is HTML-escaped in markup and separately encoded via _js_string_literal
    (JSON-encode + unicode-escape <, >, &) before landing inside the inline <script>, so
    a folder literally named </script><script>... can't break out of the script element.
  • Appearance/labeling only — no new endpoints, no change to auth, redaction, or the decision
    path.

Tests added (run in CI)

  • tests/unit/test_dash_project_title.py — project name derived from the repo folder;
    <title>/topbar carry it; two different projects render different titles; the JS title
    update uses the project-qualified base title instead of the old hardcoded string; HTML
    special characters are escaped in markup; quotes are safely embedded in the JS string;
    _js_string_literal cannot be used to close the enclosing <script> tag; the bearer token
    still never appears in the shell.

Public-release safety (doberman-core only)

  • [x] Contains nothing from the "not allowed" list: no enterprise/hosted code, no proprietary detection, no customer data, no secrets, no commercial-license code
  • [x] Core still builds/tests/runs with NO enterprise package installed

Security checklist

  • [x] Fails closed on error / uncertainty (n/a — no new decision logic; this is a display-only change)
  • [x] No secret, full file, or unredacted prompt logged or committed
  • [x] Any guardrail/learning change is raise-only (no silent loosening) (n/a — no guardrail/learning change)
  • [x] Every BLOCK/AUTH carries reason codes + a human explanation (n/a — no verdict logic touched)
  • [x] doberman-core does not import doberman_enterprise

Edge cases covered / Deviations from plan / Risks introduced

  • Edge cases: HTML-special characters in the project folder name; embedded quotes; a
    </script>-style script-breakout attempt via the folder name; two dashboards for different
    projects open at once.
  • Deviations: none.
  • Risks: none — purely cosmetic labeling of an already per-project-scoped server.

Related

Tickets: #401
Tickets: #435
Tickets: #436
Tickets: #437
Tickets: #443
Tickets: #444
Tickets: #454

Discussion

  • Anonymous

    Anonymous - 2026-08-24

    Ticket changed by: fu351

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-08-24

    Originally posted by: fu351

    Merged, thanks @QY-25123! Escaping the project name separately for each context, html.escape for markup and _js_string_literal for the script body, is the correct treatment of the hard spot here, an untrusted string inside an inline script tag, and the </script> breakout test proves it. A dependency pin I merged put a changelog conflict on your branch, so I landed this through [#454] instead of asking you to rebase or touching your fork, your commits are in as you wrote them, app.py merged clean.

     

    Related

    Tickets: #454


Log in to post a comment.