Skip to content
Merged
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
22 changes: 11 additions & 11 deletions dashboard
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# dashboard — Launch goosetown dashboard. Idempotent. Port on stdout, noise on stderr.
#
# Multi-instance safe: each goose instance (identified by GOOSE_SERVER__SECRET_KEY)
# Multi-instance safe: each goose instance (identified by GOOSE_GTWALL_FILE)
# gets its own dashboard. Instances never interfere with each other — no shared screen
# names, no global pkill, no "stale session" replacement.
set -euo pipefail
Expand All @@ -19,11 +19,13 @@ info() { echo "$*" >&2; }
command -v uv &>/dev/null || die "uv not found. Install: curl -LsSf https://astral.sh/uv/install.sh | sh"

# ── Instance identity ───────────────────────────────────────────────────────
# GOOSE_SERVER__SECRET_KEY is a UUID unique per goose instance. The first 8 chars
# (WALL_ID) already scope wall files and position dirs — we reuse it to scope
# screen sessions and portfiles so multiple dashboards can coexist.
[[ -n "${GOOSE_SERVER__SECRET_KEY:-}" ]] || die2 "GOOSE_SERVER__SECRET_KEY not set"
WALL_ID="${GOOSE_SERVER__SECRET_KEY:0:8}"
# GOOSE_GTWALL_FILE points to the wall file for this goose instance. Extract the
# WALL_ID from the filename to scope screen sessions and portfiles so multiple
# dashboards can coexist.
[[ -n "${GOOSE_GTWALL_FILE:-}" ]] || die2 "GOOSE_GTWALL_FILE not set"
# Sanitize to safe chars for screen session names and portfiles (must match gtwall's rules)
WALL_ID=$(basename "$GOOSE_GTWALL_FILE" .log | sed 's/^wall-//' | tr -cd 'A-Za-z0-9_-')
[[ -n "$WALL_ID" ]] || die2 "Could not derive WALL_ID from GOOSE_GTWALL_FILE"
SCREEN_NAME="goosetown-ui-${WALL_ID}"
PORTFILE="${WALLS_DIR}/dashboard-${WALL_ID}.port"

Expand Down Expand Up @@ -58,10 +60,8 @@ resolve_session() {

# ── Wall resolution ─────────────────────────────────────────────────────────
resolve_wall() {
local wall_file="${WALLS_DIR}/wall-${WALL_ID}.log"
mkdir -p "$WALLS_DIR"
touch "$wall_file"
echo "$wall_file"
# Use GOOSE_GTWALL_FILE directly - it's already set by the goose wrapper
echo "$GOOSE_GTWALL_FILE"
}

# ── Port helpers ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -197,7 +197,7 @@ Usage: ./dashboard [--stop|--status|--open|--help]
--help This message

Multiple goose instances get separate dashboards (ports 4242-4300).
Each instance is scoped by GOOSE_SERVER__SECRET_KEY.
Each instance is scoped by GOOSE_GTWALL_FILE.

Exit codes: 0=ok, 1=error/not-running, 2=config-error, 3=port-exhaustion, 4=startup-timeout
EOF
Expand Down
12 changes: 11 additions & 1 deletion goose
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@ touch "$TELEPATHY_FILE"
# Export for tom extension - orchestrator and all delegates will see this
export GOOSE_MOIM_MESSAGE_FILE="$TELEPATHY_FILE"

# Generate unique wall file for this session
WALLS_DIR="${HOME}/.goosetown/walls"
mkdir -p "$WALLS_DIR"
WALL_FILE="${WALLS_DIR}/wall-$$-${RANDOM}.log"
touch "$WALL_FILE"
export GOOSE_GTWALL_FILE="$WALL_FILE"

# Cleanup on exit
cleanup() {
rm -f "$TELEPATHY_FILE"
rm -f "$WALL_FILE"
rm -rf "${WALL_FILE%.log}.positions"
}
trap cleanup EXIT

echo "🦆 Goosetown telepathy enabled: $TELEPATHY_FILE" >&2
echo "🦆 Goosetown wall enabled: $WALL_FILE" >&2

exec "$GOOSE_BIN" "$@"
"$GOOSE_BIN" "$@"
12 changes: 11 additions & 1 deletion goose_gui
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,24 @@ touch "$TELEPATHY_FILE"
# Export for tom extension
export GOOSE_MOIM_MESSAGE_FILE="$TELEPATHY_FILE"

# Generate unique wall file for this session
WALLS_DIR="${HOME}/.goosetown/walls"
mkdir -p "$WALLS_DIR"
WALL_FILE="${WALLS_DIR}/wall-$$-${RANDOM}.log"
touch "$WALL_FILE"
export GOOSE_GTWALL_FILE="$WALL_FILE"

# Cleanup on exit
cleanup() {
rm -f "$TELEPATHY_FILE"
echo "Telepathy file cleaned up." >&2
rm -f "$WALL_FILE"
rm -rf "${WALL_FILE%.log}.positions"
echo "Telepathy and wall files cleaned up." >&2
}
trap cleanup EXIT

echo "🦆 Goosetown GUI telepathy enabled: $TELEPATHY_FILE" >&2
echo "🦆 Goosetown GUI wall enabled: $WALL_FILE" >&2
echo "Keep this terminal open. Press Ctrl+C when done to clean up." >&2

# Launch GUI with -n to force new instance (ensures env var is picked up)
Expand Down
29 changes: 17 additions & 12 deletions gtwall
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,26 @@
WALL_DIR="${GTWALL_DIR:-${HOME}/.goosetown}"
WALLS_DIR="${WALL_DIR}/walls"

# Use GOOSE_SERVER__SECRET_KEY as session ID - it's unique per goose session
# and shared by orchestrator + all delegates automatically
if [[ -n "${GOOSE_SERVER__SECRET_KEY:-}" ]]; then
# Use first 8 chars of the UUID for readability
SESSION_ID="${GOOSE_SERVER__SECRET_KEY:0:8}"
# Use GOOSE_GTWALL_FILE if set, otherwise fallback to default wall
if [[ -n "${GOOSE_GTWALL_FILE:-}" ]]; then
WALL_FILE="$GOOSE_GTWALL_FILE"
else
# Fallback for non-goose usage
SESSION_ID="default"
# Fallback for non-goose usage (manual invocation)
mkdir -p "$WALLS_DIR"
WALL_FILE="${WALLS_DIR}/wall-default.log"
fi

# Derive positions directory from wall file path
POS_DIR="${WALL_FILE%.log}.positions"

# Extract display-friendly session ID from wall filename for help/status text
SESSION_ID=$(basename "$WALL_FILE" .log | sed 's/^wall-//')
# Sanitize SESSION_ID to safe filesystem characters (alphanumeric, dash, underscore)
SESSION_ID=$(printf '%s' "$SESSION_ID" | tr -cd 'A-Za-z0-9_-')
if [[ -z "$SESSION_ID" ]]; then
SESSION_ID="default"
fi

WALL_FILE="${WALLS_DIR}/wall-${SESSION_ID}.log"
POS_DIR="${WALLS_DIR}/positions-${SESSION_ID}"

# Lock management - track if we hold the lock for cleanup
LOCKFILE_HELD=""

Expand Down Expand Up @@ -247,7 +248,11 @@ ID Rules:

Session:
EOF
printf ' Session ID: %s (from GOOSE_SERVER__SECRET_KEY)\n' "$SESSION_ID"
if [[ -n "${GOOSE_GTWALL_FILE:-}" ]]; then
printf ' Session ID: %s (from GOOSE_GTWALL_FILE)\n' "$SESSION_ID"
else
printf ' Session ID: %s (default - no GOOSE_GTWALL_FILE set)\n' "$SESSION_ID"
fi
printf ' Wall: %s\n' "$WALL_FILE"
printf ' Positions: %s/\n' "$POS_DIR"
}
Expand Down Expand Up @@ -353,7 +358,7 @@ USAGE
shopt -s nullglob
for f in "${WALLS_DIR}"/wall-*.log; do
if [[ -f "$f" ]]; then
session=$(basename "$f" .log | sed 's/wall-//')
session=$(basename "$f" .log | sed 's/^wall-//')
lines=$(wc -l < "$f" | tr -d ' ')
if [[ "$session" == "$SESSION_ID" ]]; then
printf ' %s: %s messages (current)\n' "$session" "$lines"
Expand Down
13 changes: 9 additions & 4 deletions scripts/goosetown-ui
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,18 @@ def find_parent_session(cur) -> str | None:


def find_wall_file(session_key: str | None = None) -> str | None:
if session_key is None:
secret = os.environ.get("GOOSE_SERVER__SECRET_KEY", "")
session_key = secret[:8] if secret else None
# Check GOOSE_GTWALL_FILE env var first
gtwall_file = os.environ.get("GOOSE_GTWALL_FILE", "")
if gtwall_file and os.path.isfile(gtwall_file):
return gtwall_file

# Fallback: use session_key if provided
if session_key:
path = os.path.join(WALLS_DIR, f"wall-{session_key}.log")
if os.path.isfile(path):
return path

# Last resort: newest wall file
wall_files = glob.glob(os.path.join(WALLS_DIR, "wall-*.log"))
return max(wall_files, key=os.path.getmtime) if wall_files else None

Expand Down Expand Up @@ -509,7 +514,7 @@ def main():
wall_file = args.wall or find_wall_file()
wall_id = ""
if wall_file:
wall_id = Path(wall_file).stem.replace("wall-", "")
wall_id = Path(wall_file).stem.removeprefix("wall-")
print(f"📜 Wall file: {wall_file}", file=sys.stderr)
else:
print("⚠️ No wall file found — will poll for it", file=sys.stderr)
Expand Down
9 changes: 8 additions & 1 deletion tests/test_dashboard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DASHBOARD="$SCRIPT_DIR/../dashboard"

export GOOSE_SERVER__SECRET_KEY="dash${$}${RANDOM}"
# Create unique wall file for this test session
WALLS_DIR="$HOME/.goosetown/walls"
mkdir -p "$WALLS_DIR"
WALL_FILE="${WALLS_DIR}/wall-test-dash-${$}-${RANDOM}.log"
touch "$WALL_FILE"
export GOOSE_GTWALL_FILE="$WALL_FILE"

passed=0
failed=0
Expand All @@ -18,6 +23,8 @@ skip() { echo " ⊘ $1 (skipped: $2)"; ((++skipped)) || true; }

cleanup() {
"$DASHBOARD" --stop >/dev/null 2>&1 || true
rm -f "$WALL_FILE" 2>/dev/null || true
rm -rf "${WALL_FILE%.log}.positions" 2>/dev/null || true
}
trap cleanup EXIT

Expand Down
41 changes: 20 additions & 21 deletions tests/test_gtwall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GTWALL="$SCRIPT_DIR/../gtwall"

# Use unique session key - PID + RANDOM for uniqueness
export GOOSE_SERVER__SECRET_KEY="gt${$}${RANDOM}"
# Create unique wall file for this test session
WALLS_DIR="$HOME/.goosetown/walls"
mkdir -p "$WALLS_DIR"
WALL_FILE="${WALLS_DIR}/wall-test-${$}-${RANDOM}.log"
touch "$WALL_FILE"
export GOOSE_GTWALL_FILE="$WALL_FILE"

passed=0
failed=0
Expand All @@ -16,9 +20,8 @@ pass() { echo " ✓ $1"; ((++passed)) || true; }
fail() { echo " ✗ $1: $2" >&2; ((++failed)) || true; }

cleanup() {
local wall_id="${GOOSE_SERVER__SECRET_KEY:0:8}"
rm -rf "$HOME/.goosetown/walls/wall-${wall_id}.log"* 2>/dev/null || true
rm -rf "$HOME/.goosetown/walls/positions-${wall_id}" 2>/dev/null || true
rm -f "$WALL_FILE" 2>/dev/null || true
rm -rf "${WALL_FILE%.log}.positions" 2>/dev/null || true
}
trap cleanup EXIT

Expand Down Expand Up @@ -84,16 +87,17 @@ test_clear() {

test_session_isolation() {
echo "test_session_isolation"
# Keys must differ in first 8 chars
local key_a="isola${$}A"
local key_b="isolb${$}B"
# Create separate wall files for isolation test
local wall_a="${WALLS_DIR}/wall-test-isola-${$}.log"
local wall_b="${WALLS_DIR}/wall-test-isolb-${$}.log"
touch "$wall_a" "$wall_b"

GOOSE_SERVER__SECRET_KEY="$key_a" "$GTWALL" writer "secret-A" >/dev/null
GOOSE_SERVER__SECRET_KEY="$key_b" "$GTWALL" writer "secret-B" >/dev/null
GOOSE_GTWALL_FILE="$wall_a" "$GTWALL" writer "secret-A" >/dev/null
GOOSE_GTWALL_FILE="$wall_b" "$GTWALL" writer "secret-B" >/dev/null

local outputA outputB
outputA=$(GOOSE_SERVER__SECRET_KEY="$key_a" "$GTWALL" reader 2>/dev/null || echo "")
outputB=$(GOOSE_SERVER__SECRET_KEY="$key_b" "$GTWALL" reader 2>/dev/null || echo "")
outputA=$(GOOSE_GTWALL_FILE="$wall_a" "$GTWALL" reader 2>/dev/null || echo "")
outputB=$(GOOSE_GTWALL_FILE="$wall_b" "$GTWALL" reader 2>/dev/null || echo "")

# Each should only see its own
if [[ "$outputA" != *"secret-B"* && "$outputB" != *"secret-A"* ]]; then
Expand All @@ -103,22 +107,17 @@ test_session_isolation() {
fi

# Cleanup isolation test walls
rm -rf "$HOME/.goosetown/walls/wall-${key_a:0:8}.log"* 2>/dev/null || true
rm -rf "$HOME/.goosetown/walls/wall-${key_b:0:8}.log"* 2>/dev/null || true
rm -rf "$HOME/.goosetown/walls/positions-${key_a:0:8}" 2>/dev/null || true
rm -rf "$HOME/.goosetown/walls/positions-${key_b:0:8}" 2>/dev/null || true
rm -f "$wall_a" "$wall_b" 2>/dev/null || true
rm -rf "${wall_a%.log}.positions" "${wall_b%.log}.positions" 2>/dev/null || true
}

test_stale_lock_cleanup() {
echo "test_stale_lock_cleanup"
"$GTWALL" --clear >/dev/null 2>&1 || true

# Get correct lock path: ${WALLS_DIR}/wall-${SESSION_ID}.log.lock
local wall_id="${GOOSE_SERVER__SECRET_KEY:0:8}"
local walls_dir="$HOME/.goosetown/walls"
local lock_dir="${walls_dir}/wall-${wall_id}.log.lock"
# Get correct lock path from GOOSE_GTWALL_FILE
local lock_dir="${WALL_FILE}.lock"

mkdir -p "$walls_dir"
mkdir -p "$lock_dir"

# Create stale lock:
Expand Down
2 changes: 1 addition & 1 deletion ui/js/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function renderBulletin(st) {
? html`<div class="bulletin-empty">
${
messages.length === 0
? 'Waiting for wall… (start a goose session or check GOOSE_SERVER__SECRET_KEY)'
? 'Waiting for wall… (start a goose session or check GOOSE_GTWALL_FILE)'
: 'No messages match current filters.'
}
</div>`
Expand Down