Compare commits
1 Commits
mimo/code/
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73bc86d3a2 |
110
app.js
110
app.js
@@ -2516,46 +2516,24 @@ function populateAtlas() {
|
||||
const grid = document.getElementById('atlas-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Counters by status
|
||||
let onlineCount = 0;
|
||||
let standbyCount = 0;
|
||||
let rebuildingCount = 0;
|
||||
let localCount = 0;
|
||||
let blockedCount = 0;
|
||||
|
||||
// Group portals by environment
|
||||
const byEnv = { production: [], staging: [], local: [] };
|
||||
|
||||
portals.forEach(portal => {
|
||||
const config = portal.config;
|
||||
const status = config.status || 'online';
|
||||
const env = config.environment || 'production';
|
||||
if (config.status === 'online') onlineCount++;
|
||||
if (config.status === 'standby') standbyCount++;
|
||||
|
||||
// Count statuses
|
||||
if (status === 'online' || status === 'active') onlineCount++;
|
||||
else if (status === 'standby') standbyCount++;
|
||||
else if (status === 'rebuilding') rebuildingCount++;
|
||||
else if (status === 'local-only') localCount++;
|
||||
else if (status === 'blocked') blockedCount++;
|
||||
|
||||
// Group by environment
|
||||
if (byEnv[env]) {
|
||||
byEnv[env].push({ config, status });
|
||||
} else {
|
||||
byEnv['production'].push({ config, status });
|
||||
}
|
||||
|
||||
// Create atlas card
|
||||
const card = document.createElement('div');
|
||||
card.className = 'atlas-card';
|
||||
card.style.setProperty('--portal-color', config.color);
|
||||
|
||||
const statusClass = `status-${status}`;
|
||||
const statusClass = `status-${config.status || 'online'}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="atlas-card-header">
|
||||
<div class="atlas-card-name">${config.name}</div>
|
||||
<div class="atlas-card-status ${statusClass}">${status.toUpperCase()}</div>
|
||||
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
|
||||
</div>
|
||||
<div class="atlas-card-desc">${config.description}</div>
|
||||
<div class="atlas-card-footer">
|
||||
@@ -2572,18 +2550,9 @@ function populateAtlas() {
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Update footer counts
|
||||
document.getElementById('atlas-online-count').textContent = onlineCount;
|
||||
document.getElementById('atlas-standby-count').textContent = standbyCount;
|
||||
document.getElementById('atlas-rebuilding-count').textContent = rebuildingCount;
|
||||
document.getElementById('atlas-local-count').textContent = localCount;
|
||||
document.getElementById('atlas-blocked-count').textContent = blockedCount;
|
||||
|
||||
// Populate status wall by environment
|
||||
populateStatusWallEnv('production', byEnv.production);
|
||||
populateStatusWallEnv('staging', byEnv.staging);
|
||||
populateStatusWallEnv('local', byEnv.local);
|
||||
|
||||
|
||||
// Update Bannerlord HUD status
|
||||
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
|
||||
if (bannerlord) {
|
||||
@@ -2592,75 +2561,6 @@ function populateAtlas() {
|
||||
}
|
||||
}
|
||||
|
||||
function populateStatusWallEnv(envName, portalList) {
|
||||
const container = document.getElementById(`${envName}-portals`);
|
||||
const summary = document.getElementById(`${envName}-summary`);
|
||||
container.innerHTML = '';
|
||||
|
||||
if (portalList.length === 0) {
|
||||
container.innerHTML = '<div class="status-portal-row"><span class="status-portal-name" style="font-style: italic; color: rgba(160,184,208,0.4);">No worlds</span></div>';
|
||||
summary.textContent = 'No portals in this environment';
|
||||
return;
|
||||
}
|
||||
|
||||
// Count statuses in this environment
|
||||
const statusCounts = {};
|
||||
portalList.forEach(({ config, status }) => {
|
||||
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
||||
});
|
||||
|
||||
// Create portal rows
|
||||
portalList.forEach(({ config, status }) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'status-portal-row';
|
||||
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = `status-portal-indicator status-dot ${status}`;
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'status-portal-name';
|
||||
nameSpan.textContent = config.name;
|
||||
|
||||
const statusSpan = document.createElement('span');
|
||||
statusSpan.style.fontSize = '9px';
|
||||
statusSpan.style.textTransform = 'uppercase';
|
||||
statusSpan.style.marginLeft = '8px';
|
||||
statusSpan.style.color = getStatusColor(status);
|
||||
statusSpan.textContent = status;
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(statusSpan);
|
||||
row.appendChild(indicator);
|
||||
container.appendChild(row);
|
||||
});
|
||||
|
||||
// Create summary
|
||||
const summaryParts = Object.entries(statusCounts).map(([status, count]) =>
|
||||
`${count} ${status}`
|
||||
);
|
||||
summary.textContent = summaryParts.join(' · ');
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case 'active':
|
||||
return 'var(--color-primary)';
|
||||
case 'standby':
|
||||
return 'var(--color-gold)';
|
||||
case 'rebuilding':
|
||||
return '#ffa500';
|
||||
case 'local-only':
|
||||
return '#00ff88';
|
||||
case 'blocked':
|
||||
return '#ff0000';
|
||||
case 'offline':
|
||||
return 'var(--color-danger)';
|
||||
default:
|
||||
return 'var(--color-text-muted)';
|
||||
}
|
||||
}
|
||||
|
||||
function focusPortal(portal) {
|
||||
// Teleport player to a position in front of the portal
|
||||
const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));
|
||||
|
||||
151
docs/BANNERLORD_LOCAL_MAC.md
Normal file
151
docs/BANNERLORD_LOCAL_MAC.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Bannerlord Local Mac Setup
|
||||
|
||||
> **Status:** READY FOR TESTING
|
||||
> **Platform:** macOS (Apple Silicon / Intel)
|
||||
> **Source:** GOG (not Steam)
|
||||
> **Last Updated:** 2026-04-10
|
||||
|
||||
## Problem
|
||||
|
||||
Bannerlord is a Windows game. Alexander has it from GOG on macOS.
|
||||
We need it running locally through emulation before the harness can observe it.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LOCAL BANNERLORD ON MAC │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ Bannerlord │ │ Emulator │ │ macOS Desktop │ │
|
||||
│ │ (GOG) │───►│ Wine/Whisky/ │───►│ (the screen) │ │
|
||||
│ │ │ │ CrossOver │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────────────┤ │
|
||||
│ │ Bannerlord Harness │ │
|
||||
│ │ ┌────────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ capture_ │ │ execute_ │ │ bannerlord│ │ │
|
||||
│ │ │ state() │ │ action() │ │ _local.py │ │ │
|
||||
│ │ └────────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ │ │ ▲ │ │ │
|
||||
│ │ ▼ │ ▼ │ │
|
||||
│ │ ┌─────────────────────────────────────────┐ │ │
|
||||
│ │ │ MCP Servers (desktop-control) │ │ │
|
||||
│ │ │ Screenshots + keyboard/mouse │ │ │
|
||||
│ │ └─────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Hermes WebSocket │ │
|
||||
│ │ Telemetry + ODA loop │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `scripts/bannerlord_launcher.sh` | Shell launcher — detects emulator + game, launches |
|
||||
| `nexus/bannerlord_local.py` | Python module — programmatic readiness + launch control |
|
||||
| `nexus/bannerlord_harness.py` | Existing harness — extended with `--local` and `--launch-local` |
|
||||
| `portals.json` | Portal metadata — updated with `local_launch` block |
|
||||
|
||||
## Emulator Priority
|
||||
|
||||
1. **Whisky** — `/Applications/Whisky.app` (preferred, best macOS integration)
|
||||
2. **CrossOver** — `/Applications/CrossOver.app` (good, paid)
|
||||
3. **Homebrew Wine** — `wine64` / `wine` on PATH (free, may need Rosetta on ARM)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Check Readiness
|
||||
|
||||
```bash
|
||||
# Shell
|
||||
./scripts/bannerlord_launcher.sh --check --verbose
|
||||
|
||||
# Python
|
||||
python3 -m nexus.bannerlord_local --check --json
|
||||
|
||||
# Through harness
|
||||
python3 -m nexus.bannerlord_harness --local --mock
|
||||
```
|
||||
|
||||
### Launch Game
|
||||
|
||||
```bash
|
||||
# Shell
|
||||
./scripts/bannerlord_launcher.sh --launch
|
||||
|
||||
# Python
|
||||
python3 -m nexus.bannerlord_local --launch --json
|
||||
|
||||
# Through harness (launches game, then runs ODA)
|
||||
python3 -m nexus.bannerlord_harness --launch-local --mock
|
||||
```
|
||||
|
||||
### Stop Game
|
||||
|
||||
```bash
|
||||
python3 -m nexus.bannerlord_local --stop
|
||||
```
|
||||
|
||||
## GOG Install Paths Searched
|
||||
|
||||
The launcher checks these paths in order:
|
||||
|
||||
1. `/Applications/Games/Mount & Blade II Bannerlord`
|
||||
2. `~/GOG Games/Mount and Blade II Bannerlord`
|
||||
3. `~/Games/Mount & Blade II Bannerlord`
|
||||
4. `/Applications/Mount & Blade II Bannerlord`
|
||||
5. `~/Library/Application Support/GOG.com/Galaxy/Applications/*/`
|
||||
6. Recursive `find` as last resort
|
||||
|
||||
The game must have `bin/Generic/Bannerlord.exe` relative to the install root.
|
||||
|
||||
## Portal Metadata
|
||||
|
||||
The `portals.json` bannerlord entry now includes:
|
||||
|
||||
```json
|
||||
"environment": "local",
|
||||
"local_launch": {
|
||||
"platform": "macos",
|
||||
"source": "gog",
|
||||
"emulator_required": true,
|
||||
"emulator_options": ["whisky", "crossover", "wine"],
|
||||
"launcher": "scripts/bannerlord_launcher.sh",
|
||||
"harness_bridge": "nexus/bannerlord_local.py",
|
||||
"check_command": "python3 -m nexus.bannerlord_local --check --json"
|
||||
}
|
||||
```
|
||||
|
||||
## Honest Status
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Launcher script | Written, needs Mac testing |
|
||||
| Python local module | Written, needs Mac testing |
|
||||
| Harness integration | Added `--local`/`--launch-local` flags |
|
||||
| Portal metadata | Updated |
|
||||
| MCP observation of emulated window | Untested — depends on emulator window visibility |
|
||||
| ODA loop with emulated game | Untested — needs game actually running |
|
||||
|
||||
## What Could Go Wrong
|
||||
|
||||
- **Emulator not installed:** User must install Whisky, CrossOver, or wine
|
||||
- **Game not found:** User must install GOG Bannerlord to a known path
|
||||
- **Performance:** Wine on Apple Silicon requires Rosetta + possible DXVK setup
|
||||
- **Window title:** The emulated window may not match "Mount & Blade II: Bannerlord" — the harness may need to detect the actual window title
|
||||
- **MCP desktop-control on macOS:** pyautogui on macOS needs Accessibility permissions
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Alexander runs `./scripts/bannerlord_launcher.sh --check --verbose` on his Mac
|
||||
2. If missing emulator, install Whisky (`brew install --cask whisky`)
|
||||
3. If missing game, install GOG Bannerlord
|
||||
4. Run `--launch` to verify the game opens
|
||||
5. Run `--launch-local --mock` to verify harness integration
|
||||
6. Test MCP screenshots of the emulated window
|
||||
46
index.html
46
index.html
@@ -264,57 +264,11 @@
|
||||
<div class="atlas-grid" id="atlas-grid">
|
||||
<!-- Portals will be injected here -->
|
||||
</div>
|
||||
<!-- Portal Status Wall -->
|
||||
<div class="atlas-status-wall">
|
||||
<div class="status-wall-header">
|
||||
<span class="status-wall-title">WORLD STATUS WALL</span>
|
||||
<span class="status-wall-subtitle">Real-time portal health</span>
|
||||
</div>
|
||||
<div class="status-wall-grid">
|
||||
<div class="status-wall-env" id="status-wall-production">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot production"></span>
|
||||
<span class="status-env-label">PRODUCTION</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="production-portals"></div>
|
||||
<div class="status-env-summary" id="production-summary"></div>
|
||||
</div>
|
||||
<div class="status-wall-env" id="status-wall-staging">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot staging"></span>
|
||||
<span class="status-env-label">STAGING</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="staging-portals"></div>
|
||||
<div class="status-env-summary" id="staging-summary"></div>
|
||||
</div>
|
||||
<div class="status-wall-env" id="status-wall-local">
|
||||
<div class="status-env-header">
|
||||
<span class="status-env-dot local"></span>
|
||||
<span class="status-env-label">LOCAL</span>
|
||||
</div>
|
||||
<div class="status-env-portals" id="local-portals"></div>
|
||||
<div class="status-env-summary" id="local-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-wall-legend">
|
||||
<div class="legend-item"><span class="status-dot online"></span> Online</div>
|
||||
<div class="legend-item"><span class="status-dot rebuilding"></span> Rebuilding</div>
|
||||
<div class="legend-item"><span class="status-dot local-only"></span> Local-only</div>
|
||||
<div class="legend-item"><span class="status-dot blocked"></span> Blocked</div>
|
||||
<div class="legend-item"><span class="status-dot offline"></span> Offline</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atlas-footer">
|
||||
<div class="atlas-status-summary">
|
||||
<span class="status-indicator online"></span> <span id="atlas-online-count">0</span> ONLINE
|
||||
|
||||
<span class="status-indicator standby"></span> <span id="atlas-standby-count">0</span> STANDBY
|
||||
|
||||
<span class="status-indicator rebuilding"></span> <span id="atlas-rebuilding-count">0</span> REBUILDING
|
||||
|
||||
<span class="status-indicator local-only"></span> <span id="atlas-local-count">0</span> LOCAL
|
||||
|
||||
<span class="status-indicator blocked"></span> <span id="atlas-blocked-count">0</span> BLOCKED
|
||||
</div>
|
||||
<div class="atlas-hint">Click a portal to focus or teleport</div>
|
||||
</div>
|
||||
|
||||
@@ -836,8 +836,43 @@ async def main():
|
||||
default=1.0,
|
||||
help="Delay between iterations in seconds (default: 1.0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--local",
|
||||
action="store_true",
|
||||
help="Check local macOS Bannerlord readiness before starting",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--launch-local",
|
||||
action="store_true",
|
||||
help="Launch local Bannerlord on macOS via emulator before ODA loop",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle local macOS Bannerlord
|
||||
if args.local or args.launch_local:
|
||||
try:
|
||||
from nexus.bannerlord_local import (
|
||||
check_local_readiness, launch_bannerlord, LocalStatus,
|
||||
)
|
||||
|
||||
state = check_local_readiness()
|
||||
log.info(f"Local check: {state.status.value}")
|
||||
log.info(f" Emulator: {state.emulator.name or 'none'}")
|
||||
log.info(f" Game: {state.game.game_dir or 'not found'}")
|
||||
log.info(f" Message: {state.message}")
|
||||
|
||||
if args.launch_local:
|
||||
if state.status == LocalStatus.READY:
|
||||
state = launch_bannerlord(state)
|
||||
log.info(f"Launch result: {state.status.value} — {state.message}")
|
||||
elif state.status == LocalStatus.RUNNING:
|
||||
log.info(f"Already running (PID: {state.process_id})")
|
||||
else:
|
||||
log.error(f"Cannot launch: {state.message}")
|
||||
return
|
||||
except ImportError:
|
||||
log.warning("bannerlord_local module not available — skipping local check")
|
||||
|
||||
# Create harness
|
||||
harness = BannerlordHarness(
|
||||
hermes_ws_url=args.hermes_ws,
|
||||
|
||||
394
nexus/bannerlord_local.py
Normal file
394
nexus/bannerlord_local.py
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bannerlord Local Manager — macOS Emulator Bridge
|
||||
|
||||
Detects and manages a local Bannerlord installation on macOS.
|
||||
Provides status queries, launch control, and process monitoring
|
||||
for the Bannerlord harness.
|
||||
|
||||
This module bridges the gap between:
|
||||
- The GamePortal Protocol (MCP-based observation/action)
|
||||
- A local GOG Bannerlord running through Wine/Whisky/CrossOver on macOS
|
||||
|
||||
The harness does NOT change — this module just manages the game process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
log = logging.getLogger("bannerlord.local")
|
||||
|
||||
|
||||
class EmulatorType(Enum):
|
||||
WHISKY = "whisky"
|
||||
CROSSOVER = "crossover"
|
||||
WINE = "wine"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class LocalStatus(Enum):
|
||||
READY = "ready"
|
||||
MISSING_EMULATOR = "missing_emulator"
|
||||
MISSING_GAME = "missing_game"
|
||||
RUNNING = "running"
|
||||
CRASHED = "crashed"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
# Standard GOG install paths on macOS
|
||||
GOG_SEARCH_PATHS = [
|
||||
Path("/Applications/Games/Mount & Blade II Bannerlord"),
|
||||
Path.home() / "GOG Games" / "Mount and Blade II Bannerlord",
|
||||
Path.home() / "Games" / "Mount & Blade II Bannerlord",
|
||||
Path("/Applications/Mount & Blade II Bannerlord"),
|
||||
]
|
||||
|
||||
BANNERLORD_EXE_RELATIVE = "bin/Generic/Bannerlord.exe"
|
||||
|
||||
LAUNCHER_SCRIPT = Path(__file__).parent.parent / "scripts" / "bannerlord_launcher.sh"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmulatorInfo:
|
||||
"""Detected Windows emulator on macOS."""
|
||||
name: str = ""
|
||||
path: str = ""
|
||||
emulator_type: EmulatorType = EmulatorType.UNKNOWN
|
||||
found: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameInstall:
|
||||
"""Detected Bannerlord GOG installation."""
|
||||
game_dir: str = ""
|
||||
game_exe: str = ""
|
||||
found: bool = False
|
||||
source: str = "" # "gog", "gog-galaxy", "manual"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalState:
|
||||
"""Full local Bannerlord state."""
|
||||
status: LocalStatus = LocalStatus.ERROR
|
||||
emulator: EmulatorInfo = field(default_factory=EmulatorInfo)
|
||||
game: GameInstall = field(default_factory=GameInstall)
|
||||
process_id: Optional[int] = None
|
||||
message: str = ""
|
||||
is_macos: bool = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"status": self.status.value,
|
||||
"emulator": {
|
||||
"name": self.emulator.name,
|
||||
"path": self.emulator.path,
|
||||
"type": self.emulator.emulator_type.value,
|
||||
"found": self.emulator.found,
|
||||
},
|
||||
"game": {
|
||||
"game_dir": self.game.game_dir,
|
||||
"game_exe": self.game.game_exe,
|
||||
"found": self.game.found,
|
||||
"source": self.game.source,
|
||||
},
|
||||
"process_id": self.process_id,
|
||||
"message": self.message,
|
||||
"is_macos": self.is_macos,
|
||||
}
|
||||
|
||||
|
||||
def detect_macos() -> bool:
|
||||
"""Check if running on macOS."""
|
||||
return platform.system() == "Darwin"
|
||||
|
||||
|
||||
def detect_emulator() -> EmulatorInfo:
|
||||
"""Find a Windows emulator on macOS."""
|
||||
info = EmulatorInfo()
|
||||
|
||||
# Whisky
|
||||
whisky_path = "/Applications/Whisky.app/Contents/Resources/Libraries/wine/bin/wine64"
|
||||
if os.path.isfile(whisky_path) and os.access(whisky_path, os.X_OK):
|
||||
info.name = "Whisky"
|
||||
info.path = whisky_path
|
||||
info.emulator_type = EmulatorType.WHISKY
|
||||
info.found = True
|
||||
return info
|
||||
|
||||
# CrossOver
|
||||
cx_path = "/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine"
|
||||
if os.path.isfile(cx_path) and os.access(cx_path, os.X_OK):
|
||||
info.name = "CrossOver"
|
||||
info.path = cx_path
|
||||
info.emulator_type = EmulatorType.CROSSOVER
|
||||
info.found = True
|
||||
return info
|
||||
|
||||
# Homebrew wine
|
||||
for candidate in ["wine64", "wine"]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["which", candidate],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
info.name = candidate
|
||||
info.path = result.stdout.strip()
|
||||
info.emulator_type = EmulatorType.WINE
|
||||
info.found = True
|
||||
return info
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
continue
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def detect_game() -> GameInstall:
|
||||
"""Find the Bannerlord GOG installation."""
|
||||
install = GameInstall()
|
||||
|
||||
# Check standard paths
|
||||
for path in GOG_SEARCH_PATHS:
|
||||
exe_path = path / BANNERLORD_EXE_RELATIVE
|
||||
if exe_path.is_file():
|
||||
install.game_dir = str(path)
|
||||
install.game_exe = str(exe_path)
|
||||
install.found = True
|
||||
install.source = "gog"
|
||||
return install
|
||||
|
||||
# Check GOG Galaxy paths
|
||||
galaxy_base = Path.home() / "Library/Application Support/GOG.com/Galaxy/Applications"
|
||||
if galaxy_base.is_dir():
|
||||
for child in galaxy_base.iterdir():
|
||||
candidate = child / "Mount & Blade II Bannerlord" / BANNERLORD_EXE_RELATIVE
|
||||
if candidate.is_file():
|
||||
install.game_dir = str(candidate.parent.parent)
|
||||
install.game_exe = str(candidate)
|
||||
install.found = True
|
||||
install.source = "gog-galaxy"
|
||||
return install
|
||||
|
||||
# Last resort: find
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["find", "/Applications", str(Path.home() / "GOG Games"),
|
||||
str(Path.home() / "Games"), "-name", "Bannerlord.exe",
|
||||
"-type", "f"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
first_line = result.stdout.strip().split("\n")[0]
|
||||
install.game_exe = first_line
|
||||
install.game_dir = str(Path(first_line).parent.parent)
|
||||
install.found = True
|
||||
install.source = "search"
|
||||
return install
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
return install
|
||||
|
||||
|
||||
def check_local_readiness() -> LocalState:
|
||||
"""Full local readiness check. Returns complete state."""
|
||||
state = LocalState()
|
||||
state.is_macos = detect_macos()
|
||||
|
||||
if not state.is_macos:
|
||||
state.status = LocalStatus.ERROR
|
||||
state.message = "Not macOS — local manager is Mac-only"
|
||||
return state
|
||||
|
||||
state.emulator = detect_emulator()
|
||||
if not state.emulator.found:
|
||||
state.status = LocalStatus.MISSING_EMULATOR
|
||||
state.message = "No Windows emulator found (install Whisky, CrossOver, or wine)"
|
||||
return state
|
||||
|
||||
state.game = detect_game()
|
||||
if not state.game.found:
|
||||
state.status = LocalStatus.MISSING_GAME
|
||||
state.message = "Bannerlord GOG installation not found in known paths"
|
||||
return state
|
||||
|
||||
# Check if already running
|
||||
pid = _read_pid()
|
||||
if pid and _is_process_running(pid):
|
||||
state.status = LocalStatus.RUNNING
|
||||
state.process_id = pid
|
||||
state.message = f"Bannerlord already running (PID: {pid})"
|
||||
else:
|
||||
state.status = LocalStatus.READY
|
||||
state.message = "Ready to launch"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def launch_bannerlord(state: Optional[LocalState] = None) -> LocalState:
|
||||
"""Launch Bannerlord via the emulator. Returns updated state."""
|
||||
if state is None:
|
||||
state = check_local_readiness()
|
||||
|
||||
if state.status not in (LocalStatus.READY, LocalStatus.RUNNING):
|
||||
return state
|
||||
|
||||
if state.status == LocalStatus.RUNNING:
|
||||
state.message = f"Already running (PID: {state.process_id})"
|
||||
return state
|
||||
|
||||
# Check if launcher script exists
|
||||
if LAUNCHER_SCRIPT.is_file():
|
||||
log.info(f"Using launcher script: {LAUNCHER_SCRIPT}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bash", str(LAUNCHER_SCRIPT), "--launch"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
cwd=str(LAUNCHER_SCRIPT.parent.parent),
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Parse PID from output
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if "PID:" in line:
|
||||
try:
|
||||
pid = int(line.split("PID:")[1].strip().rstrip(")"))
|
||||
state.process_id = pid
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
state.status = LocalStatus.RUNNING
|
||||
state.message = "Launched via launcher script"
|
||||
return state
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
||||
log.warning(f"Launcher script failed: {e}, falling back to direct launch")
|
||||
|
||||
# Direct launch fallback
|
||||
try:
|
||||
log.info(f"Launching Bannerlord directly via {state.emulator.name}")
|
||||
proc = subprocess.Popen(
|
||||
[state.emulator.path, state.game.game_exe],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=state.game.game_dir,
|
||||
)
|
||||
state.process_id = proc.pid
|
||||
state.status = LocalStatus.RUNNING
|
||||
state.message = f"Launched (PID: {proc.pid})"
|
||||
_write_pid(proc.pid)
|
||||
except Exception as e:
|
||||
state.status = LocalStatus.CRASHED
|
||||
state.message = f"Launch failed: {e}"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def stop_bannerlord() -> bool:
|
||||
"""Stop a running Bannerlord process."""
|
||||
pid = _read_pid()
|
||||
if not pid or not _is_process_running(pid):
|
||||
_clear_pid()
|
||||
return False
|
||||
|
||||
try:
|
||||
os.kill(pid, 15) # SIGTERM
|
||||
time.sleep(1)
|
||||
if _is_process_running(pid):
|
||||
os.kill(pid, 9) # SIGKILL
|
||||
_clear_pid()
|
||||
log.info(f"Stopped Bannerlord (PID: {pid})")
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
_clear_pid()
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# PID FILE MANAGEMENT
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PID_FILE = Path("/tmp/bannerlord.pid")
|
||||
|
||||
|
||||
def _read_pid() -> Optional[int]:
|
||||
try:
|
||||
if PID_FILE.is_file():
|
||||
return int(PID_FILE.read_text().strip())
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _write_pid(pid: int):
|
||||
try:
|
||||
PID_FILE.write_text(str(pid))
|
||||
except OSError as e:
|
||||
log.warning(f"Failed to write PID file: {e}")
|
||||
|
||||
|
||||
def _clear_pid():
|
||||
try:
|
||||
if PID_FILE.is_file():
|
||||
PID_FILE.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _is_process_running(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (ProcessLookupError, PermissionError):
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Bannerlord Local Manager — macOS")
|
||||
parser.add_argument("--check", action="store_true", help="Check readiness")
|
||||
parser.add_argument("--launch", action="store_true", help="Launch the game")
|
||||
parser.add_argument("--stop", action="store_true", help="Stop running game")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.stop:
|
||||
stopped = stop_bannerlord()
|
||||
if args.json:
|
||||
print(json.dumps({"stopped": stopped}))
|
||||
else:
|
||||
print("Stopped." if stopped else "Not running.")
|
||||
return
|
||||
|
||||
if args.launch:
|
||||
state = launch_bannerlord()
|
||||
else:
|
||||
state = check_local_readiness()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(state.to_dict(), indent=2))
|
||||
else:
|
||||
print(f"Status: {state.status.value}")
|
||||
print(f"Emulator: {state.emulator.name or 'none'} ({state.emulator.emulator_type.value})")
|
||||
print(f"Game: {state.game.game_dir or 'not found'}")
|
||||
if state.process_id:
|
||||
print(f"PID: {state.process_id}")
|
||||
print(f"Message: {state.message}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
portals.json
34
portals.json
@@ -4,7 +4,6 @@
|
||||
"name": "Morrowind",
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#ff6600",
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
@@ -18,19 +17,28 @@
|
||||
"id": "bannerlord",
|
||||
"name": "Bannerlord",
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"status": "active",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "local",
|
||||
"access_mode": "operator",
|
||||
"readiness_state": "active",
|
||||
"telemetry_source": "hermes-harness:bannerlord",
|
||||
"owner": "Timmy",
|
||||
"app_id": 261550,
|
||||
"window_title": "Mount & Blade II: Bannerlord",
|
||||
"local_launch": {
|
||||
"platform": "macos",
|
||||
"source": "gog",
|
||||
"emulator_required": true,
|
||||
"emulator_options": ["whisky", "crossover", "wine"],
|
||||
"launcher": "scripts/bannerlord_launcher.sh",
|
||||
"harness_bridge": "nexus/bannerlord_local.py",
|
||||
"check_command": "python3 -m nexus.bannerlord_local --check --json"
|
||||
},
|
||||
"destination": {
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
@@ -43,7 +51,6 @@
|
||||
"name": "Workshop",
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
@@ -58,7 +65,6 @@
|
||||
"name": "Archive",
|
||||
"description": "The repository of all knowledge. History, logs, and ancient data.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#0066ff",
|
||||
"position": { "x": 25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": -1.57 },
|
||||
@@ -73,7 +79,6 @@
|
||||
"name": "Chapel",
|
||||
"description": "A sanctuary for reflection and digital peace.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": 1.57 },
|
||||
@@ -88,7 +93,6 @@
|
||||
"name": "Courtyard",
|
||||
"description": "The open nexus. A place for agents to gather and connect.",
|
||||
"status": "online",
|
||||
"environment": "production",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": -2.5 },
|
||||
@@ -103,7 +107,6 @@
|
||||
"name": "Gate",
|
||||
"description": "The transition point. Entry and exit from the Nexus core.",
|
||||
"status": "standby",
|
||||
"environment": "staging",
|
||||
"color": "#ff4466",
|
||||
"position": { "x": -15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": 2.5 },
|
||||
@@ -112,20 +115,5 @@
|
||||
"type": "harness",
|
||||
"params": { "mode": "transit" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dev",
|
||||
"name": "Dev Sandbox",
|
||||
"description": "Local development world. Unstable, experimental, honest.",
|
||||
"status": "local-only",
|
||||
"environment": "local",
|
||||
"color": "#00ff88",
|
||||
"position": { "x": 0, "y": 0, "z": 20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "http://localhost:3000",
|
||||
"type": "local",
|
||||
"params": { "mode": "dev" }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
223
scripts/bannerlord_launcher.sh
Executable file
223
scripts/bannerlord_launcher.sh
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bannerlord Local Launcher for macOS
|
||||
# Detects Wine/Whisky/CrossOver, finds GOG Bannerlord install, launches it.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/bannerlord_launcher.sh [--check] [--launch] [--verbose]
|
||||
#
|
||||
# Modes:
|
||||
# --check Check environment only (no launch). Exits 0 if ready.
|
||||
# --launch Launch the game (default if no flags)
|
||||
# --verbose Print detailed diagnostic info
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BANNERLORD_EXE="bin/Generic/Bannerlord.exe"
|
||||
GOG_PATHS=(
|
||||
"/Applications/Games/Mount & Blade II Bannerlord"
|
||||
"$HOME/GOG Games/Mount and Blade II Bannerlord"
|
||||
"$HOME/Games/Mount & Blade II Bannerlord"
|
||||
"/Applications/Mount & Blade II Bannerlord"
|
||||
)
|
||||
# Also check common GOG Galaxy paths
|
||||
GOG_GALAXY_PATHS=(
|
||||
"$HOME/Library/Application Support/GOG.com/Galaxy/Applications/*/Mount & Blade II Bannerlord"
|
||||
)
|
||||
|
||||
# Emulator priority: Whisky > CrossOver > Homebrew Wine > system wine
|
||||
EMULATOR_NAMES=("Whisky" "CrossOver" "Wine" "wine64" "wine")
|
||||
|
||||
VERBOSE=0
|
||||
CHECK_ONLY=0
|
||||
LAUNCH=0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# ARGUMENT PARSING
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--check) CHECK_ONLY=1 ;;
|
||||
--launch) LAUNCH=1 ;;
|
||||
--verbose) VERBOSE=1 ;;
|
||||
*) echo "Unknown arg: $arg"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$CHECK_ONLY" -eq 0 ] && [ "$LAUNCH" -eq 0 ]; then
|
||||
LAUNCH=1 # Default to launch mode
|
||||
fi
|
||||
|
||||
log() { echo "[bannerlord] $*"; }
|
||||
vlog() { [ "$VERBOSE" -eq 1 ] && echo "[bannerlord:debug] $*" || true; }
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# EMULATOR DETECTION
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
find_emulator() {
|
||||
local emulator_path=""
|
||||
local emulator_name=""
|
||||
local emulator_type=""
|
||||
|
||||
# Check for Whisky (macOS Wine wrapper)
|
||||
if [ -d "/Applications/Whisky.app" ]; then
|
||||
emulator_path="/Applications/Whisky.app/Contents/Resources/Libraries/wine/bin/wine64"
|
||||
if [ -x "$emulator_path" ]; then
|
||||
emulator_name="Whisky"
|
||||
emulator_type="whisky"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for CrossOver
|
||||
if [ -z "$emulator_path" ] && [ -d "/Applications/CrossOver.app" ]; then
|
||||
emulator_path="/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine"
|
||||
if [ -x "$emulator_path" ]; then
|
||||
emulator_name="CrossOver"
|
||||
emulator_type="crossover"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for Homebrew wine
|
||||
if [ -z "$emulator_path" ]; then
|
||||
for candidate in wine64 wine; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
emulator_path="$(command -v "$candidate")"
|
||||
emulator_name="$candidate"
|
||||
emulator_type="wine"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$emulator_path" ]; then
|
||||
EMULATOR_PATH="$emulator_path"
|
||||
EMULATOR_NAME="$emulator_name"
|
||||
EMULATOR_TYPE="$emulator_type"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# GAME DETECTION
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
find_bannerlord() {
|
||||
# Check standard GOG paths
|
||||
for path in "${GOG_PATHS[@]}"; do
|
||||
if [ -f "$path/$BANNERLORD_EXE" ]; then
|
||||
GAME_DIR="$path"
|
||||
GAME_EXE="$path/$BANNERLORD_EXE"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Check GOG Galaxy paths (glob expansion)
|
||||
for pattern in "${GOG_GALAXY_PATHS[@]}"; do
|
||||
# shellcheck disable=SC2086
|
||||
for path in $pattern; do
|
||||
if [ -d "$path" ] && [ -f "$path/$BANNERLORD_EXE" ]; then
|
||||
GAME_DIR="$path"
|
||||
GAME_EXE="$path/$BANNERLORD_EXE"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Search with find as last resort
|
||||
local found
|
||||
found=$(find /Applications "$HOME/GOG Games" "$HOME/Games" -name "Bannerlord.exe" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$found" ]; then
|
||||
GAME_EXE="$found"
|
||||
GAME_DIR="$(dirname "$(dirname "$found")")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# STATUS REPORTING
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
emit_status() {
|
||||
local status="$1"
|
||||
local message="$2"
|
||||
# JSON output for harness consumption
|
||||
echo "{\"status\":\"$status\",\"emulator\":\"${EMULATOR_NAME:-none}\",\"emulator_type\":\"${EMULATOR_TYPE:-none}\",\"game_dir\":\"${GAME_DIR:-}\",\"game_exe\":\"${GAME_EXE:-}\",\"message\":\"$message\"}"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
main() {
|
||||
# Verify macOS
|
||||
if [ "$(uname)" != "Darwin" ]; then
|
||||
emit_status "error" "Not macOS — this launcher is Mac-only"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Bannerlord Local Launcher — macOS"
|
||||
|
||||
# Find emulator
|
||||
if find_emulator; then
|
||||
log "Emulator found: $EMULATOR_NAME ($EMULATOR_PATH)"
|
||||
vlog " Type: $EMULATOR_TYPE"
|
||||
else
|
||||
log "ERROR: No Windows emulator found."
|
||||
log "Install one of: Whisky, CrossOver, or wine (brew install --cask wine-stable)"
|
||||
emit_status "missing_emulator" "No Windows emulator installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find game
|
||||
if find_bannerlord; then
|
||||
log "Bannerlord found: $GAME_DIR"
|
||||
vlog " Exe: $GAME_EXE"
|
||||
else
|
||||
log "ERROR: Bannerlord not found in known GOG paths."
|
||||
log "Checked: ${GOG_PATHS[*]}"
|
||||
emit_status "missing_game" "Bannerlord GOG installation not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check mode
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
log "Check passed. Ready to launch."
|
||||
emit_status "ready" "Emulator and game both found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Launch
|
||||
if [ "$LAUNCH" -eq 1 ]; then
|
||||
log "Launching Bannerlord via $EMULATOR_NAME..."
|
||||
emit_status "launching" "Starting Bannerlord through $EMULATOR_NAME"
|
||||
|
||||
cd "$GAME_DIR"
|
||||
# Launch in background, redirect output
|
||||
"$EMULATOR_PATH" "$GAME_EXE" "$@" >/dev/null 2>&1 &
|
||||
local pid=$!
|
||||
log "Bannerlord started (PID: $pid)"
|
||||
echo "$pid" > /tmp/bannerlord.pid
|
||||
|
||||
# Wait a moment and check it's still running
|
||||
sleep 2
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
log "Bannerlord is running."
|
||||
emit_status "running" "Bannerlord PID $pid"
|
||||
exit 0
|
||||
else
|
||||
log "WARNING: Bannerlord process exited quickly. Check Wine logs."
|
||||
emit_status "crashed" "Process exited within 2 seconds"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
169
style.css
169
style.css
@@ -365,11 +365,7 @@ canvas#nexus-canvas {
|
||||
}
|
||||
|
||||
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
|
||||
.status-rebuilding { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
|
||||
.status-local-only { background: rgba(0, 255, 136, 0.2); color: #00ff88; border: 1px solid #00ff88; }
|
||||
.status-blocked { background: rgba(255, 0, 0, 0.2); color: #ff0000; border: 1px solid #ff0000; }
|
||||
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
|
||||
|
||||
.atlas-card-desc {
|
||||
@@ -414,165 +410,6 @@ canvas#nexus-canvas {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Portal Status Wall */
|
||||
.atlas-status-wall {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: rgba(10, 15, 40, 0.5);
|
||||
}
|
||||
|
||||
.status-wall-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-wall-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-wall-subtitle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.status-wall-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-wall-env {
|
||||
background: rgba(20, 30, 60, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-env-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.status-env-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-env-dot.production {
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 5px var(--color-primary);
|
||||
}
|
||||
|
||||
.status-env-dot.staging {
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 5px var(--color-gold);
|
||||
}
|
||||
|
||||
.status-env-dot.local {
|
||||
background: #00ff88;
|
||||
box-shadow: 0 0 5px #00ff88;
|
||||
}
|
||||
|
||||
.status-env-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-env-portals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status-portal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.status-portal-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-portal-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-env-summary {
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.status-wall-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.online { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
|
||||
.status-dot.active { background: var(--color-primary); box-shadow: 0 0 4px var(--color-primary); }
|
||||
.status-dot.standby { background: var(--color-gold); box-shadow: 0 0 4px var(--color-gold); }
|
||||
.status-dot.rebuilding { background: #ffa500; box-shadow: 0 0 4px #ffa500; }
|
||||
.status-dot.local-only { background: #00ff88; box-shadow: 0 0 4px #00ff88; }
|
||||
.status-dot.blocked { background: #ff0000; box-shadow: 0 0 4px #ff0000; }
|
||||
.status-dot.offline { background: var(--color-danger); box-shadow: 0 0 4px var(--color-danger); }
|
||||
|
||||
/* Additional status indicators for footer */
|
||||
.status-indicator.rebuilding { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
|
||||
.status-indicator.local-only { background: #00ff88; box-shadow: 0 0 5px #00ff88; }
|
||||
.status-indicator.blocked { background: #ff0000; box-shadow: 0 0 5px #ff0000; }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
@@ -586,12 +423,6 @@ canvas#nexus-canvas {
|
||||
.atlas-content {
|
||||
max-height: 90vh;
|
||||
}
|
||||
.status-wall-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.atlas-status-wall {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Debug overlay */
|
||||
|
||||
Reference in New Issue
Block a user