Skip to content
Open
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ FISH_API_KEY=your-fish-audio-api-key-here
# Optional: Specific Apple Calendar accounts to read (comma-separated emails)
# If not set, JARVIS reads ALL calendars from Apple Calendar
# CALENDAR_ACCOUNTS=you@gmail.com,work@company.com

# Optional: override auto-detected weather location
# If not set, JARVIS uses your current public-IP location and reports Celsius.
# WEATHER_LOCATION_LABEL=Edmonton, AB
# WEATHER_LATITUDE=53.5461
# WEATHER_LONGITUDE=-113.4938
33 changes: 30 additions & 3 deletions calendar_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,26 @@

log = logging.getLogger("jarvis.calendar")


def _parse_calendar_accounts(raw: str) -> list[str]:
"""Parse CALENDAR_ACCOUNTS, treating empty / auto as auto-discover."""
if not raw or not raw.strip():
return []
accounts = []
for item in raw.split(","):
value = item.strip()
if not value:
continue
if value.lower() == "auto":
continue
accounts.append(value)
return accounts


# Calendars to scan — set CALENDAR_ACCOUNTS env var to a comma-separated list,
# or leave empty to auto-discover ALL calendars from Apple Calendar.
_calendar_accounts_env = os.getenv("CALENDAR_ACCOUNTS", "")
USER_CALENDARS: list[str] = [
a.strip() for a in _calendar_accounts_env.split(",") if a.strip()
] if _calendar_accounts_env.strip() else []
USER_CALENDARS: list[str] = _parse_calendar_accounts(_calendar_accounts_env)

_auto_discovered = False

Expand Down Expand Up @@ -181,6 +195,19 @@ async def refresh_cache():
log.info(f"Calendar cache refreshed: {len(all_events)} events today ({elapsed:.1f}s)")


def configure_calendar_accounts(raw: str) -> None:
"""Apply updated calendar account settings without a server restart."""
global USER_CALENDARS, _auto_discovered, _event_cache, _cache_time
USER_CALENDARS = _parse_calendar_accounts(raw)
_auto_discovered = False
_event_cache = []
_cache_time = 0.0
if USER_CALENDARS:
log.info(f"Calendar accounts configured explicitly: {USER_CALENDARS}")
else:
log.info("Calendar accounts set to auto-discover")


async def get_todays_events() -> list[dict]:
"""Get today's events from cache. Returns cached data immediately."""
if not _event_cache and _cache_time == 0:
Expand Down
17 changes: 17 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<!-- Dropdown menu -->
<div id="menu-dropdown" style="display:none">
<button id="btn-debug-toggle">Debug Mode</button>
<button id="btn-settings">Settings</button>
<button id="btn-restart">Restart Server</button>
<button id="btn-fix-self">Fix Yourself</button>
Expand All @@ -35,6 +36,22 @@
<div id="status-text"></div>
<div id="jarvis-label">JARVIS</div>
<div id="error-text"></div>

<section id="debug-panel" aria-live="polite">
<div id="debug-header">
<div>
<div id="debug-title">Debug Mode</div>
<div id="debug-connection">Disconnected</div>
</div>
<div id="debug-actions">
<button id="btn-debug-clear">Clear</button>
<button id="btn-debug-close" aria-label="Close debug panel">&times;</button>
</div>
</div>
<div id="debug-log">
<div class="debug-log-empty">Live background log will appear here.</div>
</div>
</section>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
124 changes: 124 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@ import "./style.css";
// ---------------------------------------------------------------------------

type State = "idle" | "listening" | "thinking" | "speaking";
type DebugLogEntry = {
ts: string;
logger: string;
level: string;
message: string;
};

let currentState: State = "idle";
let isMuted = false;
let debugMode = window.localStorage.getItem("jarvis_debug_mode") === "1";

const statusEl = document.getElementById("status-text")!;
const errorEl = document.getElementById("error-text")!;
const debugPanelEl = document.getElementById("debug-panel")!;
const debugLogEl = document.getElementById("debug-log")!;
const debugConnectionEl = document.getElementById("debug-connection")!;

function showError(msg: string) {
errorEl.textContent = msg;
Expand Down Expand Up @@ -76,6 +87,78 @@ function transition(newState: State) {
}
}

function setDebugConnection(connected: boolean) {
debugConnectionEl.textContent = connected ? "Live stream connected" : "Waiting for connection...";
}

function ensureDebugPlaceholder() {
if (debugLogEl.childElementCount === 0) {
const empty = document.createElement("div");
empty.className = "debug-log-empty";
empty.textContent = "Live background log will appear here.";
debugLogEl.appendChild(empty);
}
}

function clearDebugLog() {
debugLogEl.innerHTML = "";
ensureDebugPlaceholder();
}

function appendDebugEntry(entry: DebugLogEntry) {
const nearBottom =
debugLogEl.scrollHeight - debugLogEl.scrollTop - debugLogEl.clientHeight < 40;

const empty = debugLogEl.querySelector(".debug-log-empty");
if (empty) empty.remove();

const row = document.createElement("div");
row.className = "debug-log-entry";

const timeEl = document.createElement("span");
timeEl.className = "debug-log-time";
timeEl.textContent = entry.ts;

const levelEl = document.createElement("span");
levelEl.className = `debug-log-level level-${entry.level.toLowerCase()}`;
levelEl.textContent = entry.level;

const loggerEl = document.createElement("span");
loggerEl.className = "debug-log-logger";
loggerEl.textContent = entry.logger;
loggerEl.title = entry.logger;

const messageEl = document.createElement("span");
messageEl.className = "debug-log-message";
messageEl.textContent = entry.message;

row.append(timeEl, levelEl, loggerEl, messageEl);
debugLogEl.appendChild(row);

while (debugLogEl.childElementCount > 250) {
debugLogEl.removeChild(debugLogEl.firstElementChild!);
}

if (nearBottom) {
debugLogEl.scrollTop = debugLogEl.scrollHeight;
}
}

function applyDebugMode() {
debugPanelEl.classList.toggle("open", debugMode);
btnDebugToggle.textContent = debugMode ? "Hide Debug Mode" : "Debug Mode";
window.localStorage.setItem("jarvis_debug_mode", debugMode ? "1" : "0");

if (!debugMode) {
socket.send({ type: "debug_logs", enabled: false });
return;
}

if (socket.isConnected()) {
socket.send({ type: "debug_logs", enabled: true });
}
}

// ---------------------------------------------------------------------------
// Voice input
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -141,9 +224,28 @@ socket.onMessage((msg) => {
console.log("[task]", "spawned:", msg.task_id, msg.prompt);
} else if (type === "task_complete") {
console.log("[task]", "complete:", msg.task_id, msg.status, msg.summary);
} else if (type === "debug_log_snapshot") {
clearDebugLog();
const entries = (msg.entries as DebugLogEntry[]) || [];
for (const entry of entries) appendDebugEntry(entry);
ensureDebugPlaceholder();
} else if (type === "debug_log") {
const entry = msg.entry as DebugLogEntry | undefined;
if (entry) appendDebugEntry(entry);
}
});

socket.onOpen(() => {
setDebugConnection(true);
if (debugMode) {
socket.send({ type: "debug_logs", enabled: true });
}
});

socket.onConnectionChange((connected) => {
setDebugConnection(connected);
});

// ---------------------------------------------------------------------------
// Kick off
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -175,6 +277,9 @@ ensureAudioContext();
const btnMute = document.getElementById("btn-mute")!;
const btnMenu = document.getElementById("btn-menu")!;
const menuDropdown = document.getElementById("menu-dropdown")!;
const btnDebugToggle = document.getElementById("btn-debug-toggle")!;
const btnDebugClear = document.getElementById("btn-debug-clear")!;
const btnDebugClose = document.getElementById("btn-debug-close")!;
const btnRestart = document.getElementById("btn-restart")!;
const btnFixSelf = document.getElementById("btn-fix-self")!;

Expand All @@ -196,6 +301,22 @@ btnMenu.addEventListener("click", (e) => {
menuDropdown.style.display = menuDropdown.style.display === "none" ? "block" : "none";
});

btnDebugToggle.addEventListener("click", (e) => {
e.stopPropagation();
menuDropdown.style.display = "none";
debugMode = !debugMode;
applyDebugMode();
});

btnDebugClear.addEventListener("click", () => {
clearDebugLog();
});

btnDebugClose.addEventListener("click", () => {
debugMode = false;
applyDebugMode();
});

document.addEventListener("click", () => {
menuDropdown.style.display = "none";
});
Expand Down Expand Up @@ -233,3 +354,6 @@ btnSettings.addEventListener("click", (e) => {
setTimeout(() => {
checkFirstTimeSetup();
}, 2000);

ensureDebugPlaceholder();
applyDebugMode();
Loading