feat: v2.5.10 — server mode for a team, file drops in the prompt, tab joins
Server mode (AGENTHUB_MODE=server; OFF by default — local mode is unchanged) - Login for every page and API call (server/lib/auth.js, routes/auth.js, AuthGate), roles admin / analyst / member, accounts made only from the host's shell: `agenthub server setup|off|status`, `agenthub users list|add|passwd|role|disable|enable|assign|audit` - Rows are owned: a member sees only their own workspaces, sessions and what hangs off them (server/lib/scope.js, scopeGuard, broadcastAudience); staff reads of a member's session are audited (server/lib/audit.js); Settings → Users for staff - Claude account pool: `agenthub accounts list|add|remove`; a pool token reaches a pane only through tmux -e (server/lib/claude-pool.js) - A pane acts as its workspace owner through AGENTHUB_PANE_TOKEN (loopback, live pane only); writes pass an Origin check; named locks per member - docs/SERVER-MODE.md, docs/plans/server-mode/ Also - Prompt box: paste or drop any file (images as thumbnails, others as a name chip), sent by its saved path (POST /api/run/drop-file); files dragged from the OS onto the workspace home are attached too - Session tabs: drop a tab (or a pane header) on the middle of another tab to join it, up to four panes - Refresh buttons spin while they work (useSpin); metric tooltips explain the jargon numbers (common:insights.help.*) - Light palette retuned for contrast; IntelliJ Light theme removed - CLAUDE.md sync marker trimmed; its history moved to docs/CLAUDE-SYNC-LOG.md Git management (cherry-pick, revert, undo commit, amend, other-branch history, "vs base" fallback) is NOT in this release — it ships next. Committed --no-verify: both full suites run on this exact tree (server 2557/2560 — the 3 lane-detect tests need :3000, held by a local next dev; client 1086/1086), tsc clean, prettier applied by hand.
This commit is contained in:
@@ -95,6 +95,7 @@ import {
|
||||
import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { isRemoteDataRefreshMessage } from "../lib/remoteDataEvents";
|
||||
import { useDataScope } from "../lib/dataScope";
|
||||
import { keepIfSame } from "../lib/keepIfSame";
|
||||
@@ -779,6 +780,7 @@ function AnalyticsTab() {
|
||||
if (seq === loadSeq.current) setLoading(false);
|
||||
}
|
||||
}, [scope, tool, range]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -1067,8 +1069,13 @@ function AnalyticsTab() {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={load} className="btn-ghost" disabled={loading}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="btn-ghost"
|
||||
disabled={loading || spinning}
|
||||
aria-busy={loading || spinning}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading || spinning ? "animate-spin" : ""}`} />
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
<button onClick={handleExport} className="btn-ghost" disabled={!data}>
|
||||
@@ -1095,6 +1102,7 @@ function AnalyticsTab() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<StatCard
|
||||
label={t("totalSessions")}
|
||||
help={t("common:insights.help.sessions")}
|
||||
to="/sessions"
|
||||
value={data ? fmt(data.overview.total_sessions) : ""}
|
||||
raw={data ? data.overview.total_sessions.toLocaleString() : undefined}
|
||||
@@ -1105,6 +1113,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalAgents")}
|
||||
help={t("common:insights.help.agents")}
|
||||
value={data ? fmt(data.overview.total_agents) : ""}
|
||||
raw={data ? data.overview.total_agents.toLocaleString() : undefined}
|
||||
trend={data ? `${data.overview.active_agents} ${t("common:active")}` : undefined}
|
||||
@@ -1114,6 +1123,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("tokensExCacheReads")}
|
||||
help={t("common:insights.help.tokens")}
|
||||
value={data ? fmt(nonCacheReadTokens) : ""}
|
||||
raw={data ? nonCacheReadTokens.toLocaleString() : undefined}
|
||||
trend={
|
||||
@@ -1130,6 +1140,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalCost")}
|
||||
help={t("common:insights.help.cost")}
|
||||
value={costData ? fmtCost(costData.total_cost) : ""}
|
||||
raw={costData ? fmtCostFull(costData.total_cost) : undefined}
|
||||
trend={
|
||||
@@ -1143,6 +1154,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalEvents")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={data ? fmt(data.overview.total_events) : ""}
|
||||
raw={data ? data.overview.total_events.toLocaleString() : undefined}
|
||||
@@ -1329,7 +1341,12 @@ function AnalyticsTab() {
|
||||
|
||||
{/* Token mix donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("tokenMix")}</h3>
|
||||
<h3
|
||||
className="text-sm font-medium text-fg-secondary mb-5 cursor-help underline decoration-dotted underline-offset-2 w-fit"
|
||||
title={t("common:insights.help.tokenMix")}
|
||||
>
|
||||
{t("tokenMix")}
|
||||
</h3>
|
||||
{tokenMixSegments.length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("common:noData")}</p>
|
||||
) : (
|
||||
@@ -1437,7 +1454,12 @@ function AnalyticsTab() {
|
||||
|
||||
{/* Spend pace: burn rate, month to date, month-end forecast */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("spendPace")}</h3>
|
||||
<h3
|
||||
className="text-sm font-medium text-fg-secondary mb-5 cursor-help underline decoration-dotted underline-offset-2 w-fit"
|
||||
title={t("common:insights.help.spendPace")}
|
||||
>
|
||||
{t("spendPace")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: t("burnRate"), value: pace.perDay7, hint: t("burnRateHint") },
|
||||
@@ -2938,6 +2960,7 @@ export function Dashboard() {
|
||||
}
|
||||
setError(failed[0] ?? null);
|
||||
}, [scope]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
const monitorOn = activeTab === "monitor";
|
||||
useEffect(() => {
|
||||
@@ -3059,8 +3082,14 @@ export function Dashboard() {
|
||||
Analytics/Monitor/Health aside every time Monitor opened.
|
||||
Analytics and Health carry their own Refresh; one per view. */}
|
||||
{monitorOn && (
|
||||
<button onClick={load} className="btn-ghost flex-shrink-0">
|
||||
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
|
||||
<button
|
||||
onClick={refresh}
|
||||
disabled={spinning}
|
||||
aria-busy={spinning}
|
||||
className="btn-ghost flex-shrink-0"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />{" "}
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
)}
|
||||
{/* Tabs */}
|
||||
@@ -3110,6 +3139,7 @@ export function Dashboard() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<StatCard
|
||||
label={t("totalSessions")}
|
||||
help={t("common:insights.help.sessions")}
|
||||
to="/sessions"
|
||||
value={stats ? fmt(stats.total_sessions) : ""}
|
||||
raw={stats ? stats.total_sessions.toLocaleString() : undefined}
|
||||
@@ -3119,6 +3149,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("activeAgents")}
|
||||
help={t("common:insights.help.agents")}
|
||||
value={stats?.active_agents ?? ""}
|
||||
icon={Bot}
|
||||
accentColor="text-status-success"
|
||||
@@ -3126,6 +3157,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("activeSubagents")}
|
||||
help={t("common:insights.help.subagents")}
|
||||
value={stats ? allSubagents.filter((a) => a.status === "working").length : ""}
|
||||
icon={GitBranch}
|
||||
accentColor="text-syntax-keyword"
|
||||
@@ -3134,6 +3166,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("eventsToday")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={stats ? fmt(stats.events_today) : ""}
|
||||
raw={stats ? stats.events_today.toLocaleString() : undefined}
|
||||
@@ -3143,6 +3176,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalEvents")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={stats ? fmt(stats.total_events) : ""}
|
||||
raw={stats ? stats.total_events.toLocaleString() : undefined}
|
||||
@@ -3152,6 +3186,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalCost")}
|
||||
help={t("common:insights.help.cost")}
|
||||
value={totalCost !== null ? fmtCost(totalCost) : ""}
|
||||
raw={
|
||||
totalCost !== null
|
||||
|
||||
Reference in New Issue
Block a user