Skip to content

Commit ec908af

Browse files
committed
Dashboard mode added. Journal & notes widget polish first pass.
1 parent b027e9c commit ec908af

8 files changed

Lines changed: 413 additions & 87 deletions

File tree

commands/dashboard.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from datetime import datetime
99
from urllib import request as urlrequest, error as urlerror
1010
from modules.scheduler import status_current_path
11+
from utilities.webview_launcher import launch_webview_window
1112

1213
try:
1314
import yaml
@@ -140,10 +141,10 @@ def _ensure_dashboard_server(host, port, env, server_script, *, visible_console=
140141
time.sleep(0.25)
141142
return False
142143

143-
def _read_dashboard_browser_setting():
144-
"""Optional browser override from user/settings config."""
144+
def _read_dashboard_config():
145+
"""Optional dashboard launch settings from user/settings config."""
145146
if yaml is None:
146-
return ""
147+
return {}
147148
settings_dir = os.path.join(ROOT_DIR, "user", "settings")
148149
for fname in ("config.yml", "Config.yml"):
149150
path = os.path.join(settings_dir, fname)
@@ -153,13 +154,25 @@ def _read_dashboard_browser_setting():
153154
with open(path, "r", encoding="utf-8") as f:
154155
cfg = yaml.safe_load(f) or {}
155156
if isinstance(cfg, dict):
156-
val = cfg.get("dashboard_browser")
157-
if not val:
158-
val = cfg.get("browser")
159-
return str(val or "").strip()
157+
return cfg
160158
except Exception:
161159
continue
162-
return ""
160+
return {}
161+
162+
163+
def _read_dashboard_browser_setting():
164+
cfg = _read_dashboard_config()
165+
val = cfg.get("dashboard_browser") if isinstance(cfg, dict) else ""
166+
if not val and isinstance(cfg, dict):
167+
val = cfg.get("browser")
168+
return str(val or "").strip()
169+
170+
171+
def _read_dashboard_mode_setting():
172+
cfg = _read_dashboard_config()
173+
if not isinstance(cfg, dict):
174+
return "browser"
175+
return str(cfg.get("dashboard_mode") or "browser").strip().lower() or "browser"
163176

164177
def _normalize_browser_command(raw):
165178
token = str(raw or "").strip()
@@ -196,6 +209,10 @@ def _open_dashboard_url(url, browser_cmd=""):
196209
return "default"
197210

198211

212+
def _open_dashboard_webview(url, title="Chronos Dashboard"):
213+
return launch_webview_window(url, title)
214+
215+
199216
def run(args, properties):
200217
"""
201218
Bundles settings into a generated manifest and opens the dashboard HTML.
@@ -248,15 +265,29 @@ def run(args, properties):
248265

249266
url = f"http://{host}:{port}/dashboard.html"
250267
browser_from_props = ""
268+
mode_from_props = ""
251269
if isinstance(properties, dict):
252270
browser_from_props = str(properties.get("browser") or "").strip()
271+
mode_from_props = str(properties.get("dashboard_mode") or properties.get("mode") or "").strip().lower()
253272
browser_setting = browser_from_props or _read_dashboard_browser_setting()
273+
dashboard_mode = mode_from_props or _read_dashboard_mode_setting()
254274
try:
255-
opened_with = _open_dashboard_url(url, browser_setting)
256-
if opened_with == "default":
257-
print(f"Opened dashboard: {url}")
275+
if dashboard_mode == "webview":
276+
if _open_dashboard_webview(url, "Chronos Dashboard"):
277+
print(f"Opened dashboard in webview: {url}")
278+
else:
279+
print("Warning: pywebview launcher unavailable; falling back to browser.")
280+
opened_with = _open_dashboard_url(url, browser_setting)
281+
if opened_with == "default":
282+
print(f"Opened dashboard: {url}")
283+
else:
284+
print(f"Opened dashboard in '{opened_with}': {url}")
258285
else:
259-
print(f"Opened dashboard in '{opened_with}': {url}")
286+
opened_with = _open_dashboard_url(url, browser_setting)
287+
if opened_with == "default":
288+
print(f"Opened dashboard: {url}")
289+
else:
290+
print(f"Opened dashboard in '{opened_with}': {url}")
260291
except Exception as e:
261292
print(f"Could not open dashboard: {e}\nOpen manually: {url}")
262293

@@ -265,6 +296,7 @@ def get_help_message():
265296
return """
266297
Usage: dashboard
267298
Description: Opens the Chronos dashboard UI in your default browser.
299+
Optional: set `dashboard_mode` to `browser` or `webview` in user/settings/config.yml.
268300
Optional: set `browser` (or `dashboard_browser`) in user/settings/config.yml, or pass `browser:<cmd>`.
269301
Also pre-bundles user/settings YAML into generated/settings_bundle.js for the UI to consume.
270302
"""

user/settings/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
default_editor: chronos_editor
2+
dashboard_mode: browser

utilities/dashboard/app.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3335,6 +3335,11 @@ ready(async () => {
33353335
window.addEventListener('resize', () => {
33363336
try { applyPaneSizes(); } catch { }
33373337
try { positionOpenDropdowns(); } catch { }
3338+
try {
3339+
document.querySelectorAll('.widget[data-maximized="true"]').forEach((el) => {
3340+
window.ChronosMaximizeWidget?.(el);
3341+
});
3342+
} catch { }
33383343
});
33393344

33403345
export { };

utilities/dashboard/core/runtime.js

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ function saveWidgetState(el) {
303303
height: el.style?.height || null,
304304
display: el.style?.display || '',
305305
minimized: el.classList?.contains('minimized') || false,
306+
maximized: el.dataset?.maximized === 'true',
306307
};
307308
_writeStateMap(map);
308309
}
@@ -330,6 +331,12 @@ function restoreWidgetState(el) {
330331
if (st.height) el.style.height = st.height;
331332
if (st.display !== undefined) el.style.display = st.display;
332333
if (st.minimized) el.classList.add('minimized'); else el.classList.remove('minimized');
334+
if (st.maximized) {
335+
el.dataset.maximized = 'true';
336+
applyMaximizedWidgetBounds(el);
337+
} else {
338+
delete el.dataset.maximized;
339+
}
333340
return true;
334341
}
335342
return false;
@@ -343,6 +350,93 @@ function centerWidget(el) {
343350
el.style.top = `${top}px`;
344351
} catch { }
345352
}
353+
354+
function captureWidgetRestoreState(el) {
355+
if (!el) return;
356+
try {
357+
const root = getScaleRoot();
358+
const rootRect = root ? root.getBoundingClientRect() : { left: 0, top: 0 };
359+
const scale = getScaleFactor(root) || 1;
360+
const rect = el.getBoundingClientRect();
361+
const computedLeft = `${Math.round((rect.left - rootRect.left) / scale)}px`;
362+
const computedTop = `${Math.round((rect.top - rootRect.top) / scale)}px`;
363+
const computedWidth = `${Math.round(rect.width / scale)}px`;
364+
const computedHeight = `${Math.round(rect.height / scale)}px`;
365+
el.dataset.restoreLeft = el.style.left || computedLeft;
366+
el.dataset.restoreTop = el.style.top || computedTop;
367+
el.dataset.restoreWidth = el.style.width || computedWidth;
368+
el.dataset.restoreHeight = el.style.height || computedHeight;
369+
el.dataset.restoreRight = el.style.right || '';
370+
el.dataset.restoreBottom = el.style.bottom || '';
371+
} catch { }
372+
}
373+
374+
function applyMaximizedWidgetBounds(el) {
375+
if (!el) return;
376+
try {
377+
const root = getScaleRoot();
378+
const scale = getScaleFactor(root) || 1;
379+
const viewportWidth = Math.max(320, Math.round((window.innerWidth - 24) / scale));
380+
const viewportHeight = Math.max(220, Math.round((window.innerHeight - 58) / scale));
381+
el.style.left = '12px';
382+
el.style.top = '46px';
383+
el.style.right = 'auto';
384+
el.style.bottom = 'auto';
385+
el.style.width = `${viewportWidth}px`;
386+
el.style.height = `${viewportHeight}px`;
387+
} catch { }
388+
}
389+
390+
function restoreWidgetBounds(el) {
391+
if (!el) return;
392+
try {
393+
el.style.left = el.dataset.restoreLeft || '';
394+
el.style.top = el.dataset.restoreTop || '';
395+
el.style.width = el.dataset.restoreWidth || '';
396+
el.style.height = el.dataset.restoreHeight || '';
397+
el.style.right = el.dataset.restoreRight || '';
398+
el.style.bottom = el.dataset.restoreBottom || '';
399+
} catch { }
400+
}
401+
402+
function maximizeWidget(el) {
403+
if (!el || isAnchoredWidget(el)) return false;
404+
try {
405+
if (el.dataset.maximized === 'true') {
406+
applyMaximizedWidgetBounds(el);
407+
saveWidgetState(el);
408+
return true;
409+
}
410+
captureWidgetRestoreState(el);
411+
el.dataset.maximized = 'true';
412+
el.classList.remove('minimized');
413+
applyMaximizedWidgetBounds(el);
414+
focusWidget(el);
415+
saveWidgetState(el);
416+
return true;
417+
} catch {
418+
return false;
419+
}
420+
}
421+
422+
function restoreWidget(el) {
423+
if (!el || isAnchoredWidget(el)) return false;
424+
try {
425+
delete el.dataset.maximized;
426+
restoreWidgetBounds(el);
427+
ensureInViewport(el);
428+
focusWidget(el);
429+
saveWidgetState(el);
430+
return true;
431+
} catch {
432+
return false;
433+
}
434+
}
435+
436+
function toggleWidgetMaximized(el, force) {
437+
const next = typeof force === 'boolean' ? force : el?.dataset?.maximized !== 'true';
438+
return next ? maximizeWidget(el) : restoreWidget(el);
439+
}
346440
function persistOnChanges(el) {
347441
try {
348442
const mo = new MutationObserver(() => saveWidgetState(el));
@@ -557,12 +651,22 @@ function installWidgetResizers(el) {
557651
// dataset-based constraints during mount, after the empty shell exists.
558652
try {
559653
const header = el.querySelector('.header');
560-
const headerW = header ? Math.ceil(header.scrollWidth) : 0;
654+
const title = header ? header.querySelector('.title') : null;
655+
const controls = header ? header.querySelector('.controls') : null;
656+
const headerCs = header ? getComputedStyle(header) : null;
657+
const padL = headerCs ? (parseFloat(headerCs.paddingLeft || '0') || 0) : 0;
658+
const padR = headerCs ? (parseFloat(headerCs.paddingRight || '0') || 0) : 0;
659+
const gap = headerCs ? (parseFloat(headerCs.columnGap || headerCs.gap || '0') || 0) : 0;
660+
const titleW = title ? Math.ceil(title.scrollWidth || title.getBoundingClientRect().width || 0) : 0;
661+
const controlsW = controls ? Math.ceil(controls.scrollWidth || controls.getBoundingClientRect().width || 0) : 0;
662+
const headerW = header ? Math.ceil(titleW + controlsW + padL + padR + gap + 12) : 0;
561663
const headerH = header ? Math.ceil(header.getBoundingClientRect().height) : 0;
562664
const dataMinW = Number(el.dataset?.minWidth || 0);
563665
const dataMinH = Number(el.dataset?.minHeight || 0);
564-
const baseMinW = Math.max(160, headerW + 12, dataMinW || 0);
565-
const baseMinH = Math.max(80, headerH + 20, dataMinH || 0);
666+
const inlineMinW = parseFloat(el.style.minWidth || '0') || 0;
667+
const inlineMinH = parseFloat(el.style.minHeight || '0') || 0;
668+
const baseMinW = Math.max(160, headerW, dataMinW || 0, inlineMinW || 0);
669+
const baseMinH = Math.max(80, headerH + 20, dataMinH || 0, inlineMinH || 0);
566670
el.__minW = baseMinW;
567671
el.__minH = baseMinH;
568672
el.__minSizeSet = true;
@@ -831,6 +935,9 @@ try {
831935
window.ensureWidgetInView = focusWidget;
832936
window.ChronosLaunchWizard = launchWizard;
833937
window.ChronosFocusWidget = focusWidget;
938+
window.ChronosMaximizeWidget = maximizeWidget;
939+
window.ChronosRestoreWidget = restoreWidget;
940+
window.ChronosToggleWidgetMaximized = toggleWidgetMaximized;
834941
} catch { }
835942

836943
// ---- Generic widget header dragging ----

0 commit comments

Comments
 (0)