feat(tg): Mini App V6 — premium overhaul (#609)

* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs)

Design system: native type with tabular-numeral heroes (mono demoted to
the wordmark), borderless elevated cards, floating dock, Telegram
window-chrome painting via the theme bridge; Inbox moves behind a header
bell with humanized notifications (UUIDs resolve to task names).

Chat: honest Mine/Fleet split — participant-scoped CEO threads with real
unread counts and mark-read, watched fleet threads with reply-as-CEO on
task-linked conversations (watch-only otherwise), markdown transcripts,
live pulse flashes, and a pinned Secretary live chat on the panel's SSE
session runtime.

Metrics: new tab with period-segmented spend hero, by-agent/team/model
breakdowns, delivery + efficiency health, and a per-agent drilldown over
usage time-series (agent_slug) + member scorecard.

Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper;
task sheet gains the CEO decide verbs (approve / request changes /
unblock).

Security: /api/dashboard router now require_panel_token-gated at router
level (mirrors /api/usage), closing unauthenticated metrics exposure.

* fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars

The mono returns as the numeral/brand voice (.tg-display — heroes, stat
values, wordmark) while labels stay native sentence case. The hand-drawn
duotone glyphs and lucide feature icons are replaced by Phosphor (MIT):
duotone at rest via an IconContext at the shell, filled weight on the
dock's active tab; row glyph maps (board statuses, inbox kinds, approval
kinds, quick actions) all move over. Team avatar tiles drop their borders
— tint-only squircles.

* fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap

* polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing

Applied the design-taste audit against the cockpit: every em-dash in
visible UI copy rewritten (periods/commas/colons), the remaining lucide
chrome (carets, arrows, send, close, spinners) moved to Phosphor so the
tg tree ships one icon family (send is the native paper-plane, carets
bold), the hand-rolled chevron SVG deleted, and metadata lines rationed
to a single middle-dot separator.

* polish(tg): pipeline chip strip scrolls without a visible scrollbar

* fix(tests): metrics observability fixture uses a relative timestamp

The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window
exactly 30 days later, detonating the suite on every branch. Two days
back from now() stays inside every window (30d metrics, 7d scorecards)
permanently.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-20 17:29:22 +02:00
committed by GitHub
co-authored by Renn F
parent cd73ad6a74
commit 3c5ee46347
41 changed files with 4227 additions and 812 deletions
@@ -29,7 +29,9 @@ const CUT_LABELS: Record<VideoCut, string> = {
*/
function CutPlayer({ post }: { post: VideoPost }) {
const paths = post.mp4_paths ?? {};
const [cut, setCut] = useState<VideoCut>(paths.vertical ? "vertical" : "square");
const [cut, setCut] = useState<VideoCut>(
paths.vertical ? "vertical" : "square",
);
// url === null means the fetch for that cut failed; a stale entry for a
// different cut is simply ignored in render, so no synchronous state
// reset is needed when the cut changes.
@@ -87,7 +89,9 @@ function CutPlayer({ post }: { post: VideoPost }) {
playsInline
className={cn(
"w-full rounded-md bg-black",
cut === "vertical" ? "aspect-[9/16] max-h-[60dvh]" : "aspect-square",
cut === "vertical"
? "aspect-[9/16] max-h-[60dvh]"
: "aspect-square",
)}
/>
) : (
@@ -134,15 +138,15 @@ function CaptionEditor({
disabled={!editing}
onChange={(e) => onChange(e.target.value)}
rows={3}
className={cn("text-sm", editing && overLimit && "border-destructive")}
className={cn("text-sm", editing && overLimit && "border-rose-400/60")}
/>
<p
className={cn(
"text-right text-[11px] tabular-nums",
editing && overLimit ? "text-destructive" : "text-muted-foreground",
editing && overLimit ? "text-rose-400" : "text-muted-foreground",
)}
>
{caption.length}/{maxChars}
{caption.length} / {maxChars}
</p>
</div>
);
@@ -196,7 +200,8 @@ export function VideoPostDetail({
const reject = useMutation({
mutationFn: (reason: string) => videoApi.reject(post.task_id, reason),
onSuccess: () => finish(true, "Draft rejected — feedback goes back to the author."),
onSuccess: () =>
finish(true, "Draft rejected. Feedback goes back to the author."),
onError: (err) => {
haptics.error();
toast.error(getErrorMessage(err));
@@ -213,7 +218,9 @@ export function VideoPostDetail({
</Badge>
))}
{post.render_status === "failed" && (
<Badge variant="destructive">render failed</Badge>
<span className="rounded-full bg-rose-500/15 px-2 py-0.5 text-xs font-medium text-rose-300">
render failed
</span>
)}
</div>