refactor: unify into single muyue binary with embedded desktop mode
All checks were successful
Beta Release / beta (push) Successful in 37s
All checks were successful
Beta Release / beta (push) Successful in 37s
- Merge muyue + muyue-desktop into one binary (13MB) - `muyue` starts TUI, `muyue desktop` launches web UI in browser - Move frontend from cmd/muyue-desktop/frontend/ to web/ (standard Go layout) - Add web/embed.go with //go:embed all:dist for frontend assets - Add internal/desktop/ package (server, browser open, SPA routing, signals) - Split internal/api/api.go into server.go + handlers.go - Add internal/desktop/desktop.go with SPA fallback and --port/--no-open flags - Clean package.json: remove unused @xterm/xterm, switch to ESM - Fix vite.config.js proxy to use port 8095 for dev mode - Add Makefile targets: frontend, desktop, dev-desktop - Update all CI workflows: single binary build, web/ paths - Remove cmd/muyue-desktop/ entirely 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
121
web/src/components/Dashboard.jsx
Normal file
121
web/src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ tools, updates, api, onRescan }) {
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [installLog, setInstallLog] = useState([])
|
||||
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
const total = tools.length
|
||||
const pct = total > 0 ? (installed / total) * 100 : 0
|
||||
const missing = tools.filter(t => !t.installed).map(t => t.Name || t.name)
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (missing.length === 0) return
|
||||
setInstalling(true)
|
||||
setInstallLog(prev => [...prev, { text: `Installing ${missing.length} tools...`, type: 'info' }])
|
||||
try {
|
||||
await api.installTools(missing)
|
||||
setInstallLog(prev => [...prev, { text: 'Install started. Rescan to see changes.', type: 'ok' }])
|
||||
const data = await api.runScan()
|
||||
const toolData = await api.getTools()
|
||||
onRescan(toolData.tools || [])
|
||||
} catch (err) {
|
||||
setInstallLog(prev => [...prev, { text: err.message, type: 'error' }])
|
||||
}
|
||||
setInstalling(false)
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
await api.runScan()
|
||||
const data = await api.getTools()
|
||||
onRescan(data.tools || [])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">System</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ color: 'var(--text-main)' }}>{installed}/{total} tools installed</span>
|
||||
</div>
|
||||
|
||||
<div className="section-header">Installed Tools</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
{tools.map((t, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${t.installed ? 'ok' : 'missing'}`}>
|
||||
{t.installed ? '[OK]' : '[--]'}
|
||||
</span>
|
||||
<span className="tool-name">{t.Name || t.name}</span>
|
||||
{(t.Version || t.version) && (
|
||||
<span className="tool-version">{extractVersion(t.Version || t.version)}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="progress-bar" style={{ marginBottom: 16 }}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
{installing && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span className="loading-spinner"> Installing...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{installLog.length > 0 && (
|
||||
<div>
|
||||
<div className="section-header">Install Log</div>
|
||||
{installLog.map((log, i) => (
|
||||
<div key={i} style={{
|
||||
color: log.type === 'error' ? 'var(--error)' :
|
||||
log.type === 'ok' ? 'var(--success)' : 'var(--text-dim)',
|
||||
fontSize: 12, padding: '2px 0'
|
||||
}}>
|
||||
{log.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">Quick Actions</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
||||
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
||||
[i] Install missing ({missing.length})
|
||||
</button>
|
||||
<button onClick={() => api.getUpdates().then(d => {})}> [u] Check updates</button>
|
||||
<button onClick={handleScan}>[s] Rescan system</button>
|
||||
<button onClick={() => api.configureMCP()}>[m] Configure MCP</button>
|
||||
</div>
|
||||
|
||||
<div className="section-header">Updates</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{updates.length === 0 ? (
|
||||
<span style={{ color: 'var(--text-muted)' }}>No update data yet</span>
|
||||
) : updates.map((u, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${u.needsUpdate ? 'missing' : 'ok'}`}>
|
||||
{u.needsUpdate ? '[!!]' : '[OK]'}
|
||||
</span>
|
||||
<span className="tool-name">{u.tool}</span>
|
||||
{u.needsUpdate && (
|
||||
<span style={{ color: 'var(--warning)', fontSize: 11 }}>
|
||||
{u.current} → {u.latest}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function extractVersion(s) {
|
||||
if (!s) return ''
|
||||
const m = s.match(/\d+\.\d+\.\d+/)
|
||||
return m ? m[0] : s.slice(0, 12)
|
||||
}
|
||||
Reference in New Issue
Block a user