summaryrefslogtreecommitdiff
path: root/web/static/js
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-05 10:43:19 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-05 10:43:19 -1000
commit1eab4d59454fa5999675d51b99e77ac6580aba95 (patch)
tree6b653e39d33fd879f29f769cdf3bd3f6bfcd3f05 /web/static/js
parent5ddb419137b814481a208d1dd0d18ac36ed554ea (diff)
Improve session handling, shopping UI, and cleanup
Session improvements: - Extend session lifetime to 7 days for mobile convenience - Add idle timeout to extend session on activity - Use standard cookie name for better compatibility Shopping model: - Add FlattenItemsForStore helper for extracting store items - Add StoreNames helper for store list - Improve shopping-tab.html with inline add forms Frontend: - Add WebSocket reconnection and agent approval UI to app.js - Simplify timeline calendar JS (move event positioning to CSS) - Update login page styling Deployment: - Remove unused git checkout step from deploy.sh - Update apache.conf WebSocket proxy settings Documentation: - Add Agent Context API feature spec to issues/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'web/static/js')
-rw-r--r--web/static/js/app.js108
1 files changed, 108 insertions, 0 deletions
diff --git a/web/static/js/app.js b/web/static/js/app.js
index 3ecd0a1..5dffacc 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -3,6 +3,108 @@
// Constants
const AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes in milliseconds
+// Timeline Calendar Initialization
+function initTimelineCalendar(calendarId, nowLineId, untimedSectionId) {
+ const calendar = document.getElementById(calendarId);
+ if (!calendar) return;
+
+ const events = calendar.querySelectorAll('.calendar-event');
+ const hourHeight = 40;
+ const startHour = parseInt(calendar.dataset.startHour) || 8;
+ const nowHour = parseInt(calendar.dataset.nowHour);
+ const nowMinute = parseInt(calendar.dataset.nowMinute);
+
+ // Build event data for overlap detection
+ const eventData = [];
+ events.forEach(function(el) {
+ const hour = parseInt(el.dataset.hour);
+ const minute = parseInt(el.dataset.minute);
+ const endHourVal = parseInt(el.dataset.endHour);
+ const endMinute = parseInt(el.dataset.endMinute);
+ const startMin = hour * 60 + minute;
+ let endMin;
+ if (endHourVal > hour || (endHourVal === hour && endMinute > minute)) {
+ endMin = endHourVal * 60 + endMinute;
+ } else {
+ endMin = startMin + 55;
+ }
+ eventData.push({ el, startMin, endMin, column: 0 });
+ });
+
+ // Assign columns for overlapping events
+ eventData.sort((a, b) => a.startMin - b.startMin);
+ for (let i = 0; i < eventData.length; i++) {
+ const ev = eventData[i];
+ const overlaps = eventData.filter((other, j) =>
+ j < i && other.endMin > ev.startMin && other.startMin < ev.endMin
+ );
+ const usedCols = overlaps.map(o => o.column);
+ let col = 0;
+ while (usedCols.includes(col)) col++;
+ ev.column = col;
+ }
+
+ // Position events with column-based indentation
+ eventData.forEach(function(ev) {
+ const el = ev.el;
+ const hour = parseInt(el.dataset.hour);
+ const minute = parseInt(el.dataset.minute);
+ const top = (hour - startHour) * hourHeight + (minute / 60) * hourHeight;
+ const durationMinutes = ev.endMin - ev.startMin;
+ const height = Math.max(28, (durationMinutes / 60) * hourHeight - 4);
+
+ el.style.top = top + 'px';
+ el.style.height = height + 'px';
+ el.style.left = (8 + ev.column * 16) + 'px';
+ el.style.display = 'block';
+
+ // Debounced hover effect (100ms delay)
+ let hoverTimeout;
+ el.addEventListener('mouseenter', function() {
+ hoverTimeout = setTimeout(() => el.classList.add('hover-active'), 100);
+ });
+ el.addEventListener('mouseleave', function() {
+ clearTimeout(hoverTimeout);
+ el.classList.remove('hover-active');
+ });
+
+ const url = el.dataset.url;
+ if (url) {
+ el.style.cursor = 'pointer';
+ el.addEventListener('click', function(e) {
+ if (e.target.tagName !== 'INPUT') {
+ window.open(url, '_blank');
+ }
+ });
+ }
+ });
+
+ // Position the "now" line
+ if (nowLineId) {
+ const nowLine = document.getElementById(nowLineId);
+ if (nowLine && !isNaN(nowHour)) {
+ const nowTop = (nowHour - startHour) * hourHeight + (nowMinute / 60) * hourHeight;
+ if (nowTop >= 0) {
+ nowLine.style.top = nowTop + 'px';
+ nowLine.style.display = 'block';
+ }
+ }
+ }
+
+ // Hide untimed section if empty
+ if (untimedSectionId) {
+ const untimedSection = document.getElementById(untimedSectionId);
+ if (untimedSection && untimedSection.querySelectorAll('.untimed-item').length === 0) {
+ untimedSection.style.display = 'none';
+ }
+ }
+}
+
+function initAllTimelineCalendars() {
+ initTimelineCalendar('today-calendar', 'now-line', 'untimed-section');
+ initTimelineCalendar('tomorrow-calendar', null, 'tomorrow-untimed-section');
+}
+
// Get CSRF token from body hx-headers attribute
function getCSRFToken() {
const body = document.body;
@@ -45,6 +147,12 @@ function setupHtmxListeners() {
}
});
+ // After HTMX swap completes - reinitialize JS components
+ document.body.addEventListener('htmx:afterSwap', function(evt) {
+ // Initialize timeline calendars if they exist in the swapped content
+ initAllTimelineCalendars();
+ });
+
// After HTMX request completes
document.body.addEventListener('htmx:afterRequest', function(evt) {
const target = evt.detail.target;