feat: terminal fullscreen, asyncpg fix, CI improvements #59

Merged
jimmy merged 18 commits from dev into main 2026-05-06 01:30:57 +08:00
4 changed files with 60 additions and 11 deletions
Showing only changes of commit 019fddc079 - Show all commits
+24
View File
@@ -22,6 +22,10 @@ ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "")
TOTP_SECRET = os.environ.get("TOTP_SECRET", "") TOTP_SECRET = os.environ.get("TOTP_SECRET", "")
# SSH terminal # SSH terminal
# NOTE: The default values below are for development only.
# In production, override these via environment variables:
# SSH_HOST, SSH_USER, VPS_SSH_HOST, VPS_SSH_USER,
# CADDY_VPS_SSH_HOST, CADDY_VPS_SSH_USER, MAC_SSH_HOST, MAC_SSH_USER
SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal") SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal")
SSH_USER = os.environ.get("SSH_USER", "zjgump") SSH_USER = os.environ.get("SSH_USER", "zjgump")
SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/ssh/id_ed25519") SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/ssh/id_ed25519")
@@ -51,6 +55,26 @@ WEBAUTHN_ORIGINS = os.environ.get("WEBAUTHN_ORIGINS", "https://nas.jimmygan.com,
# CORS # CORS
CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://nas.jimmygan.com,https://nas.jimmygan.com:8443").split(",") CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://nas.jimmygan.com,https://nas.jimmygan.com:8443").split(",")
# Dashboard public URL (used in email templates, etc.)
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "https://nas.jimmygan.com")
# External service URLs (used by frontend sidebar)
EXTERNAL_SERVICES = {
"navidrome": os.environ.get("NAVIDROME_URL", "https://music.jimmygan.com:8443"),
"jellyfin": os.environ.get("JELLYFIN_URL", "https://media.jimmygan.com:8443"),
"audiobookshelf": os.environ.get("AUDIOBOOKSHELF_URL", "https://books.jimmygan.com:8443"),
"immich": os.environ.get("IMMICH_URL", "https://photos.jimmygan.com:8443"),
"gitea_web": os.environ.get("GITEA_WEB_URL", "https://git.jimmygan.com:8443"),
"stirling_pdf": os.environ.get("STIRLING_PDF_URL", "https://pdf.jimmygan.com:8443"),
"vaultwarden": os.environ.get("VAULTWARDEN_URL", "https://vault.jimmygan.com:8443"),
"n8n": os.environ.get("N8N_URL", "https://n8n.jimmygan.com:8443"),
"speedtest": os.environ.get("SPEEDTEST_URL", "https://speed.jimmygan.com:8443"),
}
# Network addresses (used by frontend for LAN/Tailscale direct access)
LAN_IP = os.environ.get("LAN_IP", "192.168.31.222")
TS_IP = os.environ.get("TS_IP", "100.78.131.124")
# Chat Summary # Chat Summary
CHAT_SUMMARY_DB = os.environ.get("CHAT_SUMMARY_DB", "/app/data/chat-summarizer/chat_summary.db") CHAT_SUMMARY_DB = os.environ.get("CHAT_SUMMARY_DB", "/app/data/chat-summarizer/chat_summary.db")
+10
View File
@@ -149,3 +149,13 @@ def openclaw_token(request: Request):
if not config.is_lan_ip(ip): if not config.is_lan_ip(ip):
raise HTTPException(status_code=403, detail="Access denied") raise HTTPException(status_code=403, detail="Access denied")
return {"token": OPENCLAW_GATEWAY_TOKEN} return {"token": OPENCLAW_GATEWAY_TOKEN}
@router.get("/external-services")
def external_services():
"""Return external service URLs and network config for the frontend sidebar."""
return {
"lan_ip": config.LAN_IP,
"ts_ip": config.TS_IP,
"services": config.EXTERNAL_SERVICES,
}
+8 -6
View File
@@ -8,6 +8,8 @@ import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
import config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Email configuration from environment # Email configuration from environment
@@ -64,7 +66,7 @@ Status: {task['status']}
Description: Description:
{task.get('description', 'No description')} {task.get('description', 'No description')}
View task: https://nas.jimmygan.com/?page=opc View task: {config.DASHBOARD_URL}/?page=opc
""" """
html_body = f""" html_body = f"""
@@ -79,7 +81,7 @@ View task: https://nas.jimmygan.com/?page=opc
</table> </table>
<p><strong>Description:</strong></p> <p><strong>Description:</strong></p>
<p>{task.get('description', 'No description')}</p> <p>{task.get('description', 'No description')}</p>
<p><a href="https://nas.jimmygan.com/?page=opc" style="background-color: #4F46E5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">View Task</a></p> <p><a href="{config.DASHBOARD_URL}/?page=opc" style="background-color: #4F46E5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">View Task</a></p>
</body> </body>
</html> </html>
""" """
@@ -107,7 +109,7 @@ Proposed Actions:
{actions_text} {actions_text}
Please review and approve in the OPC dashboard: Please review and approve in the OPC dashboard:
https://nas.jimmygan.com/?page=opc {config.DASHBOARD_URL}/?page=opc
""" """
html_body = f""" html_body = f"""
@@ -122,7 +124,7 @@ https://nas.jimmygan.com/?page=opc
<ul> <ul>
{"".join([f"<li><strong>{action.get('type')}</strong>: {action.get('title', action.get('message', 'N/A'))}</li>" for action in actions])} {"".join([f"<li><strong>{action.get('type')}</strong>: {action.get('title', action.get('message', 'N/A'))}</li>" for action in actions])}
</ul> </ul>
<p><a href="https://nas.jimmygan.com/?page=opc" style="background-color: #4F46E5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Review & Approve</a></p> <p><a href="{config.DASHBOARD_URL}/?page=opc" style="background-color: #4F46E5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Review & Approve</a></p>
</body> </body>
</html> </html>
""" """
@@ -140,7 +142,7 @@ Agent: {agent_name}
Task: {task['title']} Task: {task['title']}
Actions Executed: {actions_count} Actions Executed: {actions_count}
View details: https://nas.jimmygan.com/?page=opc View details: {config.DASHBOARD_URL}/?page=opc
""" """
html_body = f""" html_body = f"""
@@ -150,7 +152,7 @@ View details: https://nas.jimmygan.com/?page=opc
<p><strong>Agent:</strong> {agent_name}</p> <p><strong>Agent:</strong> {agent_name}</p>
<p><strong>Task:</strong> {task['title']}</p> <p><strong>Task:</strong> {task['title']}</p>
<p><strong>Actions Executed:</strong> {actions_count}</p> <p><strong>Actions Executed:</strong> {actions_count}</p>
<p><a href="https://nas.jimmygan.com/?page=opc" style="background-color: #10B981; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">View Details</a></p> <p><a href="{config.DASHBOARD_URL}/?page=opc" style="background-color: #10B981; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">View Details</a></p>
</body> </body>
</html> </html>
""" """
@@ -27,14 +27,22 @@
{ id: "security", label: "Security", icon: "shield" }, { id: "security", label: "Security", icon: "shield" },
]; ];
const LAN_IP = "192.168.31.222"; let lanIp = $state("192.168.31.222");
const TS_IP = "100.78.131.124"; let tsIp = $state("100.78.131.124");
let lanReachable = $state(false); let lanReachable = $state(false);
let serviceUrls = $state({});
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
fetch("/api/client-ip").then(r => r.json()).then(d => { fetch("/api/client-ip").then(r => r.json()).then(d => {
if (d.lan) lanReachable = true; if (d.lan) lanReachable = true;
}).catch(() => {}); }).catch(() => {});
// Fetch external service URLs from backend config
fetch("/api/system/external-services").then(r => r.json()).then(d => {
if (d.lan_ip) lanIp = d.lan_ip;
if (d.ts_ip) tsIp = d.ts_ip;
if (d.services) serviceUrls = d.services;
}).catch(() => {});
} }
const defaultMedia = [ const defaultMedia = [
@@ -197,10 +205,15 @@
} }
function extHref(svc) { function extHref(svc) {
if (lanReachable && svc.port) return `http://${LAN_IP}:${svc.port}`; // Look up service URL from fetched config by label (lowercased, underscored)
if (svc.remoteHref) return lanReachable ? svc.remoteHref : toPublicHttpsUrl(svc.remoteHref); const key = svc.label ? svc.label.toLowerCase().replace(/\s+/g, "_") : "";
const configUrl = serviceUrls[key];
const remoteHref = configUrl || svc.remoteHref;
if (lanReachable && svc.port) return `http://${lanIp}:${svc.port}`;
if (remoteHref) return lanReachable ? remoteHref : toPublicHttpsUrl(remoteHref);
if (svc.port) { if (svc.port) {
const host = /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) ? location.hostname : TS_IP; const host = /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) ? location.hostname : tsIp;
return `http://${host}:${svc.port}`; return `http://${host}:${svc.port}`;
} }
return svc.href; return svc.href;