From 21c218b10721b9d04545bbcce25f5aa181d6fcf7 Mon Sep 17 00:00:00 2001 From: Fedacking Date: Mon, 16 Mar 2026 19:48:23 -0300 Subject: [PATCH 1/2] Show extra host links on preview pages Read extraHosts from preview-config.nix metadata files and expose them as extra_urls on the Preview API response. The previews list shows compact inline links and the detail page shows them as buttons. --- dashboard/backend/src/models.rs | 7 ++ dashboard/backend/src/shell.rs | 36 ++++++++- dashboard/frontend/src/lib/api.ts | 6 ++ .../frontend/src/pages/PreviewDetail.tsx | 33 ++++++-- dashboard/frontend/src/pages/Previews.tsx | 77 ++++++++++++------- 5 files changed, 121 insertions(+), 38 deletions(-) diff --git a/dashboard/backend/src/models.rs b/dashboard/backend/src/models.rs index fcc9ed6..ac492e1 100644 --- a/dashboard/backend/src/models.rs +++ b/dashboard/backend/src/models.rs @@ -52,12 +52,19 @@ pub struct GitHubUserInfo { // ── Previews ── +#[derive(Debug, Serialize, Clone)] +pub struct ExtraUrl { + pub label: String, + pub url: String, +} + #[derive(Debug, Serialize, Clone)] pub struct Preview { pub slug: String, pub repo: String, pub branch: String, pub url: String, + pub extra_urls: Vec, } #[derive(Debug, Deserialize)] diff --git a/dashboard/backend/src/shell.rs b/dashboard/backend/src/shell.rs index ce0b886..5b9439e 100644 --- a/dashboard/backend/src/shell.rs +++ b/dashboard/backend/src/shell.rs @@ -6,7 +6,7 @@ use tokio::sync::broadcast; use crate::config::Config; use crate::error::AppError; -use crate::models::Preview; +use crate::models::{ExtraUrl, Preview}; /// A structured action extracted from Claude's stream-json output. #[derive(Debug, Clone)] @@ -139,7 +139,34 @@ pub async fn list_previews(config: &Config) -> Result, AppError> { parse_preview_list(&clean, &config.preview_domain) } -fn parse_preview_list(output: &str, _domain: &str) -> Result, AppError> { +/// Read the `.meta` file for a preview and extract extra host URLs. +fn read_extra_urls(slug: &str, domain: &str) -> Vec { + let meta_path = format!("/var/lib/preview-deploys/{slug}.meta"); + let content = match std::fs::read_to_string(&meta_path) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + let meta: serde_json::Value = match serde_json::from_str(&content) { + Ok(v) => v, + Err(_) => return Vec::new(), + }; + let extra_hosts = match meta.get("extraHosts").and_then(|v| v.as_array()) { + Some(hosts) => hosts, + None => return Vec::new(), + }; + extra_hosts + .iter() + .filter_map(|h| { + let prefix = h.get("prefix")?.as_str()?; + Some(ExtraUrl { + label: prefix.to_string(), + url: format!("https://{prefix}-{slug}.{domain}"), + }) + }) + .collect() +} + +fn parse_preview_list(output: &str, domain: &str) -> Result, AppError> { let mut previews = Vec::new(); let preview_dir = "/var/lib/preview-deploys"; @@ -171,11 +198,14 @@ fn parse_preview_list(output: &str, _domain: &str) -> Result, AppEr }) .unwrap_or_default(); + let extra_urls = read_extra_urls(&slug, domain); + previews.push(Preview { slug, repo, branch, url, + extra_urls, }); } } @@ -812,6 +842,8 @@ SLUG STATUS BRANCH URL assert_eq!(result[0].slug, "123"); assert_eq!(result[0].branch, "feat/foo"); assert_eq!(result[0].url, "https://123.example.com"); + // extra_urls will be empty since .meta files don't exist in test + assert!(result[0].extra_urls.is_empty()); assert_eq!(result[1].slug, "456"); } diff --git a/dashboard/frontend/src/lib/api.ts b/dashboard/frontend/src/lib/api.ts index d95c7cd..262416c 100644 --- a/dashboard/frontend/src/lib/api.ts +++ b/dashboard/frontend/src/lib/api.ts @@ -4,11 +4,17 @@ export interface UserInfo { role: string; } +export interface ExtraUrl { + label: string; + url: string; +} + export interface Preview { slug: string; repo: string; branch: string; url: string; + extra_urls: ExtraUrl[]; } export interface Task { diff --git a/dashboard/frontend/src/pages/PreviewDetail.tsx b/dashboard/frontend/src/pages/PreviewDetail.tsx index 0e63137..f8869e1 100644 --- a/dashboard/frontend/src/pages/PreviewDetail.tsx +++ b/dashboard/frontend/src/pages/PreviewDetail.tsx @@ -1,8 +1,10 @@ import { useEffect, useState, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import { ChevronLeft, ExternalLink } from 'lucide-react'; import LogViewer from '@/components/LogViewer'; -import { getConfig } from '@/lib/api'; +import { getConfig, listPreviews } from '@/lib/api'; +import type { ExtraUrl } from '@/lib/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -19,6 +21,13 @@ export default function PreviewDetail() { }).catch(() => {}); }, []); + const { data: previews } = useQuery({ + queryKey: ['previews'], + queryFn: listPreviews, + }); + + const extraUrls: ExtraUrl[] = previews?.find((p) => p.slug === slug)?.extra_urls ?? []; + const handleConnectionChange = useCallback((c: boolean) => setConnected(c), []); const previewUrl = slug && previewDomain ? `https://${slug}.${previewDomain}` : ''; @@ -34,12 +43,22 @@ export default function PreviewDetail() { {connected ? 'Live' : 'Disconnected'} - +
+ + {extraUrls.map((eu) => ( + + ))} +
diff --git a/dashboard/frontend/src/pages/Previews.tsx b/dashboard/frontend/src/pages/Previews.tsx index 6e16778..fb1a232 100644 --- a/dashboard/frontend/src/pages/Previews.tsx +++ b/dashboard/frontend/src/pages/Previews.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; +import { ExternalLink } from 'lucide-react'; import { listPreviews, createPreview, destroyPreview } from '@/lib/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -156,37 +157,55 @@ export default function Previews() { ) : (
{filteredPreviews.map((p) => ( -
-
-
- {p.slug} - {p.repo} - {p.branch} +
+
+
+
+ {p.slug} + {p.repo} + {p.branch} +
+
+
+ + +
-
- - - -
+ {p.extra_urls?.length > 0 && ( +
+ {p.extra_urls.map((eu) => ( + + {eu.label} + + + ))} +
+ )}
))}
From 310b7b5f9971a32c59340aebdcf2196a39186738 Mon Sep 17 00:00:00 2001 From: Fedacking Date: Mon, 16 Mar 2026 20:06:59 -0300 Subject: [PATCH 2/2] Show extra host links in task preview tab Fetch preview metadata to display extra host URLs (admin, demo, etc.) alongside the main preview URL in the task detail preview tab header. --- dashboard/frontend/src/pages/TaskDetail.tsx | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/dashboard/frontend/src/pages/TaskDetail.tsx b/dashboard/frontend/src/pages/TaskDetail.tsx index a7b2635..63d723d 100644 --- a/dashboard/frontend/src/pages/TaskDetail.tsx +++ b/dashboard/frontend/src/pages/TaskDetail.tsx @@ -27,6 +27,7 @@ import { getTask, listSubtasks, listTaskActions, + listPreviews, getMe, parseImageUrls, reopenTask, @@ -88,6 +89,14 @@ export default function TaskDetail() { staleTime: 30_000, }); + const { data: previews } = useQuery({ + queryKey: ['previews'], + queryFn: listPreviews, + enabled: !!task?.preview_slug, + }); + + const previewExtraUrls = previews?.find((p) => p.slug === task?.preview_slug)?.extra_urls ?? []; + const onConnectionChange = useCallback((c: boolean) => setConnected(c), []); const queryClient = useQueryClient(); @@ -336,17 +345,29 @@ export default function TaskDetail() { {/* Preview tab — embedded iframe of the deployed preview */} {task?.preview_url && ( -
+
{task.preview_url} - Open in new tab + Open + {previewExtraUrls.map((eu) => ( + + + {eu.label} + + ))}