Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/devbox/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func main() {
AuthPassword: cfg.Auth.Password,
AuthSessionTTL: cfg.Auth.SessionTTL,
LinksPath: cfg.Console.LinksPath,
BrowserDataPath: cfg.Console.BrowserDataPath,
BrowserInsecureTLS: cfg.Console.BrowserInsecureTLS,
}, col, appMgr, storeMgr)

go func() {
Expand Down
2 changes: 2 additions & 0 deletions console-ui/src/components/AppShell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DiskManager from '../pages/DiskManager'
import NetworkConnections from '../pages/NetworkConnections'
import MonitoringApp from '../pages/Monitoring'
import AIActivity from '../pages/AIActivity'
import BrowserFace from '../pages/Browser'

// Reusable face header
function FaceHeader({ accent = T.blue, title, subtitle, version, kb, onMgmt, extra, errorMode }) {
Expand Down Expand Up @@ -103,6 +104,7 @@ function AppShell({ appId, app, authed, onRequireAuth, onOpenManagement }) {
if (appId === 'network-connections') return <NetworkConnections/>;
if (appId === 'monitoring') return <MonitoringApp/>;
if (appId === 'ai-activity') return <AIActivity/>;
if (appId === 'browser') return <BrowserFace/>;
// Generic iframe fallback for any installed app with a HostPort
if (app) return <IframeFace app={app} onMgmt={onOpenManagement}/>;
return null;
Expand Down
1 change: 1 addition & 0 deletions console-ui/src/data/systemApps.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const SYSTEM_APPS = [
{ id: 'alerts', kind: 'system', name: '告警中心', icon: 'bell', color: T.amber, bg: 'linear-gradient(160deg,#fbbf24,#d97706)' },
{ id: 'audit', kind: 'system', name: '操作日志', icon: 'shield', color: '#7c3aed', bg: 'linear-gradient(160deg,#a78bfa,#5b21b6)' },
{ id: 'settings', kind: 'system', name: '系统设置', icon: 'gear', color: T.slate, bg: 'linear-gradient(160deg,#64748b,#334155)' },
{ id: 'browser', kind: 'system', name: '浏览器', icon: 'globe', color: T.blue, bg: 'linear-gradient(160deg,#3b82f6,#1d4ed8)' },
]
59 changes: 59 additions & 0 deletions console-ui/src/hooks/useApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,62 @@ export async function getLogs(appId, tail = 100) {
return '';
}
}

// ─── Browser 应用:书签 / 历史 ───────────────────────────────────
// 后端持久化在 /etc/devbox/browser.json(单机单用户一份)。增删后手动调
// 返回的 refresh() 重拉(不轮询——浏览器交互是用户驱动的,不需要定时刷)。

export function useBookmarks() {
return usePoll('/browser/bookmarks', { fallback: [] });
}

export function useHistory() {
return usePoll('/browser/history', { fallback: [] });
}

export async function addBookmark(title, url) {
const r = await authFetch(`${API}/browser/bookmarks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url }),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json().catch(() => null);
}

export async function removeBookmark(id) {
const r = await authFetch(`${API}/browser/bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json().catch(() => null);
}

// 记录访问:fire-and-forget,失败不影响导航
export async function addHistory(url, title) {
try {
await authFetch(`${API}/browser/history`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, title }),
});
} catch { /* ignore */ }
}

export async function clearHistory() {
const r = await authFetch(`${API}/browser/history`, { method: 'DELETE' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json().catch(() => null);
}

// 探测目标 URL 能否被 iframe 直连(后端 HEAD 检测 X-Frame-Options / CSP frame-ancestors)。
// 导航前调:能直连就 iframe 直连(快、无副作用),检测到拦截头才走代理。
// 任何探测失败/异常默认 direct=true(交回前端直连,由浏览器自然报错)。
export async function probeDirectEmbed(url) {
try {
const r = await authFetch(`${API}/browser/probe?url=${encodeURIComponent(url)}`);
if (!r.ok) return { direct: true, reason: 'probe-failed' };
const d = await r.json();
return d && typeof d.direct === 'boolean' ? d : { direct: true, reason: 'unknown' };
} catch {
return { direct: true, reason: 'error' };
}
}
Loading