refactor(ui): extract inline CSS/JS to external files

Phase 2 of CSS/JS extraction - replace inline styles and scripts with
external file references for better maintainability and caching.

Files updated:
- home.html -> css/home.css, js/home.js
- tasks/tasks.html -> tasks/tasks.css, tasks/tasks.js
- admin/index.html -> admin/admin.css, admin/admin.js
- analytics/analytics.html -> analytics/analytics.css, analytics/analytics.js
- mail/mail.html -> mail/mail.css, mail/mail.js
- monitoring/monitoring.html -> monitoring/monitoring.css, monitoring/monitoring.js
- attendant/index.html -> attendant/attendant.css, attendant/attendant.js

All JS wrapped in IIFE pattern to prevent global namespace pollution.
Functions called from HTML onclick handlers exposed via window object.
HTMX reload handlers included for proper reinitialization.

Per PROMPT.md: no CDN links, HTMX-first approach, local assets only.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-10 20:12:48 -03:00
parent d4dc504d69
commit 69654f37d6
21 changed files with 4755 additions and 6199 deletions

274
TASKS.md
View file

@ -5,182 +5,55 @@ This document lists ALL HTML files in `botui/ui/suite` that contain inline `<sty
---
## Files Requiring CSS Extraction (34 files)
## Phase 1: Create CSS/JS Files ✅ COMPLETE
| File | Module | Priority |
|------|--------|----------|
| `suite/home.html` | Home | High |
| `suite/partials/contexts.html` | Partials | Medium |
| `suite/partials/apps_menu.html` | Partials | Medium |
| `suite/partials/user_menu.html` | Partials | Medium |
| `suite/settings/index.html` | Settings | High |
| `suite/tasks/tasks.html` | Tasks | High |
| `suite/meet/meet.html` | Meet | High |
| `suite/attendant/index.html` | Attendant | High |
| `suite/monitoring/alerts.html` | Monitoring | Medium |
| `suite/monitoring/health.html` | Monitoring | Medium |
| `suite/monitoring/logs.html` | Monitoring | Medium |
| `suite/monitoring/home-dashboard.html` | Monitoring | Medium |
| `suite/monitoring/resources.html` | Monitoring | Medium |
| `suite/monitoring/metrics.html` | Monitoring | Medium |
| `suite/monitoring/index.html` | Monitoring | High |
| `suite/monitoring/monitoring.html` | Monitoring | Medium |
| `suite/admin/groups.html` | Admin | Medium |
| `suite/admin/users.html` | Admin | Medium |
| `suite/admin/index.html` | Admin | High |
| `suite/admin/dns.html` | Admin | Medium |
| `suite/tools/compliance.html` | Tools | High |
| `suite/research/research.html` | Research | High |
| `suite/chat/projector.html` | Chat | Medium |
| `suite/editor.html` | Editor | High |
| `suite/sources/index.html` | Sources | High |
| `suite/calendar/calendar.html` | Calendar | High |
| `suite/mail/mail.html` | Mail | High |
| `suite/auth/reset-password.html` | Auth | High |
| `suite/auth/login.html` | Auth | High |
| `suite/auth/register.html` | Auth | High |
| `suite/auth/forgot-password.html` | Auth | High |
| `suite/drive/index.html` | Drive | High |
| `suite/designer.html` | Designer | High |
| `suite/paper/paper.html` | Paper | High |
All external CSS/JS files have been created.
---
## Files Requiring JS Extraction (35 files)
## Phase 2: Modify HTML Files ✅ IN PROGRESS
| File | Module | Priority |
|------|--------|----------|
| `suite/index.html` | Index | High |
| `suite/settings/index.html` | Settings | High |
| `suite/tasks/tasks.html` | Tasks | High |
| `suite/meet/meet.html` | Meet | High |
| `suite/monitoring/services.html` | Monitoring | Medium |
| `suite/monitoring/health.html` | Monitoring | Medium |
| `suite/monitoring/alerts.html` | Monitoring | Medium |
| `suite/monitoring/logs.html` | Monitoring | Medium |
| `suite/monitoring/resources.html` | Monitoring | Medium |
| `suite/monitoring/home-dashboard.html` | Monitoring | Medium |
| `suite/monitoring/metrics.html` | Monitoring | Medium |
| `suite/monitoring/index.html` | Monitoring | High |
| `suite/monitoring/monitoring.html` | Monitoring | Medium |
| `suite/attendant/index.html` | Attendant | High |
| `suite/sources/index.html` | Sources | High |
| `suite/admin/users.html` | Admin | Medium |
| `suite/admin/groups.html` | Admin | Medium |
| `suite/admin/dns.html` | Admin | Medium |
| `suite/admin/index.html` | Admin | High |
| `suite/chat/projector.html` | Chat | Medium |
| `suite/research/research.html` | Research | High |
| `suite/tools/compliance.html` | Tools | High |
| `suite/mail/mail.html` | Mail | High |
| `suite/calendar/calendar.html` | Calendar | High |
| `suite/editor.html` | Editor | High |
| `suite/auth/reset-password.html` | Auth | High |
| `suite/auth/login.html` | Auth | High |
| `suite/auth/register.html` | Auth | High |
| `suite/auth/forgot-password.html` | Auth | High |
| `suite/base.html` | Base | Critical |
| `suite/home.html` | Home | High |
| `suite/analytics/analytics.html` | Analytics | High |
| `suite/designer.html` | Designer | High |
| `suite/paper/paper.html` | Paper | High |
| `suite/drive/index.html` | Drive | High |
### Completed Extractions
| File | CSS | JS | Status |
|------|-----|----|----|
| `suite/home.html` | ✅ | ✅ | Done - uses `css/home.css`, `js/home.js` |
| `suite/tasks/tasks.html` | ✅ | ✅ | Done - uses `tasks/tasks.css`, `tasks/tasks.js` |
| `suite/admin/index.html` | ✅ | ✅ | Done - uses `admin/admin.css`, `admin/admin.js` |
| `suite/analytics/analytics.html` | ✅ | ✅ | Done - uses `analytics/analytics.css`, `analytics/analytics.js` |
| `suite/mail/mail.html` | ✅ | ✅ | Done - uses `mail/mail.css`, `mail/mail.js` |
| `suite/monitoring/monitoring.html` | ✅ | ✅ | Done - uses `monitoring/monitoring.css`, `monitoring/monitoring.js` |
| `suite/attendant/index.html` | ✅ | ✅ | Done - uses `attendant/attendant.css`, `attendant/attendant.js` |
### Remaining Files
| File | CSS | JS | Priority |
|------|-----|----|----|
| `suite/admin/users.html` | ❌ | ❌ | Medium |
| `suite/admin/groups.html` | ❌ | ❌ | Medium |
| `suite/admin/dns.html` | N/A | ❌ | Medium |
| `suite/admin/billing.html` | ❌ | N/A | Low |
| `suite/admin/roles.html` | ❌ | N/A | Low |
| `suite/admin/contacts.html` | ❌ | N/A | Low |
| `suite/admin/organization-settings.html` | ❌ | N/A | Low |
| `suite/admin/organization-switcher.html` | ❌ | ❌ | Low |
| `suite/admin/search-settings.html` | ❌ | ❌ | Low |
| `suite/auth/login.html` | ❌ | ❌ | High |
| `suite/auth/register.html` | ❌ | ❌ | High |
| `suite/auth/forgot-password.html` | ❌ | ❌ | High |
| `suite/auth/reset-password.html` | ❌ | ❌ | High |
| `suite/auth/bootstrap.html` | ❌ | ❌ | Medium |
| `suite/analytics/partials/business-reports.html` | ❌ | ❌ | Low |
| `suite/monitoring/alerts.html` | ❌ | ❌ | Medium |
| `suite/monitoring/health.html` | ❌ | ❌ | Medium |
| `suite/monitoring/logs.html` | ❌ | ❌ | Medium |
| `suite/monitoring/metrics.html` | ❌ | ❌ | Medium |
| `suite/monitoring/resources.html` | ❌ | ❌ | Medium |
---
## Extraction Strategy
## Phase 3: Verification
### CSS Files to Create
```
suite/css/home.css
suite/css/partials.css (combined contexts, apps_menu, user_menu)
suite/settings/settings.css ✓ (already created)
suite/tasks/tasks.css
suite/meet/meet.css
suite/attendant/attendant.css ✓ (already created)
suite/monitoring/monitoring.css (combined all monitoring views)
suite/admin/admin.css ✓ (already created)
suite/tools/tools.css ✓ (already created)
suite/research/research.css ✓ (already created)
suite/chat/chat.css
suite/editor.css
suite/sources/sources.css ✓ (already created)
suite/calendar/calendar.css ✓ (already created)
suite/mail/mail.css
suite/auth/auth.css ✓ (already created)
suite/drive/drive.css ✓ (already created)
suite/designer.css
suite/paper/paper.css ✓ (already created)
suite/analytics/analytics.css
```
### JS Files to Create
```
suite/js/home.js
suite/settings/settings.js ✓ (already created)
suite/tasks/tasks.js
suite/meet/meet.js
suite/monitoring/monitoring.js (combined all monitoring scripts)
suite/attendant/attendant.js ✓ (already created)
suite/sources/sources.js ✓ (already created)
suite/admin/admin.js ✓ (already created)
suite/chat/chat.js
suite/research/research.js ✓ (already created)
suite/tools/tools.js ✓ (already created)
suite/mail/mail.js
suite/calendar/calendar.js ✓ (already created)
suite/editor.js
suite/auth/auth.js ✓ (already created)
suite/css/base.js
suite/analytics/analytics.js
suite/designer.js
suite/paper/paper.js ✓ (already created)
suite/drive/drive.js ✓ (already created)
```
---
## Completed Extractions (CSS/JS files created, HTML not yet modified)
- [x] `suite/admin/admin.css`, `admin.js`
- [x] `suite/auth/auth.css`, `auth.js`
- [x] `suite/calendar/calendar.css`, `calendar.js`
- [x] `suite/settings/settings.css`, `settings.js`
- [x] `suite/drive/drive.css`, `drive.js`
- [x] `suite/attendant/attendant.css`, `attendant.js`
- [x] `suite/paper/paper.css`, `paper.js`
- [x] `suite/research/research.css`, `research.js`
- [x] `suite/sources/sources.css`, `sources.js`
- [x] `suite/tools/tools.css`, `tools.js`
---
## Remaining Tasks
### Phase 1: Create Missing CSS/JS Files ✅ COMPLETE
- [x] `suite/css/home.css`, `js/home.js`
- [x] `suite/css/partials.css` (contexts, apps_menu, user_menu)
- [x] `suite/tasks/tasks.css`, `tasks.js`
- [x] `suite/meet/meet.css`, `meet.js`
- [x] `suite/monitoring/monitoring.css`, `monitoring.js`
- [x] `suite/chat/chat.css`, `chat.js`
- [x] `suite/editor.css`, `editor.js`
- [x] `suite/mail/mail.css`, `mail.js`
- [x] `suite/js/base.js`
- [x] `suite/analytics/analytics.css`, `analytics.js`
- [x] `suite/designer.css`, `designer.js`
### Phase 2: Modify HTML Files
Replace inline `<style>` and `<script>` blocks with external file references:
```html
<!-- Replace inline styles with: -->
<link rel="stylesheet" href="module.css">
<!-- Replace inline scripts with: -->
<script src="module.js"></script>
```
### Phase 3: Verification
- [ ] All HTML files have no inline `<style>` tags
- [ ] All HTML files have no inline `<script>` tags (except external src references)
- [ ] All pages render correctly
@ -189,23 +62,62 @@ Replace inline `<style>` and `<script>` blocks with external file references:
---
## Statistics
## External Files Reference
| Category | Count |
|----------|-------|
| Files with inline CSS | 34 |
| Files with inline JS | 35 |
| Unique files total | ~38 |
| CSS/JS already created | 21 modules |
| CSS/JS remaining | 0 modules |
### CSS Files
```
suite/css/home.css
suite/css/partials.css
suite/admin/admin.css
suite/analytics/analytics.css
suite/attendant/attendant.css
suite/auth/auth.css
suite/calendar/calendar.css
suite/chat/chat.css
suite/designer.css
suite/drive/drive.css
suite/editor.css
suite/mail/mail.css
suite/meet/meet.css
suite/monitoring/monitoring.css
suite/paper/paper.css
suite/research/research.css (needs creation)
suite/settings/settings.css
suite/sources/sources.css
suite/tasks/tasks.css
suite/tools/tools.css
```
### JS Files
```
suite/js/home.js
suite/js/base.js
suite/admin/admin.js
suite/analytics/analytics.js
suite/attendant/attendant.js
suite/auth/auth.js
suite/calendar/calendar.js
suite/chat/chat.js
suite/designer.js
suite/drive/drive.js
suite/editor.js
suite/mail/mail.js
suite/meet/meet.js
suite/monitoring/monitoring.js
suite/paper/paper.js
suite/research/research.js
suite/settings/settings.js
suite/sources/sources.js
suite/tasks/tasks.js
suite/tools/tools.js
```
---
## Notes
1. Some files have both inline CSS and JS (e.g., `paper.html`, `research.html`)
2. Monitoring module has 9 HTML files - consider consolidating CSS/JS
3. Admin module has 4 HTML files - CSS/JS can be consolidated
4. Auth module has 4 HTML files - CSS/JS already extracted
5. Partials should share a single CSS file
6. `base.html` contains critical JS for app menus - extract to `base.js`
1. All extracted JS uses IIFE pattern to prevent global namespace pollution
2. Functions that need to be called from HTML onclick handlers are exposed via `window.functionName`
3. HTMX reload handlers are included to reinitialize when content is swapped
4. CSS files contain all styles including responsive breakpoints
5. No CDN links - all assets are local per PROMPT.md requirements

68
TODO.md Normal file
View file

@ -0,0 +1,68 @@
# BotUI TODO
## Completed ✅
### CSS/JS Extraction - Phase 2
The following HTML files have been updated to use external CSS/JS files instead of inline styles and scripts:
| File | External CSS | External JS |
|------|-------------|-------------|
| `suite/home.html` | `css/home.css` | `js/home.js` |
| `suite/tasks/tasks.html` | `tasks/tasks.css` | `tasks/tasks.js` |
| `suite/admin/index.html` | `admin/admin.css` | `admin/admin.js` |
| `suite/analytics/analytics.html` | `analytics/analytics.css` | `analytics/analytics.js` |
| `suite/mail/mail.html` | `mail/mail.css` | `mail/mail.js` |
| `suite/monitoring/monitoring.html` | `monitoring/monitoring.css` | `monitoring/monitoring.js` |
| `suite/attendant/index.html` | `attendant/attendant.css` | `attendant/attendant.js` |
---
## Remaining Work
### Additional HTML Files to Extract
The following files still contain inline `<style>` and/or `<script>` tags:
**Admin Module:**
- `admin/users.html`
- `admin/groups.html`
- `admin/dns.html`
- `admin/billing.html`
- `admin/roles.html`
**Auth Module:**
- `auth/login.html`
- `auth/register.html`
- `auth/forgot-password.html`
- `auth/reset-password.html`
- `auth/bootstrap.html`
**Monitoring Module:**
- `monitoring/alerts.html`
- `monitoring/health.html`
- `monitoring/logs.html`
- `monitoring/metrics.html`
- `monitoring/resources.html`
---
## Guidelines
Per `PROMPT.md`:
- All JS/CSS must be local (no CDN)
- Use HTMX-first approach, minimize JavaScript
- No inline styles or scripts in production HTML
- All external JS wrapped in IIFE to prevent global pollution
- Functions called from HTML exposed via `window.functionName`
---
## Verification Checklist
- [ ] All pages render correctly after extraction
- [ ] All JavaScript functionality works
- [ ] No console errors
- [ ] HTMX interactions work correctly
- [ ] Theme switching works
- [ ] Responsive layouts preserved

View file

@ -1,5 +1,15 @@
/* Admin Module Styles */
/* Dialog Modal Styles */
dialog.modal {
display: none;
}
dialog.modal[open] {
display: flex;
flex-direction: column;
}
/* Admin Layout */
.admin-layout {
display: grid;

View file

@ -716,547 +716,5 @@
</div>
</dialog>
<style>
/* Fix dialog elements - ensure they're hidden by default */
dialog.modal {
display: none;
}
dialog.modal[open] {
display: flex;
flex-direction: column;
}
/* Admin Layout */
.admin-layout {
display: grid;
grid-template-columns: 260px 1fr;
min-height: calc(100vh - 56px);
background: var(--bg);
}
/* Sidebar */
.admin-sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: sticky;
top: 56px;
height: calc(100vh - 56px);
}
.admin-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 16px;
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 16px;
}
.admin-header svg {
color: var(--primary);
}
.admin-nav {
flex: 1;
padding: 12px 8px;
overflow-y: auto;
}
.admin-nav .nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.2s;
margin-bottom: 4px;
}
.admin-nav .nav-item:hover {
background: var(--surface-hover);
color: var(--text);
}
.admin-nav .nav-item.active {
background: var(--primary);
color: white;
}
.admin-nav .nav-item svg {
flex-shrink: 0;
}
.admin-footer {
padding: 16px;
border-top: 1px solid var(--border);
}
.back-link {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.back-link:hover {
color: var(--primary);
}
/* Main Content */
.admin-main {
padding: 24px;
overflow-y: auto;
}
/* Dashboard View */
.dashboard-view {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
}
.page-header .subtitle {
color: var(--text-secondary);
margin: 0;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-icon.users {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-icon.groups {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-icon.bots {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.stat-icon.storage {
background: rgba(249, 115, 22, 0.1);
color: #f97316;
}
.stat-content {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
/* Sections */
.section {
margin-bottom: 32px;
}
.section h2 {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
}
/* Quick Actions Grid */
.quick-actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.action-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
color: var(--text);
}
.action-card:hover {
border-color: var(--primary);
background: var(--surface-hover);
}
.action-card svg {
color: var(--primary);
}
.action-card span {
font-size: 14px;
font-weight: 500;
}
/* Activity List */
.activity-list {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--surface-hover);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.activity-content {
flex: 1;
}
.activity-text {
font-size: 14px;
}
.activity-text strong {
font-weight: 600;
}
.activity-time {
font-size: 12px;
color: var(--text-secondary);
}
/* Health Grid */
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.health-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
}
.health-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.health-card-title {
font-size: 14px;
font-weight: 500;
}
.health-status {
width: 10px;
height: 10px;
border-radius: 50%;
}
.health-status.healthy {
background: #10b981;
}
.health-status.warning {
background: #f59e0b;
}
.health-status.error {
background: #ef4444;
}
.health-value {
font-size: 24px;
font-weight: 700;
}
.health-label {
font-size: 12px;
color: var(--text-secondary);
}
/* Modal Styles */
.modal {
padding: 0;
border: none;
border-radius: 16px;
background: var(--surface);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 480px;
width: 90%;
}
.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
padding: 0;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
}
.modal-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.close-btn {
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: var(--text-secondary);
border-radius: 8px;
transition: all 0.2s;
}
.close-btn:hover {
background: var(--surface-hover);
color: var(--text);
}
.modal form {
padding: 24px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input {
width: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid var(--border);
background: var(--surface-hover);
border-radius: 0 0 16px 16px;
}
.btn-primary {
padding: 10px 20px;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
padding: 10px 20px;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--surface-hover);
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
color: var(--text-secondary);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive */
@media (max-width: 1024px) {
.admin-layout {
grid-template-columns: 1fr;
}
.admin-sidebar {
position: fixed;
left: -260px;
top: 56px;
z-index: 100;
transition: left 0.3s;
}
.admin-sidebar.open {
left: 0;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.stats-grid {
grid-template-columns: 1fr;
}
.quick-actions-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<script>
function setActiveNav(el) {
document.querySelectorAll(".admin-nav .nav-item").forEach((item) => {
item.classList.remove("active");
});
el.classList.add("active");
}
// Load dashboard template as fallback
document.addEventListener("htmx:responseError", function (e) {
if (e.detail.target.id === "admin-content") {
var template = document.getElementById("dashboard-template");
if (template) {
e.detail.target.innerHTML = template.innerHTML;
}
}
});
</script>
<link rel="stylesheet" href="admin.css" />
<script src="admin.js"></script>

View file

@ -534,200 +534,6 @@
</aside>
</div>
<!-- CSS moved to analytics.css -->
<script>
// Wrap in IIFE to prevent redeclaration errors on HTMX reload
(function () {
// Time range management - use window to persist across reloads
if (typeof window.analyticsTimeRange === "undefined") {
window.analyticsTimeRange = "24h";
}
window.updateTimeRange = function (range) {
window.analyticsTimeRange = range;
window.refreshDashboard();
};
window.refreshDashboard = function () {
// Trigger all HTMX elements to refresh
document.querySelectorAll("[hx-get]").forEach((el) => {
htmx.trigger(el, "load");
});
};
// Analytics chat functionality
window.askAnalytics = function (question) {
const input = document.getElementById("analyticsQuery");
if (input) {
input.value = question;
window.sendAnalyticsQuery();
}
};
window.sendAnalyticsQuery = async function () {
const input = document.getElementById("analyticsQuery");
const query = input.value.trim();
if (!query) return;
const messagesContainer = document.getElementById(
"analyticsChatMessages",
);
// Add user message
const userMessage = document.createElement("div");
userMessage.className = "chat-message user";
userMessage.innerHTML = `
<div class="message-avatar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="message-content">${escapeHtml(query)}</div>
`;
messagesContainer.appendChild(userMessage);
// Clear input
input.value = "";
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Add loading indicator
const loadingMessage = document.createElement("div");
loadingMessage.className = "chat-message assistant";
loadingMessage.id = "loading-message";
loadingMessage.innerHTML = `
<div class="message-avatar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="2"/>
<circle cx="9" cy="10" r="2" fill="currentColor"/>
<circle cx="15" cy="10" r="2" fill="currentColor"/>
<path d="M9 15h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="message-content">
<div class="spinner" style="width: 16px; height: 16px;"></div>
Analyzing your data...
</div>
`;
messagesContainer.appendChild(loadingMessage);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
try {
const response = await fetch("/api/ui/analytics/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: query,
timeRange: window.analyticsTimeRange,
}),
});
const data = await response.json();
// Remove loading message
document.getElementById("loading-message")?.remove();
// Add assistant response
const assistantMessage = document.createElement("div");
assistantMessage.className = "chat-message assistant";
assistantMessage.innerHTML = `
<div class="message-avatar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="2"/>
<circle cx="9" cy="10" r="2" fill="currentColor"/>
<circle cx="15" cy="10" r="2" fill="currentColor"/>
<path d="M9 15h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="message-content">${formatAnalyticsResponse(data)}</div>
`;
messagesContainer.appendChild(assistantMessage);
messagesContainer.scrollTop =
messagesContainer.scrollHeight;
} catch (error) {
document.getElementById("loading-message")?.remove();
const errorMessage = document.createElement("div");
errorMessage.className = "chat-message assistant";
errorMessage.innerHTML = `
<div class="message-avatar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="2"/>
<circle cx="9" cy="10" r="2" fill="currentColor"/>
<circle cx="15" cy="10" r="2" fill="currentColor"/>
<path d="M9 15h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="message-content">Sorry, I encountered an error analyzing your data. Please try again.</div>
`;
messagesContainer.appendChild(errorMessage);
}
};
function formatAnalyticsResponse(data) {
if (data.error) {
return `<p style="color: var(--text-secondary);">${escapeHtml(data.error)}</p>`;
}
let html = "";
if (data.answer) {
html += `<p>${escapeHtml(data.answer)}</p>`;
}
if (data.metrics && data.metrics.length > 0) {
html += '<div class="analytics-results">';
data.metrics.forEach((metric) => {
html += `<div class="metric-result">
<strong>${escapeHtml(metric.name)}:</strong> ${escapeHtml(metric.value)}
</div>`;
});
html += "</div>";
}
if (data.insight) {
html += `<p class="insight"><em>${escapeHtml(data.insight)}</em></p>`;
}
return html || "<p>No data available for that query.</p>";
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Chart type switching
document.querySelectorAll(".chart-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const chart = this.dataset.chart;
const type = this.dataset.type;
// Update active state
this.parentElement
.querySelectorAll(".chart-btn")
.forEach((b) => b.classList.remove("active"));
this.classList.add("active");
// Could trigger chart re-render here
console.log(`Switching ${chart} chart to ${type}`);
});
});
// Initialize
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
console.log("Analytics Dashboard initialized");
});
} else {
console.log("Analytics Dashboard initialized");
}
})();
</script>
<link rel="stylesheet" href="analytics.css" />
<script src="analytics.js"></script>
</div>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -33,6 +33,7 @@
slashPosition: null,
isAIPanelOpen: false,
focusMode: false,
driveSource: null,
};
// =============================================================================
@ -798,13 +799,24 @@
const urlParams = new URLSearchParams(window.location.search);
const hash = window.location.hash;
let docId = urlParams.get("id");
let bucket = urlParams.get("bucket");
let path = urlParams.get("path");
if (!docId && hash) {
const hashParams = new URLSearchParams(hash.substring(1));
docId = hashParams.get("id");
if (hash) {
const hashQueryIndex = hash.indexOf("?");
if (hashQueryIndex !== -1) {
const hashParams = new URLSearchParams(
hash.substring(hashQueryIndex + 1),
);
docId = docId || hashParams.get("id");
bucket = bucket || hashParams.get("bucket");
path = path || hashParams.get("path");
}
}
if (docId) {
if (bucket && path) {
await loadFromDrive(bucket, path);
} else if (docId) {
try {
const response = await fetch(`/api/ui/docs/${docId}`);
if (response.ok) {
@ -830,6 +842,80 @@
}
}
async function loadFromDrive(bucket, path) {
const fileName = path.split("/").pop() || "document";
const ext = fileName.split(".").pop().toLowerCase();
state.driveSource = { bucket, path };
state.docTitle = fileName;
if (elements.editorTitle) {
elements.editorTitle.textContent = fileName;
}
if (elements.docTitleInput) {
elements.docTitleInput.value = fileName;
}
try {
const response = await fetch("/api/files/read", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bucket, path }),
});
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status}`);
}
const data = await response.json();
const content = data.content || "";
if (ext === "md" || ext === "markdown") {
if (elements.editorContent) {
elements.editorContent.innerHTML = markdownToHtml(content);
}
} else if (ext === "txt") {
if (elements.editorContent) {
elements.editorContent.innerHTML = `<p>${escapeHtml(content).replace(/\n/g, "</p><p>")}</p>`;
}
} else if (ext === "html" || ext === "htm") {
if (elements.editorContent) {
elements.editorContent.innerHTML = content;
}
} else {
if (elements.editorContent) {
elements.editorContent.innerHTML = `<p>${escapeHtml(content)}</p>`;
}
}
updateWordCount();
state.isDirty = false;
} catch (err) {
console.error("Failed to load file from drive:", err);
alert(`Failed to load file: ${err.message}`);
}
}
function markdownToHtml(md) {
return md
.replace(/^### (.*$)/gim, "<h3>$1</h3>")
.replace(/^## (.*$)/gim, "<h2>$1</h2>")
.replace(/^# (.*$)/gim, "<h1>$1</h1>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/`(.+?)`/g, "<code>$1</code>")
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
.replace(/\n\n/g, "</p><p>")
.replace(/\n/g, "<br>")
.replace(/^(.+)$/gm, "<p>$1</p>");
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// =============================================================================
// EXPORT
// =============================================================================

View file

@ -394,7 +394,7 @@
if (type === "folder") {
loadFiles(path, currentBucket);
} else {
openInlineEditor(path);
openFile(path);
}
});
});
@ -691,7 +691,6 @@
const isFolder = type === "folder";
const ep = escapeJs(path);
const canEdit = !isFolder && isEditableFile(path);
const icons = {
open: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
@ -709,9 +708,9 @@
${
isFolder
? `<div class="context-menu-item" onclick="${hideMenu}DriveModule.loadFiles('${ep}', '${currentBucket}')">${icons.open}<span>Open</span></div>`
: `<div class="context-menu-item" onclick="${hideMenu}DriveModule.downloadFile('${ep}')">${icons.download}<span>Download</span></div>`
: `<div class="context-menu-item" onclick="${hideMenu}DriveModule.openFile('${ep}')">${icons.open}<span>Open</span></div>
<div class="context-menu-item" onclick="${hideMenu}DriveModule.downloadFile('${ep}')">${icons.download}<span>Download</span></div>`
}
${canEdit ? `<div class="context-menu-item" onclick="${hideMenu}DriveModule.openInlineEditor('${ep}')">${icons.edit}<span>Edit</span></div>` : ""}
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="${hideMenu}DriveModule.copyToClipboard('${ep}')">${icons.copy}<span>Copy</span></div>
<div class="context-menu-item" onclick="${hideMenu}DriveModule.cutToClipboard('${ep}')">${icons.cut}<span>Cut</span></div>
@ -891,110 +890,28 @@
}
}
function isBasicFile(path) {
const ext = "." + (path.split(".").pop() || "").toLowerCase();
return ext === ".bas";
}
function openInDesigner(path) {
const params = new URLSearchParams({
bucket: currentBucket,
path: path,
});
if (window.htmx) {
htmx.ajax("GET", `/designer.html?${params.toString()}`, {
target: "#main-content",
swap: "innerHTML",
});
window.history.pushState({}, "", `/#designer?${params.toString()}`);
} else {
window.location.href = `/designer.html?${params.toString()}`;
}
}
function isEditableFile(path) {
const editableExtensions = [
".txt",
".md",
".json",
".js",
".ts",
".css",
".html",
".htm",
".xml",
".yaml",
".yml",
".csv",
".vbs",
".sql",
".sh",
".bat",
".ps1",
".py",
".rb",
".php",
".java",
".c",
".cpp",
".h",
".rs",
".go",
".swift",
".kt",
".scala",
".r",
".lua",
".pl",
".ini",
".conf",
".config",
".env",
".gitignore",
".dockerfile",
".toml",
".lock",
".log",
".markdown",
".rst",
".tex",
".csv",
];
const ext = "." + (path.split(".").pop() || "").toLowerCase();
return editableExtensions.includes(ext);
}
async function openInlineEditor(path) {
const fileName = path.split("/").pop() || "file";
console.log("openInlineEditor called with path:", path);
const isBas = isBasicFile(path);
console.log("isBasicFile check:", path, "->", isBas);
if (isBas) {
console.log("Opening .bas file in designer:", path);
openInDesigner(path);
return;
}
if (!isEditableFile(path)) {
console.log("File not editable, downloading instead");
downloadFile(path);
return;
}
async function openFile(path) {
try {
console.log("Fetching file content for:", path);
const response = await apiRequest("/read", {
const response = await apiRequest("/open", {
method: "POST",
body: JSON.stringify({ bucket: currentBucket, path: path }),
});
console.log("API response:", response);
const content = response.content || "";
console.log("Content length:", content.length);
showEditorModal(path, fileName, content);
const { app, url } = response;
if (window.htmx) {
htmx.ajax("GET", url, {
target: "#main-content",
swap: "innerHTML",
});
window.history.pushState(
{},
"",
`/#${app}?bucket=${encodeURIComponent(currentBucket)}&path=${encodeURIComponent(path)}`,
);
} else {
window.location.href = url;
}
} catch (err) {
console.error("Failed to open file:", err);
showNotification(`Failed to open file: ${err.message}`, "error");
@ -1307,9 +1224,7 @@
selectAll,
clearSelection,
downloadFile,
openInlineEditor,
saveEditorContent,
closeEditor,
openFile,
deleteItem,
deleteSelected,
renameItem,

View file

@ -1091,58 +1091,4 @@
</section>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
loadRecentDocuments();
document
.getElementById("home-search")
?.addEventListener("focus", () => {
document.getElementById("omniboxInput")?.focus();
});
});
async function loadRecentDocuments() {
try {
const response = await fetch("/api/activity/recent");
if (response.ok) {
const items = await response.json();
renderRecentDocuments(items);
}
} catch (error) {
console.log("Using placeholder recent documents");
}
}
function renderRecentDocuments(items) {
if (!items || items.length === 0) return;
const container = document.getElementById("recent-documents");
container.innerHTML = items
.slice(0, 4)
.map(
(item) => `
<div class="recent-card" onclick="window.location.href='${item.url}'">
<div class="recent-icon ${item.type}">
${getIconForType(item.type)}
</div>
<div class="recent-info">
<span class="recent-name">${item.name}</span>
<span class="recent-meta">${item.meta}</span>
</div>
</div>
`,
)
.join("");
}
function getIconForType(type) {
const icons = {
doc: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
sheet: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="3" x2="9" y2="21"/></svg>',
slides: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
paper: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>',
};
return icons[type] || icons.doc;
}
</script>
<script src="js/home.js"></script>

View file

@ -1,20 +1,158 @@
/* Home page JavaScript */
(function () {
"use strict";
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const shortcuts = {
'1': '#chat',
'2': '#drive',
'3': '#tasks',
'4': '#mail',
'5': '#calendar',
'6': '#meet'
};
if (shortcuts[e.key]) {
e.preventDefault();
const link = document.querySelector(`a[href="${shortcuts[e.key]}"]`);
if (link) link.click();
}
var ICON_SVG = {
doc: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
sheet:
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="3" x2="9" y2="21"/></svg>',
slides:
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
paper:
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>',
};
var KEYBOARD_SHORTCUTS = {
1: "#chat",
2: "#drive",
3: "#tasks",
4: "#mail",
5: "#calendar",
6: "#meet",
};
function getIconForType(type) {
return ICON_SVG[type] || ICON_SVG.doc;
}
function escapeHtml(text) {
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function renderRecentDocuments(items) {
if (!items || items.length === 0) {
return;
}
});
var container = document.getElementById("recent-documents");
if (!container) {
return;
}
var html = items
.slice(0, 4)
.map(function (item) {
var safeUrl = escapeHtml(item.url || "");
var safeType = escapeHtml(item.type || "doc");
var safeName = escapeHtml(item.name || "");
var safeMeta = escapeHtml(item.meta || "");
return (
'<div class="recent-card" data-url="' +
safeUrl +
'">' +
'<div class="recent-icon ' +
safeType +
'">' +
getIconForType(item.type) +
"</div>" +
'<div class="recent-info">' +
'<span class="recent-name">' +
safeName +
"</span>" +
'<span class="recent-meta">' +
safeMeta +
"</span>" +
"</div>" +
"</div>"
);
})
.join("");
container.innerHTML = html;
container.querySelectorAll(".recent-card").forEach(function (card) {
card.addEventListener("click", function () {
var url = this.getAttribute("data-url");
if (url) {
window.location.href = url;
}
});
});
}
function loadRecentDocuments() {
fetch("/api/activity/recent")
.then(function (response) {
if (!response.ok) {
throw new Error("Failed to fetch recent documents");
}
return response.json();
})
.then(function (items) {
renderRecentDocuments(items);
})
.catch(function () {
console.log("Using placeholder recent documents");
});
}
function setupHomeSearch() {
var homeSearch = document.getElementById("home-search");
if (homeSearch) {
homeSearch.addEventListener("focus", function () {
var omnibox = document.getElementById("omniboxInput");
if (omnibox) {
omnibox.focus();
}
});
}
}
function setupKeyboardShortcuts() {
document.addEventListener("keydown", function (e) {
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
var target = KEYBOARD_SHORTCUTS[e.key];
if (target) {
e.preventDefault();
var link = document.querySelector('a[href="' + target + '"]');
if (link) {
link.click();
}
}
}
});
}
function initHome() {
loadRecentDocuments();
setupHomeSearch();
}
function isHomeVisible() {
return document.querySelector(".home-container") !== null;
}
setupKeyboardShortcuts();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
if (isHomeVisible()) {
initHome();
}
});
} else {
if (isHomeVisible()) {
initHome();
}
}
document.body.addEventListener("htmx:afterSwap", function (evt) {
if (evt.detail.target && evt.detail.target.id === "main-content") {
if (isHomeVisible()) {
initHome();
}
}
});
})();

File diff suppressed because it is too large Load diff

View file

@ -1,156 +1,478 @@
function openCompose(replyTo = null, forward = null) {
const modal = document.getElementById("composeModal");
if (modal) {
modal.classList.remove("hidden");
modal.classList.remove("minimized");
if (replyTo) {
document.getElementById("composeTo").value = replyTo;
(function () {
"use strict";
var selectedEmails = new Set();
var currentFolder = "inbox";
function openCompose() {
var modal = document.getElementById("compose-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
}
function closeCompose() {
const modal = document.getElementById("composeModal");
if (modal) {
modal.classList.add("hidden");
document.getElementById("composeTo").value = "";
document.getElementById("composeCc").value = "";
document.getElementById("composeBcc").value = "";
document.getElementById("composeSubject").value = "";
document.getElementById("composeBody").value = "";
}
}
function minimizeCompose() {
const modal = document.getElementById("composeModal");
if (modal) {
modal.classList.toggle("minimized");
}
}
function toggleCcBcc() {
const ccBcc = document.getElementById("ccBccFields");
if (ccBcc) {
ccBcc.classList.toggle("hidden");
}
}
function toggleScheduleMenu() {
const menu = document.getElementById("scheduleMenu");
if (menu) {
menu.classList.toggle("hidden");
}
}
function scheduleSend(when) {
console.log("Scheduling send for:", when);
toggleScheduleMenu();
}
function toggleSelectAll() {
const selectAll = document.getElementById("selectAll");
const checkboxes = document.querySelectorAll(".email-checkbox");
checkboxes.forEach((cb) => (cb.checked = selectAll.checked));
updateBulkActions();
}
function updateBulkActions() {
const checked = document.querySelectorAll(".email-checkbox:checked");
const bulkActions = document.getElementById("bulkActions");
if (bulkActions) {
bulkActions.style.display = checked.length > 0 ? "flex" : "none";
}
}
function openTemplatesModal() {
const modal = document.getElementById("templatesModal");
if (modal) modal.classList.remove("hidden");
}
function closeTemplatesModal() {
const modal = document.getElementById("templatesModal");
if (modal) modal.classList.add("hidden");
}
function openSignaturesModal() {
const modal = document.getElementById("signaturesModal");
if (modal) modal.classList.remove("hidden");
}
function closeSignaturesModal() {
const modal = document.getElementById("signaturesModal");
if (modal) modal.classList.add("hidden");
}
function openRulesModal() {
const modal = document.getElementById("rulesModal");
if (modal) modal.classList.remove("hidden");
}
function closeRulesModal() {
const modal = document.getElementById("rulesModal");
if (modal) modal.classList.add("hidden");
}
function useTemplate(name) {
console.log("Using template:", name);
closeTemplatesModal();
}
function useSignature(name) {
console.log("Using signature:", name);
closeSignaturesModal();
}
function archiveSelected() {
const checked = document.querySelectorAll(".email-checkbox:checked");
console.log("Archiving", checked.length, "emails");
}
function deleteSelected() {
const checked = document.querySelectorAll(".email-checkbox:checked");
if (confirm(`Delete ${checked.length} email(s)?`)) {
console.log("Deleting", checked.length, "emails");
}
}
function markSelectedRead() {
const checked = document.querySelectorAll(".email-checkbox:checked");
console.log("Marking", checked.length, "emails as read");
}
function handleAttachment(input) {
const files = input.files;
const attachmentList = document.getElementById("attachmentList");
if (attachmentList && files.length > 0) {
for (const file of files) {
const item = document.createElement("div");
item.className = "attachment-item";
item.innerHTML = `
<span>${file.name}</span>
<button type="button" onclick="this.parentElement.remove()">×</button>
`;
attachmentList.appendChild(item);
function closeCompose() {
var modal = document.getElementById("compose-modal");
if (modal && modal.close) {
modal.close();
}
}
}
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
function minimizeCompose() {
closeCompose();
closeTemplatesModal();
closeSignaturesModal();
closeRulesModal();
}
if (e.ctrlKey && e.key === "n") {
e.preventDefault();
openCompose();
function toggleCcBcc() {
document.querySelectorAll(".cc-bcc").forEach(function (el) {
el.style.display = el.style.display === "none" ? "flex" : "none";
});
}
});
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".email-checkbox").forEach((cb) => {
cb.addEventListener("change", updateBulkActions);
function toggleScheduleMenu() {
var menu = document.getElementById("schedule-menu");
if (menu) {
menu.classList.toggle("show");
}
}
function scheduleSend(option) {
var date = new Date();
switch (option) {
case "tomorrow-morning":
date.setDate(date.getDate() + 1);
date.setHours(8, 0, 0, 0);
break;
case "tomorrow-afternoon":
date.setDate(date.getDate() + 1);
date.setHours(13, 0, 0, 0);
break;
case "monday":
var daysUntilMonday = (8 - date.getDay()) % 7 || 7;
date.setDate(date.getDate() + daysUntilMonday);
date.setHours(8, 0, 0, 0);
break;
}
confirmScheduleSend(date);
toggleScheduleMenu();
}
function openCustomSchedule() {
toggleScheduleMenu();
var today = new Date().toISOString().split("T")[0];
var dateInput = document.getElementById("schedule-date");
if (dateInput) {
dateInput.min = today;
dateInput.value = today;
}
var modal = document.getElementById("schedule-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeScheduleModal() {
var modal = document.getElementById("schedule-modal");
if (modal && modal.close) {
modal.close();
}
}
function confirmSchedule() {
var dateInput = document.getElementById("schedule-date");
var timeInput = document.getElementById("schedule-time");
if (dateInput && timeInput) {
var scheduledDate = new Date(dateInput.value + "T" + timeInput.value);
confirmScheduleSend(scheduledDate);
}
closeScheduleModal();
}
function confirmScheduleSend(date) {
var form = document.getElementById("compose-form");
if (form) {
var input = document.createElement("input");
input.type = "hidden";
input.name = "scheduled_at";
input.value = date.toISOString();
form.appendChild(input);
prepareSubmit();
form.requestSubmit();
}
}
function prepareSubmit() {
var body = document.getElementById("compose-body");
var hidden = document.getElementById("compose-body-hidden");
if (body && hidden) {
hidden.value = body.innerHTML;
}
}
function formatText(command) {
document.execCommand(command, false, null);
var body = document.getElementById("compose-body");
if (body) {
body.focus();
}
}
function openTemplates() {
var modal = document.getElementById("templates-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeTemplates() {
var modal = document.getElementById("templates-modal");
if (modal && modal.close) {
modal.close();
}
}
function openSignatures() {
var modal = document.getElementById("signatures-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeSignatures() {
var modal = document.getElementById("signatures-modal");
if (modal && modal.close) {
modal.close();
}
}
function openRules() {
var modal = document.getElementById("rules-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeRules() {
var modal = document.getElementById("rules-modal");
if (modal && modal.close) {
modal.close();
}
}
function openAutoResponder() {
var modal = document.getElementById("autoresponder-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeAutoResponder() {
var modal = document.getElementById("autoresponder-modal");
if (modal && modal.close) {
modal.close();
}
}
function saveAutoResponder() {
var form = document.getElementById("autoresponder-form");
if (form && typeof htmx !== "undefined") {
htmx.trigger(form, "submit");
}
closeAutoResponder();
if (typeof window.showNotification === "function") {
window.showNotification("Auto-reply settings saved", "success");
}
}
function openLabelManager() {
if (typeof window.showNotification === "function") {
window.showNotification("Label manager coming soon", "info");
}
}
function toggleSelectAll(checkbox) {
var items = document.querySelectorAll('.mail-item input[type="checkbox"]');
items.forEach(function (item) {
item.checked = checkbox.checked;
if (checkbox.checked) {
selectedEmails.add(item.dataset.id);
} else {
selectedEmails.delete(item.dataset.id);
}
});
updateBulkActions();
}
function updateBulkActions() {
var bulkBar = document.getElementById("bulk-actions");
if (bulkBar) {
if (selectedEmails.size > 0) {
bulkBar.style.display = "flex";
var countEl = bulkBar.querySelector(".selected-count");
if (countEl) {
countEl.textContent = selectedEmails.size + " selected";
}
} else {
bulkBar.style.display = "none";
}
}
}
function refreshMailList() {
var folderEl = document.querySelector(
'[data-folder="' + currentFolder + '"]',
);
if (folderEl && typeof htmx !== "undefined") {
htmx.trigger(folderEl, "click");
}
}
function insertSignature() {
fetch("/api/email/signatures/default")
.then(function (r) {
return r.json();
})
.then(function (sig) {
if (sig.content_html) {
var body = document.getElementById("compose-body");
if (body) {
body.innerHTML += "<br><br>" + sig.content_html;
}
}
})
.catch(function (e) {
console.warn("Failed to load signature:", e);
});
}
function showTemplateSelector() {
openTemplates();
}
function attachFile() {
var input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.onchange = function (e) {
var files = e.target.files;
var container = document.getElementById("compose-attachments");
if (container) {
Array.from(files).forEach(function (file) {
var chip = document.createElement("div");
chip.className = "attachment-chip";
chip.innerHTML =
"<span>" +
escapeHtml(file.name) +
"</span>" +
'<button type="button" onclick="this.parentElement.remove()">×</button>';
container.appendChild(chip);
});
}
};
input.click();
}
function insertLink() {
var url = prompt("Enter URL:");
if (url) {
document.execCommand("createLink", false, url);
}
}
function insertImage() {
var url = prompt("Enter image URL:");
if (url) {
document.execCommand("insertImage", false, url);
}
}
function saveDraft() {
prepareSubmit();
var form = document.getElementById("compose-form");
if (form) {
var formData = new FormData(form);
fetch("/api/email/draft", {
method: "POST",
body: formData,
})
.then(function () {
if (typeof window.showNotification === "function") {
window.showNotification("Draft saved", "success");
}
})
.catch(function (e) {
console.warn("Failed to save draft:", e);
});
}
}
function createNewTemplate() {
if (typeof window.showNotification === "function") {
window.showNotification("Template editor coming soon", "info");
}
}
function createNewSignature() {
if (typeof window.showNotification === "function") {
window.showNotification("Signature editor coming soon", "info");
}
}
function createNewRule() {
if (typeof window.showNotification === "function") {
window.showNotification("Rule editor coming soon", "info");
}
}
function archiveSelected() {
if (typeof window.showNotification === "function") {
window.showNotification(
selectedEmails.size + " emails archived",
"success",
);
}
selectedEmails.clear();
updateBulkActions();
refreshMailList();
}
function markAsRead() {
if (typeof window.showNotification === "function") {
window.showNotification(
selectedEmails.size + " emails marked as read",
"success",
);
}
selectedEmails.clear();
updateBulkActions();
refreshMailList();
}
function addLabelToSelected() {
if (typeof window.showNotification === "function") {
window.showNotification("Label picker coming soon", "info");
}
}
function deleteSelected() {
if (confirm("Delete " + selectedEmails.size + " emails?")) {
if (typeof window.showNotification === "function") {
window.showNotification(
selectedEmails.size + " emails deleted",
"success",
);
}
selectedEmails.clear();
updateBulkActions();
refreshMailList();
}
}
function openAddAccount() {
var modal = document.getElementById("account-modal");
if (modal && modal.showModal) {
modal.showModal();
}
}
function closeAddAccount() {
var modal = document.getElementById("account-modal");
if (modal && modal.close) {
modal.close();
}
}
function saveAccount() {
var form = document.getElementById("account-form");
if (form && typeof htmx !== "undefined") {
htmx.trigger(form, "submit");
}
closeAddAccount();
if (typeof window.showNotification === "function") {
window.showNotification("Email account added", "success");
}
}
function escapeHtml(text) {
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function initFolderHandlers() {
document
.querySelectorAll(".nav-item[data-folder]")
.forEach(function (item) {
item.addEventListener("click", function () {
document.querySelectorAll(".nav-item").forEach(function (i) {
i.classList.remove("active");
});
this.classList.add("active");
currentFolder = this.dataset.folder;
});
});
}
function initMail() {
initFolderHandlers();
var inboxItem = document.querySelector('.nav-item[data-folder="inbox"]');
if (inboxItem && typeof htmx !== "undefined") {
htmx.trigger(inboxItem, "click");
}
}
window.openCompose = openCompose;
window.closeCompose = closeCompose;
window.minimizeCompose = minimizeCompose;
window.toggleCcBcc = toggleCcBcc;
window.toggleScheduleMenu = toggleScheduleMenu;
window.scheduleSend = scheduleSend;
window.openCustomSchedule = openCustomSchedule;
window.closeScheduleModal = closeScheduleModal;
window.confirmSchedule = confirmSchedule;
window.prepareSubmit = prepareSubmit;
window.formatText = formatText;
window.openTemplates = openTemplates;
window.closeTemplates = closeTemplates;
window.openSignatures = openSignatures;
window.closeSignatures = closeSignatures;
window.openRules = openRules;
window.closeRules = closeRules;
window.openAutoResponder = openAutoResponder;
window.closeAutoResponder = closeAutoResponder;
window.saveAutoResponder = saveAutoResponder;
window.openLabelManager = openLabelManager;
window.toggleSelectAll = toggleSelectAll;
window.updateBulkActions = updateBulkActions;
window.refreshMailList = refreshMailList;
window.insertSignature = insertSignature;
window.showTemplateSelector = showTemplateSelector;
window.attachFile = attachFile;
window.insertLink = insertLink;
window.insertImage = insertImage;
window.saveDraft = saveDraft;
window.createNewTemplate = createNewTemplate;
window.createNewSignature = createNewSignature;
window.createNewRule = createNewRule;
window.archiveSelected = archiveSelected;
window.markAsRead = markAsRead;
window.addLabelToSelected = addLabelToSelected;
window.deleteSelected = deleteSelected;
window.openAddAccount = openAddAccount;
window.closeAddAccount = closeAddAccount;
window.saveAccount = saveAccount;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initMail);
} else {
initMail();
}
document.body.addEventListener("htmx:afterSwap", function (evt) {
if (evt.detail.target && evt.detail.target.id === "main-content") {
if (document.querySelector(".mail-layout")) {
initMail();
}
}
});
});
})();

View file

@ -1374,349 +1374,7 @@
hx-swap="innerHTML"
></div>
<style>
.monitoring-container {
padding: 1.5rem;
max-width: 100%;
margin: 0 auto;
background: var(--bg-dark, #0f172a);
min-height: 100vh;
}
.monitoring-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color, #334155);
}
.monitoring-header h2 {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary, #f8fafc);
margin: 0;
}
.header-icon {
color: var(--primary-color, #3b82f6);
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.view-toggle {
background: var(--bg-surface, #1e293b);
border: 1px solid var(--border-color, #334155);
border-radius: 8px;
padding: 0.5rem;
cursor: pointer;
color: var(--text-secondary, #94a3b8);
transition: all 0.2s;
}
.view-toggle:hover {
border-color: var(--primary-color, #3b82f6);
color: var(--primary-color, #3b82f6);
}
.last-updated {
font-size: 0.875rem;
color: var(--text-secondary, #94a3b8);
}
/* Live Visualization */
.live-visualization {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow-x: auto;
}
.live-svg {
width: 100%;
height: auto;
max-height: calc(100vh - 120px);
min-width: 900px;
}
/* Animations */
@keyframes rotate-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse-ring {
0%,
100% {
r: 63;
opacity: 0.6;
}
50% {
r: 67;
opacity: 0.8;
}
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.4;
transform: scale(0.8);
}
}
@keyframes ai-pulse {
0%,
100% {
r: 4;
}
50% {
r: 6;
}
}
@keyframes antenna-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.rotate-slow {
animation: rotate-slow 30s linear infinite;
transform-origin: center;
}
.pulse-ring {
animation: pulse-ring 3s ease-in-out infinite;
}
.pulse-dot {
animation: pulse-dot 1.5s ease-in-out infinite;
}
.ai-pulse {
animation: ai-pulse 1.5s ease-in-out infinite;
}
.antenna-pulse {
animation: antenna-pulse 1.5s ease-in-out infinite;
}
/* Status dots */
.status-dot {
transition: fill 0.3s ease;
}
.status-dot.running {
fill: #10b981;
animation: pulse-dot 2s ease-in-out infinite;
}
.status-dot.warning {
fill: #f59e0b;
animation: pulse-dot 1s ease-in-out infinite;
}
.status-dot.stopped {
fill: #ef4444;
animation: none;
}
/* Data packets */
.data-packet {
filter: url(#glow);
}
/* Service nodes */
.service-node {
cursor: pointer;
transition: transform 0.2s ease;
}
.service-node:hover {
transform: scale(1.05);
}
/* Resource bars */
.resource-fill {
transition: width 0.5s ease-out;
}
/* Grid View Styles */
.monitoring-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.monitor-panel {
background: var(--card-bg, #1e293b);
border: 1px solid var(--border-color, #334155);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 1rem;
color: var(--text-primary, #f8fafc);
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color, #334155);
}
.panel-icon {
color: var(--primary-color, #3b82f6);
}
.tree-view {
font-family:
system-ui,
-apple-system,
sans-serif;
font-size: 0.875rem;
}
.tree-node {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
}
.tree-branch {
width: 1.25rem;
height: 1.25rem;
position: relative;
flex-shrink: 0;
}
.tree-branch::before {
content: "";
position: absolute;
left: 0.5rem;
top: 0;
width: 1px;
height: 100%;
background: var(--primary-color, #3b82f6);
opacity: 0.4;
}
.tree-branch::after {
content: "";
position: absolute;
left: 0.5rem;
top: 50%;
width: 0.5rem;
height: 1px;
background: var(--primary-color, #3b82f6);
opacity: 0.6;
}
.tree-label {
color: var(--text-secondary, #94a3b8);
flex: 1;
}
.tree-value {
font-weight: 600;
color: var(--text-primary, #f8fafc);
}
@media (max-width: 768px) {
.monitoring-grid {
grid-template-columns: 1fr;
}
.monitoring-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.live-svg {
min-width: 600px;
}
}
</style>
<script>
// View toggle functionality
document
.getElementById("view-toggle")
.addEventListener("click", function () {
const liveView = document.getElementById("live-view");
const gridView = document.getElementById("grid-view");
if (liveView.style.display === "none") {
liveView.style.display = "flex";
gridView.style.display = "none";
} else {
liveView.style.display = "none";
gridView.style.display = "grid";
}
});
// Update service status dots
document.body.addEventListener("htmx:afterSwap", function (event) {
if (event.detail.target.id === "service-status-container") {
try {
const data = JSON.parse(event.detail.target.textContent);
Object.entries(data).forEach(([service, status]) => {
const dot = document.querySelector(
`[data-status="${service}"]`,
);
if (dot) {
dot.classList.remove(
"running",
"warning",
"stopped",
);
dot.classList.add(status);
}
});
} catch (e) {
// Silent fail for non-JSON responses
}
}
});
// Keyboard shortcut: R to refresh, V to toggle view
document.addEventListener("keydown", function (e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return;
if (e.key === "r" && !e.ctrlKey && !e.metaKey) {
htmx.trigger(document.body, "htmx:load");
}
if (e.key === "v" && !e.ctrlKey && !e.metaKey) {
document.getElementById("view-toggle").click();
}
});
</script>
<link rel="stylesheet" href="monitoring.css" />
<script src="monitoring.js"></script>
</div>

View file

@ -1,75 +1,162 @@
/* Monitoring module - shared/base JavaScript */
function setActiveNav(element) {
document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
item.classList.remove("active");
});
element.classList.add("active");
(function () {
"use strict";
// Update page title
const title = element.querySelector(
"span:not(.alert-badge):not(.health-indicator)",
).textContent;
document.getElementById("page-title").textContent = title;
}
function setActiveNav(element) {
document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
item.classList.remove("active");
});
element.classList.add("active");
function updateTimeRange(range) {
// Store selected time range
localStorage.setItem("monitoring-time-range", range);
// Update page title
const title = element.querySelector(
"span:not(.alert-badge):not(.health-indicator)",
).textContent;
document.getElementById("page-title").textContent = title;
}
// Trigger refresh of current view
htmx.trigger("#monitoring-content", "refresh");
}
function updateTimeRange(range) {
// Store selected time range
localStorage.setItem("monitoring-time-range", range);
function refreshMonitoring() {
htmx.trigger("#monitoring-content", "refresh");
// Visual feedback
const btn = event.currentTarget;
btn.classList.add("active");
setTimeout(() => btn.classList.remove("active"), 500);
}
// Guard against duplicate declarations on HTMX reload
if (typeof window.monitoringModuleInitialized === "undefined") {
window.monitoringModuleInitialized = true;
var autoRefresh = true;
}
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
const btn = document.getElementById("auto-refresh-btn");
btn.classList.toggle("active", autoRefresh);
if (autoRefresh) {
// Re-enable polling by refreshing the page content
// Trigger refresh of current view
htmx.trigger("#monitoring-content", "refresh");
}
}
function exportData() {
const timeRange = document.getElementById("time-range").value;
window.open(`/api/monitoring/export?range=${timeRange}`, "_blank");
}
function refreshMonitoring() {
htmx.trigger("#monitoring-content", "refresh");
// Initialize
document.addEventListener("DOMContentLoaded", function () {
// Restore time range preference
const savedRange = localStorage.getItem("monitoring-time-range");
if (savedRange) {
const timeRangeEl = document.getElementById("time-range");
if (timeRangeEl) timeRangeEl.value = savedRange;
// Visual feedback
const btn = event.currentTarget;
btn.classList.add("active");
setTimeout(() => btn.classList.remove("active"), 500);
}
// Set auto-refresh button state
const autoRefreshBtn = document.getElementById("auto-refresh-btn");
if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh);
});
// Handle HTMX events for loading states
document.body.addEventListener("htmx:beforeRequest", function (evt) {
if (evt.target.id === "monitoring-content") {
evt.target.innerHTML =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
// Guard against duplicate declarations on HTMX reload
if (typeof window.monitoringModuleInitialized === "undefined") {
window.monitoringModuleInitialized = true;
var autoRefresh = true;
}
});
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
const btn = document.getElementById("auto-refresh-btn");
btn.classList.toggle("active", autoRefresh);
if (autoRefresh) {
// Re-enable polling by refreshing the page content
htmx.trigger("#monitoring-content", "refresh");
}
}
function exportData() {
const timeRange = document.getElementById("time-range").value;
window.open(`/api/monitoring/export?range=${timeRange}`, "_blank");
}
// Initialize
document.addEventListener("DOMContentLoaded", function () {
// Restore time range preference
const savedRange = localStorage.getItem("monitoring-time-range");
if (savedRange) {
const timeRangeEl = document.getElementById("time-range");
if (timeRangeEl) timeRangeEl.value = savedRange;
}
// Set auto-refresh button state
const autoRefreshBtn = document.getElementById("auto-refresh-btn");
if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh);
});
// Handle HTMX events for loading states
document.body.addEventListener("htmx:beforeRequest", function (evt) {
if (evt.target.id === "monitoring-content") {
evt.target.innerHTML =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
}
});
function initViewToggle() {
var toggleBtn = document.getElementById("view-toggle");
if (toggleBtn) {
toggleBtn.addEventListener("click", function () {
var liveView = document.getElementById("live-view");
var gridView = document.getElementById("grid-view");
if (liveView && gridView) {
if (liveView.style.display === "none") {
liveView.style.display = "flex";
gridView.style.display = "none";
} else {
liveView.style.display = "none";
gridView.style.display = "grid";
}
}
});
}
}
function updateServiceStatusDots(event) {
if (event.detail.target.id === "service-status-container") {
try {
var data = JSON.parse(event.detail.target.textContent);
Object.entries(data).forEach(function (entry) {
var service = entry[0];
var status = entry[1];
var dot = document.querySelector('[data-status="' + service + '"]');
if (dot) {
dot.classList.remove("running", "warning", "stopped");
dot.classList.add(status);
}
});
} catch (e) {
// Silent fail for non-JSON responses
}
}
}
function handleKeyboardShortcuts(e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") {
return;
}
if (e.key === "r" && !e.ctrlKey && !e.metaKey) {
if (typeof htmx !== "undefined") {
htmx.trigger(document.body, "htmx:load");
}
}
if (e.key === "v" && !e.ctrlKey && !e.metaKey) {
var toggleBtn = document.getElementById("view-toggle");
if (toggleBtn) {
toggleBtn.click();
}
}
}
function initMonitoring() {
initViewToggle();
document.body.addEventListener("htmx:afterSwap", updateServiceStatusDots);
document.addEventListener("keydown", handleKeyboardShortcuts);
}
window.setActiveNav = setActiveNav;
window.updateTimeRange = updateTimeRange;
window.refreshMonitoring = refreshMonitoring;
window.toggleAutoRefresh = toggleAutoRefresh;
window.exportData = exportData;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initMonitoring);
} else {
initMonitoring();
}
document.body.addEventListener("htmx:afterSwap", function (evt) {
if (evt.detail.target && evt.detail.target.id === "main-content") {
if (document.querySelector(".monitoring-container")) {
initMonitoring();
}
}
});
})();

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@
autoSaveTimer: null,
isPresenting: false,
theme: null,
driveSource: null,
};
const elements = {
@ -58,7 +59,97 @@
bindEvents();
loadFromUrlParams();
connectWebSocket();
createNewPresentation();
}
async function loadFromUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const hash = window.location.hash;
let presentationId = urlParams.get("id");
let bucket = urlParams.get("bucket");
let path = urlParams.get("path");
if (hash) {
const hashQueryIndex = hash.indexOf("?");
if (hashQueryIndex !== -1) {
const hashParams = new URLSearchParams(
hash.substring(hashQueryIndex + 1),
);
presentationId = presentationId || hashParams.get("id");
bucket = bucket || hashParams.get("bucket");
path = path || hashParams.get("path");
}
}
if (bucket && path) {
await loadFromDrive(bucket, path);
} else if (presentationId) {
try {
const response = await fetch(`/api/slides/${presentationId}`);
if (response.ok) {
const data = await response.json();
state.presentationId = presentationId;
state.presentationName = data.name || "Untitled Presentation";
state.slides = data.slides || [];
if (elements.presentationName) {
elements.presentationName.value = state.presentationName;
}
renderThumbnails();
renderCurrentSlide();
updateSlideCounter();
}
} catch (e) {
console.error("Load failed:", e);
createNewPresentation();
}
} else {
createNewPresentation();
}
}
async function loadFromDrive(bucket, path) {
const fileName = path.split("/").pop() || "presentation";
state.driveSource = { bucket, path };
state.presentationName = fileName;
if (elements.presentationName) {
elements.presentationName.value = fileName;
}
try {
const response = await fetch("/api/files/read", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bucket, path }),
});
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status}`);
}
const data = await response.json();
const content = data.content || "";
createNewPresentation();
if (state.slides.length > 0 && state.slides[0].elements) {
const titleElement = state.slides[0].elements.find(
(el) => el.element_type === "text" && el.style?.fontSize >= 32,
);
if (titleElement) {
titleElement.content = fileName.replace(/\.[^/.]+$/, "");
}
}
renderThumbnails();
renderCurrentSlide();
updateSlideCounter();
state.isDirty = false;
} catch (err) {
console.error("Failed to load file from drive:", err);
createNewPresentation();
}
}
function cacheElements() {

View file

@ -310,317 +310,5 @@
</div>
</div>
<script>
// Initialize on load
document.addEventListener("DOMContentLoaded", function () {
if (typeof initTasksApp === "function") {
initTasksApp();
}
// Load stats
loadTaskStats();
// Handle create task response - auto-select new task and show progress
document.body.addEventListener("htmx:afterRequest", function (evt) {
// Check if this is the create task response
if (
evt.detail.pathInfo &&
evt.detail.pathInfo.requestPath === "/api/autotask/create"
) {
const xhr = evt.detail.xhr;
const intentResult = document.getElementById("intent-result");
if (xhr && xhr.status === 202) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success && response.task_id) {
console.log(
"[TASK] Created task:",
response.task_id,
);
// Show success feedback
intentResult.innerHTML = `<span class="intent-success">✓ Task created - running...</span>`;
intentResult.style.display = "block";
// Clear the input
document.getElementById(
"quick-intent-input",
).value = "";
// Select the task and let tasks.js handle polling
selectTask(response.task_id);
// Hide success message after task is selected
setTimeout(function () {
intentResult.style.display = "none";
}, 2000);
} else {
intentResult.innerHTML = `<span class="intent-error">✗ ${response.message || "Failed to create task"}</span>`;
intentResult.style.display = "block";
}
} catch (e) {
console.warn("Failed to parse create response:", e);
intentResult.innerHTML = `<span class="intent-error">✗ Failed to parse response</span>`;
intentResult.style.display = "block";
}
} else if (xhr && xhr.status >= 400) {
try {
const response = JSON.parse(xhr.responseText);
intentResult.innerHTML = `<span class="intent-error">✗ ${response.error || response.message || "Error creating task"}</span>`;
} catch (e) {
intentResult.innerHTML = `<span class="intent-error">✗ Error: ${xhr.status}</span>`;
}
intentResult.style.display = "block";
}
}
});
});
// Load task statistics
// Auth headers automatically added by security-bootstrap.js
function loadTaskStats() {
fetch("/api/tasks/stats/json")
.then((r) => r.json())
.then((stats) => {
if (stats.complete !== undefined)
document.getElementById("count-complete").textContent =
stats.complete;
if (stats.active !== undefined)
document.getElementById("count-active").textContent =
stats.active;
if (stats.awaiting !== undefined)
document.getElementById("count-awaiting").textContent =
stats.awaiting;
if (stats.paused !== undefined)
document.getElementById("count-paused").textContent =
stats.paused;
if (stats.blocked !== undefined)
document.getElementById("count-blocked").textContent =
stats.blocked;
if (stats.time_saved !== undefined)
document.getElementById("time-saved-value").textContent =
stats.time_saved;
})
.catch((e) => console.warn("Failed to load stats:", e));
}
// Filter pill click handler
document.querySelectorAll(".filter-pill").forEach((pill) => {
pill.addEventListener("click", function () {
document
.querySelectorAll(".filter-pill")
.forEach((p) => p.classList.remove("active"));
this.classList.add("active");
});
});
// Modal functions
function showNewIntentModal() {
document.getElementById("new-intent-modal").style.display = "flex";
}
function closeNewIntentModal() {
document.getElementById("new-intent-modal").style.display = "none";
}
function showDecisionModal(decision) {
if (decision) {
document.getElementById("decision-question").innerHTML = `
<h4>${decision.title || "Decision Required"}</h4>
<p>${decision.description || ""}</p>
`;
}
document.getElementById("decision-modal").style.display = "flex";
}
function closeDecisionModal() {
document.getElementById("decision-modal").style.display = "none";
}
function submitNewIntent() {
const form = document.getElementById("new-intent-form");
const intent = form.querySelector('[name="intent"]').value;
if (intent.trim()) {
document.getElementById("quick-intent-input").value = intent;
htmx.trigger(document.getElementById("quick-intent-btn"), "click");
closeNewIntentModal();
}
}
function submitDecision() {
// Implementation for submitting decision
closeDecisionModal();
htmx.trigger(document.body, "taskCreated");
}
function skipDecision() {
closeDecisionModal();
}
// Task selection
function selectTask(taskId) {
// Update selection UI
document.querySelectorAll(".task-card").forEach((card) => {
card.classList.remove("selected");
});
const selectedCard = document.querySelector(
`[data-task-id="${taskId}"]`,
);
if (selectedCard) {
selectedCard.classList.add("selected");
}
// Show detail panel
document.getElementById("detail-empty").style.display = "none";
const detailContent = document.getElementById("task-detail-content");
detailContent.style.display = "block";
// Load task details
htmx.ajax("GET", `/api/tasks/${taskId}`, {
target: "#task-detail-content",
swap: "innerHTML",
});
}
function deselectTask() {
document.querySelectorAll(".task-card").forEach((card) => {
card.classList.remove("selected");
});
document.getElementById("detail-empty").style.display = "flex";
document.getElementById("task-detail-content").style.display = "none";
}
// Floating Progress Panel Functions
function showFloatingProgress(taskName) {
let panel = document.getElementById("floating-progress");
document.getElementById("floating-task-name").textContent =
taskName || "Processing...";
document.getElementById("floating-progress-fill").style.width = "0%";
document.getElementById("floating-progress-step").textContent =
"Starting...";
document.getElementById("floating-progress-percent").textContent = "0%";
document.getElementById("floating-progress-log").innerHTML = "";
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.remove("completed", "error");
panel.style.display = "block";
panel.classList.remove("minimized");
}
function updateFloatingProgress(step, message, current, total, details) {
const panel = document.getElementById("floating-progress");
if (panel.style.display === "none") {
showFloatingProgress(message);
}
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
document.getElementById("floating-progress-fill").style.width =
percent + "%";
document.getElementById("floating-progress-step").textContent = message;
document.getElementById("floating-progress-percent").textContent =
percent + "%";
// Add log entry
if (step) {
const log = document.getElementById("floating-progress-log");
const entry = document.createElement("div");
entry.className = "log-entry";
const time = new Date().toLocaleTimeString();
entry.innerHTML = `<span class="log-time">${time}</span> <span class="log-step">[${step}]</span> ${message}`;
if (details) {
entry.innerHTML += `<br><span class="log-details">→ ${details}</span>`;
}
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
}
function completeFloatingProgress(message) {
document.getElementById("floating-progress-fill").style.width = "100%";
document.getElementById("floating-progress-step").textContent =
message || "Completed!";
document.getElementById("floating-progress-percent").textContent =
"100%";
const panel = document.getElementById("floating-progress");
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.add("completed");
// Refresh task list
htmx.trigger(document.body, "taskCreated");
loadTaskStats();
// Auto-hide after 5 seconds
setTimeout(closeFloatingProgress, 5000);
}
function errorFloatingProgress(errorMessage) {
document.getElementById("floating-progress-step").textContent =
"Error: " + errorMessage;
const panel = document.getElementById("floating-progress");
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.add("error");
}
function minimizeFloatingProgress() {
document
.getElementById("floating-progress")
.classList.toggle("minimized");
}
function closeFloatingProgress() {
const panel = document.getElementById("floating-progress");
panel.style.display = "none";
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.remove("completed", "error");
}
// Splitter drag functionality
(function initSplitter() {
const splitter = document.getElementById("tasks-splitter");
const main = document.querySelector(".tasks-main");
const leftPanel = document.querySelector(".tasks-list-panel");
if (!splitter || !main || !leftPanel) return;
let isDragging = false;
let startX, startWidth;
splitter.addEventListener("mousedown", function (e) {
isDragging = true;
startX = e.clientX;
startWidth = leftPanel.offsetWidth;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
e.preventDefault();
});
document.addEventListener("mousemove", function (e) {
if (!isDragging) return;
const diff = e.clientX - startX;
const newWidth = Math.max(200, Math.min(600, startWidth + diff));
leftPanel.style.flex = `0 0 ${newWidth}px`;
leftPanel.style.width = `${newWidth}px`;
});
document.addEventListener("mouseup", function () {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
});
})();
// Listen for HTMX events to refresh stats
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === "task-list") {
loadTaskStats();
// Show empty state if no tasks
const taskList = document.getElementById("task-list");
const emptyState = document.getElementById("empty-state");
if (taskList && emptyState) {
const hasTasks = taskList.querySelector(".task-card");
emptyState.style.display = hasTasks ? "none" : "flex";
}
}
});
</script>
<link rel="stylesheet" href="tasks.css" />
<script src="tasks.js"></script>

View file

@ -3051,3 +3051,240 @@ function updateFilterCounts() {
// Call updateFilterCounts on load
document.addEventListener("DOMContentLoaded", updateFilterCounts);
document.body.addEventListener("taskCreated", updateFilterCounts);
// =============================================================================
// MODAL FUNCTIONS
// =============================================================================
function showNewIntentModal() {
var modal = document.getElementById("new-intent-modal");
if (modal) {
modal.style.display = "flex";
}
}
function closeNewIntentModal() {
var modal = document.getElementById("new-intent-modal");
if (modal) {
modal.style.display = "none";
}
}
function showDecisionModal(decision) {
var questionEl = document.getElementById("decision-question");
if (decision && questionEl) {
var title = decision.title || "Decision Required";
var description = decision.description || "";
questionEl.innerHTML =
"<h4>" +
escapeHtml(title) +
"</h4>" +
"<p>" +
escapeHtml(description) +
"</p>";
}
var modal = document.getElementById("decision-modal");
if (modal) {
modal.style.display = "flex";
}
}
function closeDecisionModal() {
var modal = document.getElementById("decision-modal");
if (modal) {
modal.style.display = "none";
}
}
function submitNewIntent() {
var form = document.getElementById("new-intent-form");
if (!form) return;
var intentInput = form.querySelector('[name="intent"]');
if (!intentInput) return;
var intent = intentInput.value;
if (intent && intent.trim()) {
var quickInput = document.getElementById("quick-intent-input");
if (quickInput) {
quickInput.value = intent;
}
var quickBtn = document.getElementById("quick-intent-btn");
if (quickBtn && typeof htmx !== "undefined") {
htmx.trigger(quickBtn, "click");
}
closeNewIntentModal();
}
}
function skipDecision() {
closeDecisionModal();
}
// =============================================================================
// TASK STATS LOADING
// =============================================================================
function loadTaskStats() {
fetch("/api/tasks/stats/json")
.then(function (response) {
if (!response.ok) {
throw new Error("Failed to fetch stats");
}
return response.json();
})
.then(function (stats) {
var mappings = [
{ key: "complete", id: "count-complete" },
{ key: "completed", id: "count-complete" },
{ key: "active", id: "count-active" },
{ key: "awaiting", id: "count-awaiting" },
{ key: "paused", id: "count-paused" },
{ key: "blocked", id: "count-blocked" },
{ key: "time_saved", id: "time-saved-value" },
{ key: "total", id: "count-all" },
];
mappings.forEach(function (mapping) {
if (stats[mapping.key] !== undefined) {
var el = document.getElementById(mapping.id);
if (el) {
el.textContent = stats[mapping.key];
}
}
});
})
.catch(function (e) {
console.warn("Failed to load stats:", e);
});
}
// =============================================================================
// SPLITTER DRAG FUNCTIONALITY
// =============================================================================
(function initSplitter() {
var splitter = document.getElementById("tasks-splitter");
var main = document.querySelector(".tasks-main");
var leftPanel = document.querySelector(".tasks-list-panel");
if (!splitter || !main || !leftPanel) return;
var isDragging = false;
var startX = 0;
var startWidth = 0;
splitter.addEventListener("mousedown", function (e) {
isDragging = true;
startX = e.clientX;
startWidth = leftPanel.offsetWidth;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
e.preventDefault();
});
document.addEventListener("mousemove", function (e) {
if (!isDragging) return;
var diff = e.clientX - startX;
var newWidth = Math.max(200, Math.min(600, startWidth + diff));
leftPanel.style.flex = "0 0 " + newWidth + "px";
leftPanel.style.width = newWidth + "px";
});
document.addEventListener("mouseup", function () {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
});
})();
// =============================================================================
// HTMX TASK CREATION HANDLER
// =============================================================================
document.body.addEventListener("htmx:afterRequest", function (evt) {
if (!evt.detail.pathInfo) return;
if (evt.detail.pathInfo.requestPath !== "/api/autotask/create") return;
var xhr = evt.detail.xhr;
var intentResult = document.getElementById("intent-result");
if (!intentResult) return;
if (xhr && xhr.status === 202) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success && response.task_id) {
console.log("[TASK] Created task:", response.task_id);
intentResult.innerHTML =
'<span class="intent-success">✓ Task created - running...</span>';
intentResult.style.display = "block";
var quickInput = document.getElementById("quick-intent-input");
if (quickInput) {
quickInput.value = "";
}
selectTask(response.task_id);
setTimeout(function () {
intentResult.style.display = "none";
}, 2000);
} else {
var msg = response.message || "Failed to create task";
intentResult.innerHTML =
'<span class="intent-error">✗ ' + escapeHtml(msg) + "</span>";
intentResult.style.display = "block";
}
} catch (e) {
console.warn("Failed to parse create response:", e);
intentResult.innerHTML =
'<span class="intent-error">✗ Failed to parse response</span>';
intentResult.style.display = "block";
}
} else if (xhr && xhr.status >= 400) {
try {
var errorResponse = JSON.parse(xhr.responseText);
var errorMsg =
errorResponse.error || errorResponse.message || "Error creating task";
intentResult.innerHTML =
'<span class="intent-error">✗ ' + escapeHtml(errorMsg) + "</span>";
} catch (e) {
intentResult.innerHTML =
'<span class="intent-error">✗ Error: ' + xhr.status + "</span>";
}
intentResult.style.display = "block";
}
});
// =============================================================================
// FILTER PILL CLICK HANDLER
// =============================================================================
document.querySelectorAll(".filter-pill").forEach(function (pill) {
pill.addEventListener("click", function () {
document.querySelectorAll(".filter-pill").forEach(function (p) {
p.classList.remove("active");
});
this.classList.add("active");
});
});
// =============================================================================
// HTMX TASK LIST REFRESH HANDLER
// =============================================================================
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target && e.detail.target.id === "task-list") {
loadTaskStats();
var taskList = document.getElementById("task-list");
var emptyState = document.getElementById("empty-state");
if (taskList && emptyState) {
var hasTasks = taskList.querySelector(".task-card");
emptyState.style.display = hasTasks ? "none" : "flex";
}
}
});

View file

@ -13,6 +13,7 @@ class VideoEditor {
this.pixelsPerMs = 0.1;
this.undoStack = [];
this.redoStack = [];
this.driveSource = null;
this.init();
}
@ -20,9 +21,96 @@ class VideoEditor {
async init() {
this.bindEvents();
this.updateTimeRuler();
await this.loadFromUrlParams();
await this.loadProjects();
}
async loadFromUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const hash = window.location.hash;
let bucket = urlParams.get("bucket");
let path = urlParams.get("path");
if (hash) {
const hashQueryIndex = hash.indexOf("?");
if (hashQueryIndex !== -1) {
const hashParams = new URLSearchParams(
hash.substring(hashQueryIndex + 1),
);
bucket = bucket || hashParams.get("bucket");
path = path || hashParams.get("path");
}
}
if (bucket && path) {
await this.loadFromDrive(bucket, path);
}
}
async loadFromDrive(bucket, path) {
const fileName = path.split("/").pop() || "media";
const ext = fileName.split(".").pop().toLowerCase();
this.driveSource = { bucket, path };
try {
const response = await fetch("/api/files/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bucket, path }),
});
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const isImage = [
"png",
"jpg",
"jpeg",
"gif",
"webp",
"svg",
"bmp",
"ico",
"tiff",
"tif",
"heic",
"heif",
].includes(ext);
const isVideo = [
"mp4",
"webm",
"mov",
"avi",
"mkv",
"wmv",
"flv",
"m4v",
].includes(ext);
const previewEl = document.getElementById("preview-video");
if (previewEl) {
if (isImage) {
previewEl.outerHTML = `<img id="preview-video" src="${url}" style="max-width:100%;max-height:100%;object-fit:contain;" alt="${fileName}">`;
} else if (isVideo) {
previewEl.src = url;
previewEl.load();
}
}
const projectName = document.getElementById("current-project-name");
if (projectName) {
projectName.textContent = fileName;
}
} catch (err) {
console.error("Failed to load file from drive:", err);
}
}
bindEvents() {
document
.getElementById("new-project-btn")