@zeyos/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/agents/README.md +66 -0
- package/agents/shared/business-app-benchmarks.md +111 -0
- package/agents/shared/zeyos-entity-map.md +142 -0
- package/agents/shared/zeyos-entity-reference.md +570 -0
- package/agents/shared/zeyos-query-patterns.md +89 -0
- package/agents/zeyos-account-intelligence/SKILL.md +34 -0
- package/agents/zeyos-account-intelligence/agents/openai.yaml +4 -0
- package/agents/zeyos-account-intelligence/references/workflows.md +84 -0
- package/agents/zeyos-billing-insights/SKILL.md +41 -0
- package/agents/zeyos-billing-insights/agents/openai.yaml +4 -0
- package/agents/zeyos-billing-insights/references/workflows.md +106 -0
- package/agents/zeyos-campaign-and-outreach/SKILL.md +44 -0
- package/agents/zeyos-campaign-and-outreach/agents/openai.yaml +4 -0
- package/agents/zeyos-campaign-and-outreach/references/workflows.md +100 -0
- package/agents/zeyos-collaboration-and-activity/SKILL.md +37 -0
- package/agents/zeyos-collaboration-and-activity/agents/openai.yaml +4 -0
- package/agents/zeyos-collaboration-and-activity/references/workflows.md +104 -0
- package/agents/zeyos-collections-and-dunning/SKILL.md +46 -0
- package/agents/zeyos-collections-and-dunning/agents/openai.yaml +4 -0
- package/agents/zeyos-collections-and-dunning/references/workflows.md +132 -0
- package/agents/zeyos-commerce-and-inventory/SKILL.md +38 -0
- package/agents/zeyos-commerce-and-inventory/agents/openai.yaml +4 -0
- package/agents/zeyos-commerce-and-inventory/references/workflows.md +101 -0
- package/agents/zeyos-mail-operations/SKILL.md +35 -0
- package/agents/zeyos-mail-operations/agents/openai.yaml +4 -0
- package/agents/zeyos-mail-operations/references/workflows.md +110 -0
- package/agents/zeyos-notes-and-sops/SKILL.md +31 -0
- package/agents/zeyos-notes-and-sops/agents/openai.yaml +4 -0
- package/agents/zeyos-notes-and-sops/references/workflows.md +85 -0
- package/agents/zeyos-platform-and-schema/SKILL.md +37 -0
- package/agents/zeyos-platform-and-schema/agents/openai.yaml +4 -0
- package/agents/zeyos-platform-and-schema/references/workflows.md +97 -0
- package/agents/zeyos-work-management/SKILL.md +45 -0
- package/agents/zeyos-work-management/agents/openai.yaml +4 -0
- package/agents/zeyos-work-management/references/workflows.md +148 -0
- package/docs/01-api-reference/01-data-retrieval.md +601 -0
- package/docs/01-api-reference/02-authentication.md +288 -0
- package/docs/01-api-reference/03-resources.md +270 -0
- package/docs/01-api-reference/04-schema.md +539 -0
- package/docs/01-api-reference/_category_.json +9 -0
- package/docs/02-javascript-client/01-getting-started.md +146 -0
- package/docs/02-javascript-client/02-authentication.md +287 -0
- package/docs/02-javascript-client/03-making-requests.md +572 -0
- package/docs/02-javascript-client/04-practical-guide.md +348 -0
- package/docs/02-javascript-client/_category_.json +9 -0
- package/docs/03-cli/01-getting-started.md +219 -0
- package/docs/03-cli/02-commands.md +407 -0
- package/docs/03-cli/03-configuration.md +220 -0
- package/docs/03-cli/_category_.json +9 -0
- package/docs/04-agent-workflows/00-coding-agents.md +35 -0
- package/docs/04-agent-workflows/01-agent-quickstart.md +147 -0
- package/docs/04-agent-workflows/02-agent-recipes.md +109 -0
- package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +65 -0
- package/docs/04-agent-workflows/_category_.json +9 -0
- package/docs/04-sample-apps/01-kanban.md +89 -0
- package/docs/04-sample-apps/02-crm.md +81 -0
- package/docs/04-sample-apps/03-dashboard.md +80 -0
- package/docs/04-sample-apps/_category_.json +9 -0
- package/docs/05-tutorials/00-application-developers.md +43 -0
- package/docs/05-tutorials/01-integration-architecture.md +60 -0
- package/docs/05-tutorials/02-build-your-own-zeyos-frontend.md +517 -0
- package/docs/05-tutorials/03-server-side-integrations.md +185 -0
- package/docs/05-tutorials/_category_.json +9 -0
- package/docs/intro.md +197 -0
- package/openapi/api.json +24308 -0
- package/openapi/auth.json +415 -0
- package/openapi/dbref.json +56223 -0
- package/openapi/oauth2.json +781 -0
- package/openapi/sdk.json +949 -0
- package/openapi/views.txt +642 -0
- package/package.json +49 -0
- package/samples/crm/README.md +28 -0
- package/samples/crm/index.html +327 -0
- package/samples/crm/js/api.js +208 -0
- package/samples/crm/js/auth.js +61 -0
- package/samples/crm/js/main.js +545 -0
- package/samples/crm/js/state.js +90 -0
- package/samples/crm/js/ui.js +51 -0
- package/samples/dashboard/README.md +28 -0
- package/samples/dashboard/index.html +280 -0
- package/samples/dashboard/js/api.js +197 -0
- package/samples/dashboard/js/auth.js +59 -0
- package/samples/dashboard/js/main.js +382 -0
- package/samples/dashboard/js/state.js +81 -0
- package/samples/dashboard/js/ui.js +48 -0
- package/samples/kanban/README.md +28 -0
- package/samples/kanban/index.html +263 -0
- package/samples/kanban/js/api.js +152 -0
- package/samples/kanban/js/auth.js +59 -0
- package/samples/kanban/js/constants.js +40 -0
- package/samples/kanban/js/kanban.js +246 -0
- package/samples/kanban/js/main.js +362 -0
- package/samples/kanban/js/modals.js +474 -0
- package/samples/kanban/js/settings.js +82 -0
- package/samples/kanban/js/state.js +118 -0
- package/samples/kanban/js/ui.js +49 -0
- package/scripts/generate-client.mjs +344 -0
- package/src/generated/operations.js +9772 -0
- package/src/generated/schema.js +8982 -0
- package/src/index.js +85 -0
- package/src/runtime/client.js +1208 -0
- package/src/runtime/error.js +29 -0
- package/src/runtime/http.js +174 -0
- package/src/runtime/request-shape.js +35 -0
- package/src/runtime/schema.js +206 -0
- package/src/runtime/suggest.js +74 -0
- package/src/runtime/token-store.js +105 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kanban board — column rendering + HTML5 drag-and-drop.
|
|
3
|
+
* Dispatches custom events for ticket interactions; all API calls are handled
|
|
4
|
+
* in main.js to keep this module free of async side-effects.
|
|
5
|
+
*/
|
|
6
|
+
import { STATUS_MAP, PRIORITY_MAP } from './constants.js';
|
|
7
|
+
import { runtime } from './state.js';
|
|
8
|
+
|
|
9
|
+
// ── Custom Events ──────────────────────────────────────────────────────────
|
|
10
|
+
// 'ticket:open' — detail({ ticketId })
|
|
11
|
+
// 'ticket:move' — detail({ ticketId, toStatus })
|
|
12
|
+
// 'ticket:create' — detail({ status }) (quick-create in a column)
|
|
13
|
+
|
|
14
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Render (or re-render) the entire board into #kanban-board. */
|
|
17
|
+
export function renderBoard() {
|
|
18
|
+
const board = document.getElementById('kanban-board');
|
|
19
|
+
if (!board) return;
|
|
20
|
+
|
|
21
|
+
const { settings, tickets } = runtime;
|
|
22
|
+
|
|
23
|
+
if (settings.columns.length === 0) {
|
|
24
|
+
board.innerHTML = '';
|
|
25
|
+
document.getElementById('no-columns-banner')?.classList.remove('hidden');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
document.getElementById('no-columns-banner')?.classList.add('hidden');
|
|
30
|
+
board.innerHTML = '';
|
|
31
|
+
|
|
32
|
+
for (const statusValue of settings.columns) {
|
|
33
|
+
const status = STATUS_MAP[statusValue];
|
|
34
|
+
if (!status) continue;
|
|
35
|
+
const colTickets = tickets.filter(t => t.status === statusValue);
|
|
36
|
+
board.appendChild(_buildColumn(status, colTickets));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Update a single column in-place (faster than full re-render). */
|
|
41
|
+
export function updateColumn(statusValue) {
|
|
42
|
+
const col = document.querySelector(`[data-column="${statusValue}"]`);
|
|
43
|
+
if (!col) { renderBoard(); return; }
|
|
44
|
+
|
|
45
|
+
const status = STATUS_MAP[statusValue];
|
|
46
|
+
const colTickets = runtime.tickets.filter(t => t.status === statusValue);
|
|
47
|
+
|
|
48
|
+
const badge = col.querySelector('.col-badge');
|
|
49
|
+
if (badge) badge.textContent = colTickets.length;
|
|
50
|
+
|
|
51
|
+
const list = col.querySelector('.col-cards');
|
|
52
|
+
if (list) {
|
|
53
|
+
list.innerHTML = '';
|
|
54
|
+
for (const ticket of colTickets) list.appendChild(_buildCard(ticket));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Column Builder ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function _buildColumn(status, tickets) {
|
|
61
|
+
const col = document.createElement('div');
|
|
62
|
+
col.className = 'kanban-col flex flex-col rounded-xl shadow-sm overflow-hidden';
|
|
63
|
+
col.style.cssText = 'min-width:280px; width:280px; max-height:100%;';
|
|
64
|
+
col.dataset.column = status.value;
|
|
65
|
+
|
|
66
|
+
const header = document.createElement('div');
|
|
67
|
+
header.className = 'col-header flex items-center justify-between px-3 py-2.5 flex-shrink-0';
|
|
68
|
+
header.style.cssText = `background:${status.headerBg}; border-bottom:2px solid ${status.cardBorder};`;
|
|
69
|
+
header.innerHTML = `
|
|
70
|
+
<div class="flex items-center gap-2">
|
|
71
|
+
<span class="font-semibold text-sm" style="color:${status.headerText}">${status.label}</span>
|
|
72
|
+
<span class="col-badge text-xs font-medium px-1.5 py-0.5 rounded-full bg-white/60" style="color:${status.headerText}">${tickets.length}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<button class="col-add-btn text-lg leading-none opacity-50 hover:opacity-100 transition-opacity" style="color:${status.headerText}" title="Add ticket" data-status="${status.value}">+</button>
|
|
75
|
+
`;
|
|
76
|
+
col.appendChild(header);
|
|
77
|
+
|
|
78
|
+
const body = document.createElement('div');
|
|
79
|
+
body.className = 'col-cards flex-1 overflow-y-auto p-2 space-y-2 bg-white/50';
|
|
80
|
+
body.style.cssText = 'min-height:80px; background:#f8fafc;';
|
|
81
|
+
body.dataset.dropzone = status.value;
|
|
82
|
+
|
|
83
|
+
for (const ticket of tickets) body.appendChild(_buildCard(ticket));
|
|
84
|
+
|
|
85
|
+
body.addEventListener('dragover', _onDragOver);
|
|
86
|
+
body.addEventListener('dragenter', _onDragEnter);
|
|
87
|
+
body.addEventListener('dragleave', _onDragLeave);
|
|
88
|
+
body.addEventListener('drop', _onDrop);
|
|
89
|
+
|
|
90
|
+
col.appendChild(body);
|
|
91
|
+
|
|
92
|
+
header.querySelector('.col-add-btn').addEventListener('click', () => {
|
|
93
|
+
col.dispatchEvent(new CustomEvent('ticket:create', {
|
|
94
|
+
bubbles: true, detail: { status: status.value },
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return col;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Card Builder ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function _buildCard(ticket) {
|
|
104
|
+
const priority = PRIORITY_MAP[ticket.priority ?? 2];
|
|
105
|
+
const status = STATUS_MAP[ticket.status ?? 0];
|
|
106
|
+
|
|
107
|
+
const card = document.createElement('div');
|
|
108
|
+
card.className =
|
|
109
|
+
'kanban-card bg-white rounded-lg p-3 shadow-sm cursor-pointer ' +
|
|
110
|
+
'hover:shadow-md transition-all select-none group relative';
|
|
111
|
+
card.style.cssText = `border-left: 3px solid ${status?.cardBorder ?? '#94a3b8'};`;
|
|
112
|
+
card.draggable = true;
|
|
113
|
+
card.dataset.ticketId = ticket.ID;
|
|
114
|
+
|
|
115
|
+
const meta = document.createElement('div');
|
|
116
|
+
meta.className = 'flex items-center justify-between mb-1';
|
|
117
|
+
meta.innerHTML = `
|
|
118
|
+
<span class="text-xs text-slate-400 font-mono">${ticket.ticketnum ?? `#${ticket.ID}`}</span>
|
|
119
|
+
<span class="text-xs font-semibold" style="color:${priority.color}" title="${priority.label}">${priority.symbol}</span>
|
|
120
|
+
`;
|
|
121
|
+
card.appendChild(meta);
|
|
122
|
+
|
|
123
|
+
const name = document.createElement('p');
|
|
124
|
+
name.className = 'text-sm font-medium text-slate-800 leading-snug line-clamp-2 mb-1.5';
|
|
125
|
+
name.textContent = ticket.name ?? '(untitled)';
|
|
126
|
+
card.appendChild(name);
|
|
127
|
+
|
|
128
|
+
if (ticket.duedate) {
|
|
129
|
+
const due = document.createElement('div');
|
|
130
|
+
due.className = 'flex items-center gap-1 text-xs mb-1';
|
|
131
|
+
const isOverdue = ticket.duedate * 1000 < Date.now();
|
|
132
|
+
due.innerHTML = `
|
|
133
|
+
<span class="${isOverdue ? 'text-red-500' : 'text-slate-400'}">📅</span>
|
|
134
|
+
<span class="${isOverdue ? 'text-red-500 font-medium' : 'text-slate-400'}">${_formatDate(ticket.duedate)}</span>
|
|
135
|
+
`;
|
|
136
|
+
card.appendChild(due);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const actions = document.createElement('div');
|
|
140
|
+
actions.className =
|
|
141
|
+
'absolute top-1.5 right-1.5 hidden group-hover:flex gap-1';
|
|
142
|
+
actions.innerHTML = `
|
|
143
|
+
<button class="card-btn-edit p-1 rounded bg-slate-100 hover:bg-blue-100 text-slate-500 hover:text-blue-600 text-xs" title="Edit">✎</button>
|
|
144
|
+
<button class="card-btn-delete p-1 rounded bg-slate-100 hover:bg-red-100 text-slate-500 hover:text-red-600 text-xs" title="Delete">✕</button>
|
|
145
|
+
`;
|
|
146
|
+
card.appendChild(actions);
|
|
147
|
+
|
|
148
|
+
card.addEventListener('dragstart', _onDragStart);
|
|
149
|
+
card.addEventListener('dragend', _onDragEnd);
|
|
150
|
+
// Allow dropping ONTO a card (not just onto the column background). Without
|
|
151
|
+
// this some browsers require e.preventDefault() at the target element level
|
|
152
|
+
// and won't allow a drop when the cursor is over a child card.
|
|
153
|
+
card.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; });
|
|
154
|
+
|
|
155
|
+
card.addEventListener('click', e => {
|
|
156
|
+
if (e.target.closest('.card-btn-edit') || e.target.closest('.card-btn-delete')) return;
|
|
157
|
+
card.dispatchEvent(new CustomEvent('ticket:open', {
|
|
158
|
+
bubbles: true, detail: { ticketId: ticket.ID, mode: 'view' },
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
actions.querySelector('.card-btn-edit').addEventListener('click', e => {
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
card.dispatchEvent(new CustomEvent('ticket:open', {
|
|
165
|
+
bubbles: true, detail: { ticketId: ticket.ID, mode: 'edit' },
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
actions.querySelector('.card-btn-delete').addEventListener('click', e => {
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
card.dispatchEvent(new CustomEvent('ticket:delete', {
|
|
172
|
+
bubbles: true, detail: { ticketId: ticket.ID },
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return card;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Drag & Drop ────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
let _dragTicketId = null;
|
|
182
|
+
let _dragSourceStatus = null;
|
|
183
|
+
|
|
184
|
+
function _onDragStart(e) {
|
|
185
|
+
const card = e.currentTarget;
|
|
186
|
+
_dragTicketId = Number(card.dataset.ticketId);
|
|
187
|
+
_dragSourceStatus = Number(card.closest('[data-column]')?.dataset.column);
|
|
188
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
189
|
+
e.dataTransfer.setData('text/plain', String(_dragTicketId));
|
|
190
|
+
// slight delay so the ghost image renders before we fade the card
|
|
191
|
+
requestAnimationFrame(() => card.style.opacity = '0.4');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _onDragEnd(e) {
|
|
195
|
+
e.currentTarget.style.opacity = '';
|
|
196
|
+
_dragTicketId = null;
|
|
197
|
+
_dragSourceStatus = null;
|
|
198
|
+
document.querySelectorAll('.col-cards').forEach(z => z.classList.remove('drop-active'));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _onDragOver(e) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
e.dataTransfer.dropEffect = 'move';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _onDragEnter(e) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.currentTarget.classList.add('drop-active');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _onDragLeave(e) {
|
|
212
|
+
// Only remove when leaving the dropzone itself, not a child element
|
|
213
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
214
|
+
e.currentTarget.classList.remove('drop-active');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _onDrop(e) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
const zone = e.currentTarget;
|
|
221
|
+
const toStatus = Number(zone.dataset.dropzone);
|
|
222
|
+
zone.classList.remove('drop-active');
|
|
223
|
+
|
|
224
|
+
// dataTransfer is the authoritative source — it's set in dragstart and
|
|
225
|
+
// stays available through the drop event regardless of dragend timing.
|
|
226
|
+
const raw = e.dataTransfer.getData('text/plain');
|
|
227
|
+
const ticketId = raw ? Number(raw) : _dragTicketId;
|
|
228
|
+
if (!ticketId) return;
|
|
229
|
+
|
|
230
|
+
// Read source status from the module variable; NaN means same-column check
|
|
231
|
+
// is skipped (safe to always dispatch and let main.js do the guard).
|
|
232
|
+
const srcStatus = Number.isFinite(_dragSourceStatus) ? _dragSourceStatus : toStatus + 1;
|
|
233
|
+
if (toStatus === srcStatus) return;
|
|
234
|
+
|
|
235
|
+
zone.dispatchEvent(new CustomEvent('ticket:move', {
|
|
236
|
+
bubbles: true, detail: { ticketId, toStatus },
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function _formatDate(unix) {
|
|
243
|
+
return new Date(unix * 1000).toLocaleDateString(undefined, {
|
|
244
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application entry point.
|
|
3
|
+
* Bootstraps the client, handles auth, wires up all UI events,
|
|
4
|
+
* and orchestrates data loading / board re-renders.
|
|
5
|
+
*
|
|
6
|
+
* Authentication: reads config from <body> data attributes + localStorage.
|
|
7
|
+
* 1. If tokens are available → token mode
|
|
8
|
+
* 2. If URL only → try session detection via /oauth2/v1/userinfo
|
|
9
|
+
* 3. Otherwise → connection screen with troubleshooting
|
|
10
|
+
*
|
|
11
|
+
* Exposes a global ZeyOS console API for debugging / configuration.
|
|
12
|
+
*/
|
|
13
|
+
import { initTokenClient, initSessionClient, fetchTickets, fetchProjects, updateTicket, deleteTicket } from './api.js';
|
|
14
|
+
import { trySessionAuth, logout } from './auth.js';
|
|
15
|
+
import { runtime, resolveConfig, saveUrl, saveTokens, clearTokens, clearUrl, saveContext } from './state.js';
|
|
16
|
+
import { renderBoard, updateColumn } from './kanban.js';
|
|
17
|
+
import { openTicketDetail, openCreateTicket } from './modals.js';
|
|
18
|
+
import { openSettings, closeSettings, onSettingsChange } from './settings.js';
|
|
19
|
+
import { showToast, showLoading, hideLoading } from './ui.js';
|
|
20
|
+
|
|
21
|
+
// ── Boot ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
24
|
+
const config = resolveConfig();
|
|
25
|
+
|
|
26
|
+
// 1. No URL configured → show connection screen immediately
|
|
27
|
+
if (!config.url) {
|
|
28
|
+
_showConnectionScreen('No ZeyOS URL configured.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
runtime.url = config.url;
|
|
33
|
+
|
|
34
|
+
// 2. Tokens available → initialize in token mode
|
|
35
|
+
if (config.accessToken) {
|
|
36
|
+
saveTokens({
|
|
37
|
+
accessToken: config.accessToken,
|
|
38
|
+
refreshToken: config.refreshToken ?? null,
|
|
39
|
+
});
|
|
40
|
+
initTokenClient(config.url);
|
|
41
|
+
runtime.authMode = 'token';
|
|
42
|
+
await _bootApp();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. No tokens → try session detection
|
|
47
|
+
showLoading();
|
|
48
|
+
const userInfo = await trySessionAuth(config.url);
|
|
49
|
+
hideLoading();
|
|
50
|
+
|
|
51
|
+
if (userInfo) {
|
|
52
|
+
initSessionClient(config.url);
|
|
53
|
+
runtime.authMode = 'session';
|
|
54
|
+
await _bootApp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. Nothing works → connection screen
|
|
59
|
+
_showConnectionScreen('Could not connect. Set a token or log into ZeyOS first.');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── Connection Screen ────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function _showConnectionScreen(message) {
|
|
65
|
+
document.getElementById('connection-screen')?.classList.remove('hidden');
|
|
66
|
+
document.getElementById('app-shell')?.classList.add('hidden');
|
|
67
|
+
|
|
68
|
+
const msgEl = document.getElementById('connection-message');
|
|
69
|
+
if (msgEl) msgEl.textContent = message;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Main App Boot ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
async function _bootApp() {
|
|
75
|
+
document.getElementById('connection-screen')?.classList.add('hidden');
|
|
76
|
+
document.getElementById('app-shell')?.classList.remove('hidden');
|
|
77
|
+
|
|
78
|
+
onSettingsChange(_refreshBoard);
|
|
79
|
+
|
|
80
|
+
_wireNavbar();
|
|
81
|
+
_wireBoard();
|
|
82
|
+
|
|
83
|
+
showLoading();
|
|
84
|
+
try {
|
|
85
|
+
await _loadContextData();
|
|
86
|
+
await _loadTickets();
|
|
87
|
+
renderBoard();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err?.status === 401) {
|
|
90
|
+
showToast('Session expired. Please log in again.', 'error');
|
|
91
|
+
setTimeout(async () => { await logout(); location.reload(); }, 2000);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
showToast(`Failed to load data: ${err.message}`, 'error');
|
|
95
|
+
} finally {
|
|
96
|
+
hideLoading();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Data Loading ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function _loadContextData() {
|
|
103
|
+
runtime.projects = await fetchProjects();
|
|
104
|
+
_renderContextDropdown();
|
|
105
|
+
_updateContextLabel();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function _loadTickets() {
|
|
109
|
+
runtime.tickets = await fetchTickets({ context: runtime.context });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function _refreshBoard() {
|
|
113
|
+
showLoading();
|
|
114
|
+
try {
|
|
115
|
+
await _loadTickets();
|
|
116
|
+
renderBoard();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
showToast(`Refresh failed: ${err.message}`, 'error');
|
|
119
|
+
} finally {
|
|
120
|
+
hideLoading();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Navbar Wiring ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function _wireNavbar() {
|
|
127
|
+
document.getElementById('btn-settings')?.addEventListener('click', openSettings);
|
|
128
|
+
document.getElementById('btn-open-settings')?.addEventListener('click', openSettings);
|
|
129
|
+
document.getElementById('btn-close-settings')?.addEventListener('click', closeSettings);
|
|
130
|
+
document.getElementById('settings-overlay')?.addEventListener('click', closeSettings);
|
|
131
|
+
|
|
132
|
+
document.getElementById('btn-new-ticket')?.addEventListener('click', () => {
|
|
133
|
+
openCreateTicket(runtime.settings.columns[0] ?? 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
document.getElementById('btn-logout')?.addEventListener('click', async () => {
|
|
137
|
+
if (!confirm('Log out of ZeyOS Kanban?')) return;
|
|
138
|
+
await logout();
|
|
139
|
+
clearUrl();
|
|
140
|
+
location.reload();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Context button toggle
|
|
144
|
+
document.getElementById('btn-context')?.addEventListener('click', e => {
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
const drop = document.getElementById('context-dropdown');
|
|
147
|
+
if (!drop) return;
|
|
148
|
+
if (drop.classList.contains('hidden')) {
|
|
149
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
150
|
+
drop.style.top = `${rect.bottom + 4}px`;
|
|
151
|
+
drop.style.left = `${rect.left}px`;
|
|
152
|
+
drop.classList.remove('hidden');
|
|
153
|
+
} else {
|
|
154
|
+
drop.classList.add('hidden');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
document.addEventListener('click', e => {
|
|
159
|
+
if (!e.target.closest('#context-dropdown') && !e.target.closest('#btn-context')) {
|
|
160
|
+
document.getElementById('context-dropdown')?.classList.add('hidden');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Board Event Wiring ──────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function _wireBoard() {
|
|
168
|
+
const board = document.getElementById('kanban-board');
|
|
169
|
+
if (!board) return;
|
|
170
|
+
|
|
171
|
+
// Ticket open
|
|
172
|
+
board.addEventListener('ticket:open', async e => {
|
|
173
|
+
const { ticketId, mode } = e.detail;
|
|
174
|
+
await openTicketDetail(ticketId, mode);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Drag-and-drop move
|
|
178
|
+
board.addEventListener('ticket:move', async e => {
|
|
179
|
+
const { ticketId, toStatus } = e.detail;
|
|
180
|
+
const ticket = runtime.tickets.find(t => t.ID === ticketId);
|
|
181
|
+
if (!ticket) return;
|
|
182
|
+
|
|
183
|
+
const fromStatus = ticket.status;
|
|
184
|
+
// Optimistic update
|
|
185
|
+
ticket.status = toStatus;
|
|
186
|
+
updateColumn(fromStatus);
|
|
187
|
+
updateColumn(toStatus);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const updated = await updateTicket(ticketId, { status: toStatus });
|
|
191
|
+
|
|
192
|
+
// Use the server-confirmed status from the response body.
|
|
193
|
+
// If the API rejected or clamped the value it will differ from toStatus.
|
|
194
|
+
const confirmedStatus = updated?.status ?? toStatus;
|
|
195
|
+
if (confirmedStatus !== toStatus) {
|
|
196
|
+
ticket.status = confirmedStatus;
|
|
197
|
+
updateColumn(toStatus);
|
|
198
|
+
updateColumn(confirmedStatus);
|
|
199
|
+
showToast(`Status set to ${confirmedStatus} (server override).`, 'info');
|
|
200
|
+
} else {
|
|
201
|
+
showToast('Status updated.', 'success');
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Revert on failure
|
|
205
|
+
ticket.status = fromStatus;
|
|
206
|
+
updateColumn(fromStatus);
|
|
207
|
+
updateColumn(toStatus);
|
|
208
|
+
showToast(`Move failed: ${err.message}`, 'error');
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Quick-create
|
|
213
|
+
board.addEventListener('ticket:create', e => {
|
|
214
|
+
openCreateTicket(e.detail.status);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Delete from card hover button
|
|
218
|
+
board.addEventListener('ticket:delete', async e => {
|
|
219
|
+
const { ticketId } = e.detail;
|
|
220
|
+
const ticket = runtime.tickets.find(t => t.ID === ticketId);
|
|
221
|
+
if (!confirm(`Delete "${ticket?.name ?? ticketId}"?`)) return;
|
|
222
|
+
try {
|
|
223
|
+
await deleteTicket(ticketId);
|
|
224
|
+
runtime.tickets = runtime.tickets.filter(t => t.ID !== ticketId);
|
|
225
|
+
renderBoard();
|
|
226
|
+
showToast('Ticket deleted.', 'success');
|
|
227
|
+
} catch (err) {
|
|
228
|
+
showToast(`Delete failed: ${err.message}`, 'error');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Reload after create/update/delete inside modals
|
|
233
|
+
document.addEventListener('app:reload', _refreshBoard);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Context Dropdown ────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function _renderContextDropdown() {
|
|
239
|
+
const drop = document.getElementById('context-dropdown');
|
|
240
|
+
if (!drop) return;
|
|
241
|
+
|
|
242
|
+
const allItem = { type: 'all', id: null, name: 'All Tickets' };
|
|
243
|
+
const projectItems = runtime.projects.map(p => ({ type: 'project', id: p.ID, name: p.name }));
|
|
244
|
+
|
|
245
|
+
const groupHtml = (title, list) =>
|
|
246
|
+
list.length === 0 ? '' : `
|
|
247
|
+
${title ? `<li class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wide">${title}</li>` : ''}
|
|
248
|
+
${list.map(item => `
|
|
249
|
+
<li>
|
|
250
|
+
<button class="ctx-item w-full text-left px-3 py-2 text-sm hover:bg-slate-50 text-slate-700 flex items-center gap-2"
|
|
251
|
+
data-type="${item.type}" data-id="${item.id ?? ''}" data-name="${_esc(item.name)}">
|
|
252
|
+
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 ${item.type === 'all' ? 'bg-slate-400' : 'bg-purple-400'}"></span>
|
|
253
|
+
${_esc(item.name)}
|
|
254
|
+
</button>
|
|
255
|
+
</li>
|
|
256
|
+
`).join('')}
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
drop.innerHTML = `<ul class="py-1">
|
|
260
|
+
${groupHtml('', [allItem])}
|
|
261
|
+
${groupHtml('Projects', projectItems)}
|
|
262
|
+
</ul>`;
|
|
263
|
+
|
|
264
|
+
drop.querySelectorAll('.ctx-item').forEach(btn => {
|
|
265
|
+
btn.addEventListener('click', async () => {
|
|
266
|
+
drop.classList.add('hidden');
|
|
267
|
+
const ctx = {
|
|
268
|
+
type: btn.dataset.type,
|
|
269
|
+
id: btn.dataset.id ? Number(btn.dataset.id) : null,
|
|
270
|
+
name: btn.dataset.name,
|
|
271
|
+
};
|
|
272
|
+
runtime.context = ctx;
|
|
273
|
+
saveContext(ctx);
|
|
274
|
+
_updateContextLabel();
|
|
275
|
+
await _refreshBoard();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _updateContextLabel() {
|
|
281
|
+
const el = document.getElementById('context-label');
|
|
282
|
+
if (el) el.textContent = runtime.context.name ?? 'All Tickets';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _esc(str) {
|
|
286
|
+
return String(str)
|
|
287
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
288
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Console API ─────────────────────────────────────────────────────────────
|
|
292
|
+
// Provides ZeyOS.setUrl(), ZeyOS.setToken(), ZeyOS.status(), etc.
|
|
293
|
+
// Values set via the console are persisted to localStorage and override
|
|
294
|
+
// <body> data attributes on next page load.
|
|
295
|
+
|
|
296
|
+
globalThis.ZeyOS = {
|
|
297
|
+
/**
|
|
298
|
+
* Set the ZeyOS instance URL.
|
|
299
|
+
* @param {string} url - e.g. 'https://cloud.zeyos.com/demo/'
|
|
300
|
+
*/
|
|
301
|
+
setUrl(url) {
|
|
302
|
+
if (!url || typeof url !== 'string') {
|
|
303
|
+
console.error('[ZeyOS] Usage: ZeyOS.setUrl("https://cloud.zeyos.com/demo/")');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
saveUrl(url.trim());
|
|
307
|
+
console.log(`%c[ZeyOS]%c URL set to: ${url}`, 'color:#2563eb;font-weight:bold', '');
|
|
308
|
+
console.log('%c[ZeyOS]%c Call ZeyOS.reconnect() to apply.', 'color:#2563eb;font-weight:bold', '');
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Set access (and optionally refresh) token.
|
|
313
|
+
* @param {string} accessToken
|
|
314
|
+
* @param {string} [refreshToken]
|
|
315
|
+
*/
|
|
316
|
+
setToken(accessToken, refreshToken) {
|
|
317
|
+
if (!accessToken || typeof accessToken !== 'string') {
|
|
318
|
+
console.error('[ZeyOS] Usage: ZeyOS.setToken("access-token", "optional-refresh-token")');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
saveTokens({
|
|
322
|
+
accessToken: accessToken.trim(),
|
|
323
|
+
refreshToken: refreshToken?.trim() ?? null,
|
|
324
|
+
});
|
|
325
|
+
console.log('%c[ZeyOS]%c Token saved.', 'color:#2563eb;font-weight:bold', '');
|
|
326
|
+
console.log('%c[ZeyOS]%c Call ZeyOS.reconnect() to apply.', 'color:#2563eb;font-weight:bold', '');
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Print the current connection status to the console.
|
|
331
|
+
*/
|
|
332
|
+
status() {
|
|
333
|
+
const config = resolveConfig();
|
|
334
|
+
const lines = [
|
|
335
|
+
'',
|
|
336
|
+
` URL: ${config.url ?? '(not set)'}`,
|
|
337
|
+
` Access Token: ${config.accessToken ? config.accessToken.slice(0, 16) + '...' : '(not set)'}`,
|
|
338
|
+
` Refresh Token: ${config.refreshToken ? 'yes' : 'no'}`,
|
|
339
|
+
` Auth Mode: ${runtime.authMode ?? '(not connected)'}`,
|
|
340
|
+
'',
|
|
341
|
+
];
|
|
342
|
+
console.log(`%c[ZeyOS] Status%c\n${lines.join('\n')}`, 'color:#2563eb;font-weight:bold', 'color:inherit');
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Clear all stored config (URL + tokens) and reload.
|
|
347
|
+
*/
|
|
348
|
+
logout() {
|
|
349
|
+
clearTokens();
|
|
350
|
+
clearUrl();
|
|
351
|
+
console.log('%c[ZeyOS]%c Config cleared. Reloading...', 'color:#2563eb;font-weight:bold', '');
|
|
352
|
+
location.reload();
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Reload the page to re-run the boot sequence with current config.
|
|
357
|
+
*/
|
|
358
|
+
reconnect() {
|
|
359
|
+
console.log('%c[ZeyOS]%c Reconnecting...', 'color:#2563eb;font-weight:bold', '');
|
|
360
|
+
location.reload();
|
|
361
|
+
},
|
|
362
|
+
};
|