@cccarv82/freya 2.14.0 → 2.15.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/cli/web-ui.css +341 -25
- package/cli/web-ui.js +460 -156
- package/cli/web.js +713 -236
- package/package.json +3 -3
- package/scripts/lib/DataLayer.js +10 -1
- package/scripts/lib/DataManager.js +1 -0
- package/scripts/lib/Embedder.js +5 -1
- package/templates/base/scripts/lib/DataLayer.js +11 -2
- package/templates/base/scripts/lib/DataManager.js +15 -14
- package/templates/base/scripts/lib/Embedder.js +5 -1
package/cli/web-ui.js
CHANGED
|
@@ -139,10 +139,9 @@
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const li = line.match(/^[ \t]*[-*][ \t]+(.*)$/);
|
|
142
|
-
|
|
143
|
-
if (li || oli) {
|
|
142
|
+
if (li) {
|
|
144
143
|
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
145
|
-
const content = inlineFormat(
|
|
144
|
+
const content = inlineFormat(li[1]);
|
|
146
145
|
html += '<li>' + content + '</li>';
|
|
147
146
|
continue;
|
|
148
147
|
}
|
|
@@ -193,37 +192,42 @@
|
|
|
193
192
|
|
|
194
193
|
// --- Strategy 2: regex fallback for truncated/malformed JSON ---
|
|
195
194
|
var lines = [];
|
|
195
|
+
var num = 0;
|
|
196
196
|
|
|
197
197
|
// Match append_daily_log / appenddailylog actions
|
|
198
198
|
var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,300})/gi;
|
|
199
199
|
var m;
|
|
200
200
|
while ((m = logRe.exec(text)) !== null) {
|
|
201
|
+
num++;
|
|
201
202
|
var t = m[1].slice(0, 140);
|
|
202
|
-
lines.push('
|
|
203
|
+
lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + t + (m[1].length > 140 ? '...' : ''));
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// Match create_task actions
|
|
206
207
|
var taskRe = /"type"\s*:\s*"create_?task"\s*,\s*"description"\s*:\s*"([^"]{1,200})/gi;
|
|
207
208
|
while ((m = taskRe.exec(text)) !== null) {
|
|
209
|
+
num++;
|
|
208
210
|
var desc = m[1].slice(0, 120);
|
|
209
211
|
var priMatch = text.slice(m.index, m.index + 400).match(/"priority"\s*:\s*"(\w+)"/i);
|
|
210
212
|
var pri = priMatch ? ' (prioridade: **' + priMatch[1].toUpperCase() + '**)' : '';
|
|
211
|
-
lines.push('
|
|
213
|
+
lines.push(num + '. \u2705 **Criar tarefa:** ' + desc + pri);
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Match create_blocker actions
|
|
215
217
|
var blockerRe = /"type"\s*:\s*"create_?blocker"\s*,\s*"title"\s*:\s*"([^"]{1,200})/gi;
|
|
216
218
|
while ((m = blockerRe.exec(text)) !== null) {
|
|
219
|
+
num++;
|
|
217
220
|
var title = m[1].slice(0, 120);
|
|
218
221
|
var sevMatch = text.slice(m.index, m.index + 400).match(/"severity"\s*:\s*"(\w+)"/i);
|
|
219
222
|
var sev = sevMatch ? ' (severidade: **' + sevMatch[1].toUpperCase() + '**)' : '';
|
|
220
|
-
lines.push('
|
|
223
|
+
lines.push(num + '. \u{1F6A7} **Registrar blocker:** ' + title + sev);
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
// Match suggest_report actions
|
|
224
227
|
var repRe = /"type"\s*:\s*"suggest_?report"\s*,\s*"name"\s*:\s*"([^"]+)"/gi;
|
|
225
228
|
while ((m = repRe.exec(text)) !== null) {
|
|
226
|
-
|
|
229
|
+
num++;
|
|
230
|
+
lines.push(num + '. \u{1F4CA} **Sugerir relatorio:** ' + m[1]);
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
@@ -236,32 +240,33 @@
|
|
|
236
240
|
oraclequery: '\u{1F50D}'
|
|
237
241
|
};
|
|
238
242
|
|
|
239
|
-
var lines = actions.map(function(a) {
|
|
243
|
+
var lines = actions.map(function(a, i) {
|
|
240
244
|
var type = String(a.type || '').trim().toLowerCase().replace(/_/g, '');
|
|
241
245
|
var icon = icons[type] || '\u2022';
|
|
246
|
+
var num = i + 1;
|
|
242
247
|
|
|
243
248
|
if (type === 'appenddailylog') {
|
|
244
249
|
var t = String(a.text || '').slice(0, 140);
|
|
245
|
-
return '
|
|
250
|
+
return num + '. ' + icon + ' **Registrar no log:** ' + t + (String(a.text || '').length > 140 ? '...' : '');
|
|
246
251
|
}
|
|
247
252
|
if (type === 'createtask') {
|
|
248
253
|
var desc = String(a.description || '').slice(0, 120);
|
|
249
254
|
var pri = a.priority ? ' (prioridade: **' + String(a.priority).toUpperCase() + '**)' : '';
|
|
250
255
|
var cat = a.category ? ' [' + a.category + ']' : '';
|
|
251
|
-
return '
|
|
256
|
+
return num + '. ' + icon + ' **Criar tarefa:** ' + desc + pri + cat;
|
|
252
257
|
}
|
|
253
258
|
if (type === 'createblocker') {
|
|
254
259
|
var title = String(a.title || a.description || '').slice(0, 120);
|
|
255
260
|
var sev = a.severity ? ' (severidade: **' + String(a.severity).toUpperCase() + '**)' : '';
|
|
256
|
-
return '
|
|
261
|
+
return num + '. ' + icon + ' **Registrar blocker:** ' + title + sev;
|
|
257
262
|
}
|
|
258
263
|
if (type === 'suggestreport') {
|
|
259
|
-
return '
|
|
264
|
+
return num + '. ' + icon + ' **Sugerir relatorio:** ' + String(a.name || a.reportType || '');
|
|
260
265
|
}
|
|
261
266
|
if (type === 'oraclequery') {
|
|
262
|
-
return '
|
|
267
|
+
return num + '. ' + icon + ' **Consultar oracle:** ' + String(a.query || '').slice(0, 120);
|
|
263
268
|
}
|
|
264
|
-
return '
|
|
269
|
+
return num + '. \u2022 **' + String(a.type || 'acao') + '**';
|
|
265
270
|
});
|
|
266
271
|
|
|
267
272
|
return lines.join('\n');
|
|
@@ -336,6 +341,11 @@
|
|
|
336
341
|
persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
// show the thread now that it has content
|
|
345
|
+
thread.style.display = 'flex';
|
|
346
|
+
thread.style.padding = '12px';
|
|
347
|
+
thread.style.borderTop = '1px solid var(--border)';
|
|
348
|
+
|
|
339
349
|
// keep newest in view
|
|
340
350
|
try {
|
|
341
351
|
thread.scrollTop = thread.scrollHeight;
|
|
@@ -472,8 +482,8 @@
|
|
|
472
482
|
const thread = $('chatThread');
|
|
473
483
|
if (!thread) return;
|
|
474
484
|
const hasContent = thread.children.length > 0;
|
|
485
|
+
thread.style.display = hasContent ? 'flex' : 'none';
|
|
475
486
|
thread.style.padding = hasContent ? '12px' : '0';
|
|
476
|
-
thread.style.maxHeight = hasContent ? '280px' : '0';
|
|
477
487
|
thread.style.borderTop = hasContent ? '1px solid var(--border)' : 'none';
|
|
478
488
|
}
|
|
479
489
|
|
|
@@ -1426,6 +1436,7 @@
|
|
|
1426
1436
|
const health = $('railCompanion');
|
|
1427
1437
|
const graph = $('railGraph');
|
|
1428
1438
|
const docs = $('railDocs');
|
|
1439
|
+
const kanban = $('railKanban');
|
|
1429
1440
|
|
|
1430
1441
|
const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
|
|
1431
1442
|
const isDashboard = !curPage || curPage === 'dashboard';
|
|
@@ -1455,6 +1466,11 @@
|
|
|
1455
1466
|
if (curPage !== 'companion') window.location.href = '/companion';
|
|
1456
1467
|
};
|
|
1457
1468
|
}
|
|
1469
|
+
if (kanban) {
|
|
1470
|
+
kanban.onclick = () => {
|
|
1471
|
+
if (curPage !== 'kanban') window.location.href = '/kanban';
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1458
1474
|
if (tl) {
|
|
1459
1475
|
tl.onclick = () => {
|
|
1460
1476
|
if (curPage !== 'timeline') window.location.href = '/timeline';
|
|
@@ -2130,172 +2146,179 @@
|
|
|
2130
2146
|
}
|
|
2131
2147
|
}
|
|
2132
2148
|
|
|
2149
|
+
// New Companion Dashboard functions
|
|
2133
2150
|
async function refreshCompanionDash() {
|
|
2151
|
+
const filter = document.querySelector('.companionTabs .tab.active')?.dataset?.filter || 'all';
|
|
2134
2152
|
try {
|
|
2135
|
-
|
|
2136
|
-
const [pRes, sRes, aRes] = await Promise.all([
|
|
2153
|
+
const [prj, brk, alts] = await Promise.all([
|
|
2137
2154
|
api('/api/companion/projects-summary', { dir: dirOrDefault() }),
|
|
2138
2155
|
api('/api/companion/streams-breakdown', { dir: dirOrDefault() }),
|
|
2139
2156
|
api('/api/companion/alerts', { dir: dirOrDefault() })
|
|
2140
2157
|
]);
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2158
|
+
|
|
2159
|
+
const projects = (prj && prj.projects) || [];
|
|
2160
|
+
const breakdown = (brk && brk.breakdown) || [];
|
|
2161
|
+
const alerts = (alts && alts.alerts) || [];
|
|
2162
|
+
|
|
2163
|
+
// Show/hide sections based on filter
|
|
2164
|
+
$('consolidatedViewBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2165
|
+
$('projectCardsBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2166
|
+
$('streamBreakdownBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2167
|
+
$('alertsViewBox').style.display = filter === 'alerts' || filter === 'risk' ? 'block' : 'none';
|
|
2168
|
+
|
|
2169
|
+
if (filter === 'all' || filter === 'risk') {
|
|
2170
|
+
renderConsolidatedView(projects);
|
|
2171
|
+
renderProjectCards(projects, filter === 'risk');
|
|
2172
|
+
renderStreamBreakdown(breakdown, filter === 'risk');
|
|
2144
2173
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2174
|
+
|
|
2175
|
+
if (filter === 'alerts' || filter === 'risk') {
|
|
2176
|
+
renderAlerts(alerts, filter === 'risk');
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
setPill('ok', 'dashboard atualizado');
|
|
2150
2180
|
} catch (e) {
|
|
2151
|
-
setPill('err', '
|
|
2181
|
+
setPill('err', 'falha ao carregar dashboard');
|
|
2152
2182
|
console.error('refreshCompanionDash error:', e);
|
|
2153
2183
|
}
|
|
2154
2184
|
}
|
|
2155
2185
|
|
|
2156
2186
|
function renderConsolidatedView(projects) {
|
|
2157
|
-
const
|
|
2158
|
-
if (!
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
<div style="font-size:20px; font-weight:700;">⏳ ${totalPending}</div>
|
|
2175
|
-
</div>
|
|
2176
|
-
<div class="kpiCard">
|
|
2177
|
-
<div style="font-size:11px; color:var(--textSecondary); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">CRITICAL</div>
|
|
2178
|
-
<div style="font-size:20px; font-weight:700; color:#ef4444;">🚧 ${totalCritical}</div>
|
|
2179
|
-
</div>
|
|
2187
|
+
const box = $('consolidatedView');
|
|
2188
|
+
if (!box) return;
|
|
2189
|
+
|
|
2190
|
+
const totalCompleted = projects.reduce((sum, p) => sum + p.completedTasks, 0);
|
|
2191
|
+
const totalPending = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
|
|
2192
|
+
const totalCritical = projects.reduce((sum, p) => sum + (p.blockersBySeverity?.CRITICAL || 0), 0);
|
|
2193
|
+
const totalPendingTasks = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
|
|
2194
|
+
const overallCompletion = projects.reduce((sum, p) => sum + p.completionRate, 0) / Math.max(projects.length, 1);
|
|
2195
|
+
|
|
2196
|
+
const kpis = [
|
|
2197
|
+
{ icon: '✅', label: 'Concluídas', value: totalCompleted, unit: 'semana' },
|
|
2198
|
+
{ icon: '⏳', label: 'Pendentes', value: totalPending, unit: 'lembretes' },
|
|
2199
|
+
{ icon: '🚧', label: 'CRITICAL', value: totalCritical, unit: 'bloqueios' },
|
|
2200
|
+
{ icon: '📋', label: 'Taxa Conclusão', value: Math.round(overallCompletion), unit: '%' }
|
|
2201
|
+
];
|
|
2202
|
+
|
|
2203
|
+
box.innerHTML = kpis.map(kpi => `
|
|
2180
2204
|
<div class="kpiCard">
|
|
2181
|
-
<div style="font-size:
|
|
2182
|
-
<div style="font-size:
|
|
2205
|
+
<div style="font-size: 24px; margin-bottom: 4px;">${kpi.icon}</div>
|
|
2206
|
+
<div style="font-size: 28px; font-weight: 700; color: var(--accent);">${kpi.value}</div>
|
|
2207
|
+
<div style="font-size: 11px; color: var(--textMuted); margin-top: 4px;">${kpi.label}</div>
|
|
2208
|
+
<div style="font-size: 10px; color: var(--textMuted);">${kpi.unit}</div>
|
|
2183
2209
|
</div>
|
|
2184
|
-
|
|
2210
|
+
`).join('');
|
|
2185
2211
|
}
|
|
2186
2212
|
|
|
2187
|
-
function renderProjectCards(projects) {
|
|
2188
|
-
const
|
|
2189
|
-
if (!
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2213
|
+
function renderProjectCards(projects, onlyRisk = false) {
|
|
2214
|
+
const box = $('projectCardsGrid');
|
|
2215
|
+
if (!box) return;
|
|
2216
|
+
|
|
2217
|
+
let filtered = projects;
|
|
2218
|
+
if (onlyRisk) {
|
|
2219
|
+
filtered = projects.filter(p => p.status === 'AT_RISK' || p.status === 'IDLE');
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
if (filtered.length === 0) {
|
|
2223
|
+
box.innerHTML = '<div class="help">Nenhum projeto para exibir.</div>';
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
box.innerHTML = filtered.map(p => {
|
|
2228
|
+
const statusColor = p.status === 'IDLE' ? '#666' : p.status === 'AT_RISK' ? '#ff9900' : '#4ade80';
|
|
2229
|
+
const statusText = p.status === 'IDLE' ? 'Inativo' : p.status === 'AT_RISK' ? 'Risco' : 'OK';
|
|
2194
2230
|
return `
|
|
2195
|
-
<div class="projectCard" style="border-left-color
|
|
2196
|
-
<div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:
|
|
2197
|
-
<div>
|
|
2198
|
-
|
|
2199
|
-
<div style="font-size:11px; color:var(--textSecondary); margin-top:2px;">${statusLabel}</div>
|
|
2200
|
-
</div>
|
|
2201
|
-
<span class="pill" style="background:${statusColor}22; border-color:${statusColor}55; color:${statusColor};">${progressPct}%</span>
|
|
2202
|
-
</div>
|
|
2203
|
-
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:10px; padding:8px; background:var(--bg); border-radius:0;">
|
|
2204
|
-
<div style="text-align:center;">
|
|
2205
|
-
<div style="font-size:10px; color:var(--textSecondary);">Concluídas</div>
|
|
2206
|
-
<div style="font-size:14px; font-weight:700;">✓ ${p.completedTasks}/${p.totalTasks}</div>
|
|
2207
|
-
</div>
|
|
2208
|
-
<div style="text-align:center;">
|
|
2209
|
-
<div style="font-size:10px; color:var(--textSecondary);">Blockers</div>
|
|
2210
|
-
<div style="font-size:14px; font-weight:700;">🚧 ${p.blockersCount}</div>
|
|
2211
|
-
</div>
|
|
2231
|
+
<div class="projectCard" style="border-left-color: ${statusColor}; cursor: pointer;" onclick="window.location.href='/dashboard?project=${encodeURIComponent(p.slug)}'">
|
|
2232
|
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
|
2233
|
+
<div style="font-weight: 700; font-size: 13px;">${escapeHtml(p.name)}</div>
|
|
2234
|
+
<span class="pill" style="font-size: 9px; padding: 2px 6px;">${statusText}</span>
|
|
2212
2235
|
</div>
|
|
2213
|
-
<div style="font-size:
|
|
2214
|
-
<div
|
|
2215
|
-
<div
|
|
2236
|
+
<div style="display: flex; gap: 12px; margin-bottom: 8px; font-size: 11px;">
|
|
2237
|
+
<div>✓ ${p.completedTasks}/${p.totalTasks}</div>
|
|
2238
|
+
<div>🚧 ${p.openBlockers}</div>
|
|
2216
2239
|
</div>
|
|
2240
|
+
<div style="font-size: 10px; color: var(--textMuted);">Atualizado: ${p.lastUpdateAgo}</div>
|
|
2217
2241
|
</div>
|
|
2218
2242
|
`;
|
|
2219
2243
|
}).join('');
|
|
2220
|
-
grid.innerHTML = cards || '<div class="help">Nenhum projeto cadastrado.</div>';
|
|
2221
2244
|
}
|
|
2222
2245
|
|
|
2223
|
-
function renderStreamBreakdown(
|
|
2224
|
-
const
|
|
2225
|
-
if (!
|
|
2246
|
+
function renderStreamBreakdown(breakdown, onlyRisk = false) {
|
|
2247
|
+
const box = $('streamBreakdown');
|
|
2248
|
+
if (!box) return;
|
|
2249
|
+
|
|
2250
|
+
if (breakdown.length === 0) {
|
|
2251
|
+
box.innerHTML = '<div class="help">Nenhum stream para exibir.</div>';
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2226
2255
|
let html = '';
|
|
2227
|
-
for (const proj of
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2256
|
+
for (const proj of breakdown) {
|
|
2257
|
+
let streams = proj.streams;
|
|
2258
|
+
if (onlyRisk) {
|
|
2259
|
+
streams = streams.filter(s => s.blockersCount > 0);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (streams.length === 0) continue;
|
|
2263
|
+
|
|
2264
|
+
html += `<div style="margin-bottom: 12px;">
|
|
2265
|
+
<div style="font-weight: 700; font-size: 12px; margin-bottom: 4px; color: var(--accent);">${escapeHtml(proj.projectName)}</div>`;
|
|
2266
|
+
|
|
2267
|
+
for (const s of streams) {
|
|
2268
|
+
const hasBlockers = s.blockersCount > 0;
|
|
2269
|
+
html += `
|
|
2270
|
+
<div class="streamItem" style="background: ${hasBlockers ? 'rgba(239,68,68,0.05)' : 'transparent'};">
|
|
2271
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
2272
|
+
<div style="font-size: 11px;">${escapeHtml(s.streamName)}</div>
|
|
2273
|
+
<div style="font-size: 10px; color: var(--textMuted);">✓ ${s.completedTasks}/${s.totalTasks}</div>
|
|
2238
2274
|
</div>
|
|
2275
|
+
${hasBlockers ? `<div style="font-size: 10px; margin-top: 4px; color: #ef4444;">🚧 ${s.blockersCount} bloqueio(s)</div>` : ''}
|
|
2239
2276
|
</div>
|
|
2240
|
-
|
|
2277
|
+
`;
|
|
2241
2278
|
}
|
|
2242
|
-
|
|
2279
|
+
|
|
2280
|
+
html += '</div>';
|
|
2243
2281
|
}
|
|
2244
|
-
|
|
2282
|
+
|
|
2283
|
+
box.innerHTML = html || '<div class="help">Nenhum stream para exibir.</div>';
|
|
2245
2284
|
}
|
|
2246
2285
|
|
|
2247
|
-
function renderAlerts(alerts) {
|
|
2248
|
-
const
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2286
|
+
function renderAlerts(alerts, onlyHighSeverity = false) {
|
|
2287
|
+
const box = $('alertsView');
|
|
2288
|
+
if (!box) return;
|
|
2289
|
+
|
|
2290
|
+
let filtered = alerts;
|
|
2291
|
+
if (onlyHighSeverity) {
|
|
2292
|
+
filtered = alerts.filter(a => a.severity === 'CRITICAL' || a.severity === 'HIGH');
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (filtered.length === 0) {
|
|
2296
|
+
box.innerHTML = '<div class="help">Nenhum alerta para exibir.</div>';
|
|
2254
2297
|
return;
|
|
2255
2298
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
const
|
|
2259
|
-
return
|
|
2260
|
-
<div style="
|
|
2261
|
-
<div style="
|
|
2262
|
-
<
|
|
2263
|
-
<
|
|
2264
|
-
${a.projectSlug ? `<div style="font-size:10px; color:var(--textSecondary); margin-top:4px;">Projeto: ${escapeHtml(a.projectSlug)}</div>` : ''}
|
|
2299
|
+
|
|
2300
|
+
box.innerHTML = filtered.slice(0, 20).map(a => {
|
|
2301
|
+
const severityColor = a.severity === 'CRITICAL' ? '#ef4444' : a.severity === 'HIGH' ? '#ff9900' : '#facc15';
|
|
2302
|
+
return `
|
|
2303
|
+
<div class="alertItem" style="border-left-color: ${severityColor};">
|
|
2304
|
+
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px;">
|
|
2305
|
+
<span style="font-weight: 700; color: ${severityColor};">${a.severity}</span>
|
|
2306
|
+
<span style="color: var(--textMuted);">${a.type}</span>
|
|
2265
2307
|
</div>
|
|
2308
|
+
<div style="font-size: 11px; margin-top: 4px;">${escapeHtml(a.message)}</div>
|
|
2266
2309
|
</div>
|
|
2267
|
-
|
|
2310
|
+
`;
|
|
2268
2311
|
}).join('');
|
|
2269
|
-
alertsList.innerHTML = items;
|
|
2270
|
-
if (alertsZone) alertsZone.style.display = 'block';
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
function filterCompanionView(filter) {
|
|
2274
|
-
const tabs = document.querySelectorAll('.companionTabs .tab');
|
|
2275
|
-
tabs.forEach(t => t.classList.remove('active'));
|
|
2276
|
-
document.querySelector(`.companionTabs .tab[data-filter="${filter}"]`)?.classList.add('active');
|
|
2277
|
-
const consolidated = $('companionConsolidated');
|
|
2278
|
-
const projects = $('companionProjects');
|
|
2279
|
-
const streams = $('streamBreakdown').parentElement.parentElement;
|
|
2280
|
-
const alerts = $('companionAlerts');
|
|
2281
|
-
if (filter === 'all') {
|
|
2282
|
-
if (consolidated) consolidated.style.display = 'grid';
|
|
2283
|
-
if (projects) projects.style.display = 'grid';
|
|
2284
|
-
if (streams) streams.style.display = 'block';
|
|
2285
|
-
if (alerts) alerts.style.display = 'none';
|
|
2286
|
-
} else if (filter === 'alerts') {
|
|
2287
|
-
if (consolidated) consolidated.style.display = 'none';
|
|
2288
|
-
if (projects) projects.style.display = 'none';
|
|
2289
|
-
if (streams) streams.style.display = 'none';
|
|
2290
|
-
if (alerts) alerts.style.display = 'block';
|
|
2291
|
-
} else if (filter === 'risk') {
|
|
2292
|
-
if (consolidated) consolidated.style.display = 'grid';
|
|
2293
|
-
if (projects) projects.style.display = 'grid';
|
|
2294
|
-
if (streams) streams.style.display = 'block';
|
|
2295
|
-
if (alerts) alerts.style.display = 'none';
|
|
2296
|
-
}
|
|
2297
2312
|
}
|
|
2298
2313
|
|
|
2314
|
+
window.filterCompanionView = function(filter) {
|
|
2315
|
+
document.querySelectorAll('.companionTabs .tab').forEach(tab => {
|
|
2316
|
+
tab.classList.remove('active');
|
|
2317
|
+
});
|
|
2318
|
+
document.querySelector(`[data-filter="${filter}"]`).classList.add('active');
|
|
2319
|
+
refreshCompanionDash();
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2299
2322
|
async function doHealth() {
|
|
2300
2323
|
try {
|
|
2301
2324
|
saveLocal();
|
|
@@ -2757,6 +2780,7 @@
|
|
|
2757
2780
|
const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
|
|
2758
2781
|
const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
|
|
2759
2782
|
const isGraphPage = document.body && document.body.dataset && document.body.dataset.page === 'graph';
|
|
2783
|
+
const isKanbanPage = document.body && document.body.dataset && document.body.dataset.page === 'kanban';
|
|
2760
2784
|
|
|
2761
2785
|
// Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
|
|
2762
2786
|
(async () => {
|
|
@@ -2803,13 +2827,12 @@
|
|
|
2803
2827
|
}
|
|
2804
2828
|
|
|
2805
2829
|
if (isCompanionPage) {
|
|
2806
|
-
await
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
await
|
|
2812
|
-
await refreshHeatmap();
|
|
2830
|
+
await refreshCompanionDash();
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
if (isKanbanPage) {
|
|
2835
|
+
await loadKanban();
|
|
2813
2836
|
return;
|
|
2814
2837
|
}
|
|
2815
2838
|
|
|
@@ -2841,16 +2864,20 @@
|
|
|
2841
2864
|
if (typeof saveAndPlan === 'function') saveAndPlan();
|
|
2842
2865
|
return;
|
|
2843
2866
|
}
|
|
2844
|
-
// Ctrl/Cmd+K:
|
|
2867
|
+
// Ctrl/Cmd+K: Open quick-add modal
|
|
2845
2868
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
2846
2869
|
e.preventDefault();
|
|
2847
|
-
|
|
2848
|
-
if (target) target.focus();
|
|
2870
|
+
openQuickAdd();
|
|
2849
2871
|
return;
|
|
2850
2872
|
}
|
|
2851
|
-
// Escape:
|
|
2852
|
-
if (e.key === 'Escape'
|
|
2853
|
-
|
|
2873
|
+
// Escape: Close quick-add modal or blur active element
|
|
2874
|
+
if (e.key === 'Escape') {
|
|
2875
|
+
const overlay = $('quickAddOverlay');
|
|
2876
|
+
if (overlay && overlay.style.display !== 'none') {
|
|
2877
|
+
closeQuickAdd();
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
if (document.activeElement) document.activeElement.blur();
|
|
2854
2881
|
}
|
|
2855
2882
|
});
|
|
2856
2883
|
|
|
@@ -2890,7 +2917,6 @@
|
|
|
2890
2917
|
window.refreshAnomalies = refreshAnomalies;
|
|
2891
2918
|
window.refreshRiskRadar = refreshRiskRadar;
|
|
2892
2919
|
window.refreshCompanionDash = refreshCompanionDash;
|
|
2893
|
-
window.filterCompanionView = filterCompanionView;
|
|
2894
2920
|
window.copyOut = copyOut;
|
|
2895
2921
|
window.copyPath = copyPath;
|
|
2896
2922
|
window.openSelected = openSelected;
|
|
@@ -2907,4 +2933,282 @@
|
|
|
2907
2933
|
window.askFreya = askFreya;
|
|
2908
2934
|
window.askFreyaInline = askFreyaInline;
|
|
2909
2935
|
window.askFreyaFromInput = askFreyaFromInput;
|
|
2936
|
+
|
|
2937
|
+
/* ── Quick-Add Modal ── */
|
|
2938
|
+
function openQuickAdd() {
|
|
2939
|
+
const overlay = $('quickAddOverlay');
|
|
2940
|
+
if (!overlay) return;
|
|
2941
|
+
overlay.style.display = 'flex';
|
|
2942
|
+
const desc = $('qaDesc');
|
|
2943
|
+
if (desc) { desc.value = ''; desc.focus(); }
|
|
2944
|
+
var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
|
|
2945
|
+
var pri = $('qaPriority'); if (pri) pri.value = '';
|
|
2946
|
+
var slug = $('qaSlug'); if (slug) slug.value = '';
|
|
2947
|
+
var due = $('qaDue'); if (due) due.value = '';
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function closeQuickAdd() {
|
|
2951
|
+
var overlay = $('quickAddOverlay');
|
|
2952
|
+
if (overlay) overlay.style.display = 'none';
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
async function submitQuickAdd() {
|
|
2956
|
+
var desc = $('qaDesc');
|
|
2957
|
+
var text = desc ? desc.value.trim() : '';
|
|
2958
|
+
if (!text) { showToast('err', 'Descricao obrigatoria'); return; }
|
|
2959
|
+
|
|
2960
|
+
var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
|
|
2961
|
+
var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
|
|
2962
|
+
var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
|
|
2963
|
+
var due = $('qaDue'); var dueVal = due ? due.value : '';
|
|
2964
|
+
|
|
2965
|
+
var body = { dir: dirOrDefault(), description: text, category: catVal };
|
|
2966
|
+
if (priVal) body.priority = priVal;
|
|
2967
|
+
if (slugVal) body.projectSlug = slugVal;
|
|
2968
|
+
if (dueVal) body.dueDate = dueVal;
|
|
2969
|
+
|
|
2970
|
+
try {
|
|
2971
|
+
await api('/api/tasks/create', body);
|
|
2972
|
+
closeQuickAdd();
|
|
2973
|
+
showToast('ok', 'Task criada');
|
|
2974
|
+
if (isKanbanPage) await loadKanban();
|
|
2975
|
+
else await refreshToday();
|
|
2976
|
+
} catch (e) {
|
|
2977
|
+
showToast('err', 'Erro ao criar task');
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
window.openQuickAdd = openQuickAdd;
|
|
2982
|
+
window.closeQuickAdd = closeQuickAdd;
|
|
2983
|
+
window.submitQuickAdd = submitQuickAdd;
|
|
2984
|
+
|
|
2985
|
+
/* ── Delta Banner ── */
|
|
2986
|
+
async function loadDelta() {
|
|
2987
|
+
var el = $('kanbanDelta') || $('deltaBanner');
|
|
2988
|
+
if (!el) return;
|
|
2989
|
+
try {
|
|
2990
|
+
var res = await api('/api/summary/delta', { dir: dirOrDefault() });
|
|
2991
|
+
if (!res || !res.delta) { el.style.display = 'none'; return; }
|
|
2992
|
+
var d = res.delta;
|
|
2993
|
+
var parts = [];
|
|
2994
|
+
if (d.completedTasks > 0) parts.push(d.completedTasks + ' concluida(s)');
|
|
2995
|
+
if (d.resolvedBlockers > 0) parts.push(d.resolvedBlockers + ' blocker(s) resolvido(s)');
|
|
2996
|
+
if (d.newTasks > 0) parts.push(d.newTasks + ' nova(s)');
|
|
2997
|
+
if (d.newBlockers > 0) parts.push(d.newBlockers + ' novo(s) blocker(s)');
|
|
2998
|
+
if (d.overdueTasks > 0) parts.push(d.overdueTasks + ' atrasada(s)');
|
|
2999
|
+
if (parts.length === 0) { el.style.display = 'none'; return; }
|
|
3000
|
+
el.style.display = 'flex';
|
|
3001
|
+
el.className = 'delta-banner';
|
|
3002
|
+
el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>'
|
|
3003
|
+
+ '<span>Ultimas 24h: ' + escapeHtml(parts.join(' \u00b7 ')) + '</span>';
|
|
3004
|
+
} catch { el.style.display = 'none'; }
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
/* ── Kanban Board ── */
|
|
3008
|
+
var _kanbanData = { tasks: [], blockers: [] };
|
|
3009
|
+
|
|
3010
|
+
async function loadKanban() {
|
|
3011
|
+
try {
|
|
3012
|
+
var res = await api('/api/tasks/kanban', { dir: dirOrDefault() });
|
|
3013
|
+
if (!res || !res.ok) return;
|
|
3014
|
+
_kanbanData.tasks = res.tasks || [];
|
|
3015
|
+
_kanbanData.blockers = res.blockers || [];
|
|
3016
|
+
populateKanbanProjects();
|
|
3017
|
+
renderKanban();
|
|
3018
|
+
renderKanbanBlockers();
|
|
3019
|
+
loadDelta();
|
|
3020
|
+
} catch (e) {
|
|
3021
|
+
showToast('err', 'Erro ao carregar kanban');
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function populateKanbanProjects() {
|
|
3026
|
+
var sel = $('kanbanFilterProject');
|
|
3027
|
+
if (!sel) return;
|
|
3028
|
+
var slugs = new Set();
|
|
3029
|
+
_kanbanData.tasks.forEach(function(t) { if (t.projectSlug) slugs.add(t.projectSlug); });
|
|
3030
|
+
_kanbanData.blockers.forEach(function(b) { if (b.projectSlug) slugs.add(b.projectSlug); });
|
|
3031
|
+
var current = sel.value;
|
|
3032
|
+
sel.innerHTML = '<option value="">Todos os projetos</option>';
|
|
3033
|
+
Array.from(slugs).sort().forEach(function(s) {
|
|
3034
|
+
var opt = document.createElement('option');
|
|
3035
|
+
opt.value = s; opt.textContent = s;
|
|
3036
|
+
sel.appendChild(opt);
|
|
3037
|
+
});
|
|
3038
|
+
sel.value = current;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function filterKanban() { renderKanban(); renderKanbanBlockers(); }
|
|
3042
|
+
|
|
3043
|
+
function getFilteredTasks() {
|
|
3044
|
+
var sel = $('kanbanFilterProject');
|
|
3045
|
+
var filter = sel ? sel.value : '';
|
|
3046
|
+
if (!filter) return _kanbanData.tasks;
|
|
3047
|
+
return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
function renderKanban() {
|
|
3051
|
+
var tasks = getFilteredTasks();
|
|
3052
|
+
var today = new Date().toISOString().slice(0, 10);
|
|
3053
|
+
var sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
3054
|
+
|
|
3055
|
+
var cols = {
|
|
3056
|
+
DO_NOW: [], SCHEDULE: [], DELEGATE: [], COMPLETED: []
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
tasks.forEach(function(t) {
|
|
3060
|
+
if (t.status === 'COMPLETED') {
|
|
3061
|
+
if (t.completedAt && t.completedAt >= sevenAgo) cols.COMPLETED.push(t);
|
|
3062
|
+
} else if (t.status === 'PENDING' && cols[t.category]) {
|
|
3063
|
+
cols[t.category].push(t);
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
|
|
3067
|
+
var idMap = { DO_NOW: 'colDoNow', SCHEDULE: 'colSchedule', DELEGATE: 'colDelegate', COMPLETED: 'colDone' };
|
|
3068
|
+
var countMap = { DO_NOW: 'countDoNow', SCHEDULE: 'countSchedule', DELEGATE: 'countDelegate', COMPLETED: 'countDone' };
|
|
3069
|
+
|
|
3070
|
+
Object.keys(cols).forEach(function(cat) {
|
|
3071
|
+
var el = $(idMap[cat]);
|
|
3072
|
+
var countEl = $(countMap[cat]);
|
|
3073
|
+
if (countEl) countEl.textContent = cols[cat].length;
|
|
3074
|
+
if (!el) return;
|
|
3075
|
+
el.innerHTML = '';
|
|
3076
|
+
|
|
3077
|
+
cols[cat].forEach(function(t) {
|
|
3078
|
+
var card = document.createElement('div');
|
|
3079
|
+
card.className = 'kanban-card';
|
|
3080
|
+
card.draggable = (cat !== 'COMPLETED');
|
|
3081
|
+
card.dataset.taskId = t.id;
|
|
3082
|
+
card.dataset.category = cat;
|
|
3083
|
+
|
|
3084
|
+
var pc = priColor(t.priority);
|
|
3085
|
+
var isOverdue = t.dueDate && t.dueDate < today && t.status === 'PENDING';
|
|
3086
|
+
|
|
3087
|
+
var html = '<div class="kanban-card-header">'
|
|
3088
|
+
+ '<span class="kanban-pri-dot" style="background:' + pc.dot + ';" title="' + escapeHtml(pc.label) + '"></span>'
|
|
3089
|
+
+ '<span class="kanban-card-desc">' + escapeHtml(t.description || '') + '</span>'
|
|
3090
|
+
+ '</div>';
|
|
3091
|
+
|
|
3092
|
+
var meta = [];
|
|
3093
|
+
if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
|
|
3094
|
+
if (t.dueDate) {
|
|
3095
|
+
var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
|
|
3096
|
+
meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
|
|
3097
|
+
}
|
|
3098
|
+
if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
|
|
3099
|
+
|
|
3100
|
+
if (cat !== 'COMPLETED') {
|
|
3101
|
+
html += '<div class="kanban-card-actions">'
|
|
3102
|
+
+ '<button class="kanban-action-btn complete-btn" title="Concluir">\u2713</button>'
|
|
3103
|
+
+ '<button class="kanban-action-btn edit-btn" title="Editar">\u270E</button>'
|
|
3104
|
+
+ '</div>';
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
card.innerHTML = html;
|
|
3108
|
+
|
|
3109
|
+
// Drag events
|
|
3110
|
+
if (cat !== 'COMPLETED') {
|
|
3111
|
+
card.addEventListener('dragstart', function(e) {
|
|
3112
|
+
e.dataTransfer.setData('text/plain', t.id);
|
|
3113
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
3114
|
+
card.classList.add('dragging');
|
|
3115
|
+
});
|
|
3116
|
+
card.addEventListener('dragend', function() {
|
|
3117
|
+
card.classList.remove('dragging');
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
// Complete button
|
|
3121
|
+
var completeBtn = card.querySelector('.complete-btn');
|
|
3122
|
+
if (completeBtn) {
|
|
3123
|
+
completeBtn.onclick = async function() {
|
|
3124
|
+
try {
|
|
3125
|
+
await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
|
|
3126
|
+
showToast('ok', 'Concluida');
|
|
3127
|
+
await loadKanban();
|
|
3128
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// Edit button - inline edit via prompt
|
|
3133
|
+
var editBtn = card.querySelector('.edit-btn');
|
|
3134
|
+
if (editBtn) {
|
|
3135
|
+
editBtn.onclick = async function() {
|
|
3136
|
+
var newCat = prompt('Categoria (DO_NOW|SCHEDULE|DELEGATE):', cat);
|
|
3137
|
+
if (!newCat) return;
|
|
3138
|
+
var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
|
|
3139
|
+
if (newSlug === null) return;
|
|
3140
|
+
var newDue = prompt('Due date (YYYY-MM-DD):', t.dueDate || '');
|
|
3141
|
+
if (newDue === null) return;
|
|
3142
|
+
try {
|
|
3143
|
+
await api('/api/tasks/update', {
|
|
3144
|
+
dir: dirOrDefault(), id: t.id,
|
|
3145
|
+
patch: { category: newCat, projectSlug: newSlug, dueDate: newDue || null }
|
|
3146
|
+
});
|
|
3147
|
+
showToast('ok', 'Atualizada');
|
|
3148
|
+
await loadKanban();
|
|
3149
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
el.appendChild(card);
|
|
3155
|
+
});
|
|
3156
|
+
|
|
3157
|
+
// Drop zone
|
|
3158
|
+
if (cat !== 'COMPLETED') {
|
|
3159
|
+
el.addEventListener('dragover', function(e) {
|
|
3160
|
+
e.preventDefault();
|
|
3161
|
+
e.dataTransfer.dropEffect = 'move';
|
|
3162
|
+
el.classList.add('drag-over');
|
|
3163
|
+
});
|
|
3164
|
+
el.addEventListener('dragleave', function() {
|
|
3165
|
+
el.classList.remove('drag-over');
|
|
3166
|
+
});
|
|
3167
|
+
el.addEventListener('drop', async function(e) {
|
|
3168
|
+
e.preventDefault();
|
|
3169
|
+
el.classList.remove('drag-over');
|
|
3170
|
+
var taskId = e.dataTransfer.getData('text/plain');
|
|
3171
|
+
if (!taskId) return;
|
|
3172
|
+
try {
|
|
3173
|
+
await api('/api/tasks/update', {
|
|
3174
|
+
dir: dirOrDefault(), id: taskId,
|
|
3175
|
+
patch: { category: cat }
|
|
3176
|
+
});
|
|
3177
|
+
showToast('ok', 'Movida para ' + cat);
|
|
3178
|
+
await loadKanban();
|
|
3179
|
+
} catch { showToast('err', 'Falhou'); }
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function renderKanbanBlockers() {
|
|
3186
|
+
var wrap = $('kanbanBlockers');
|
|
3187
|
+
var list = $('kanbanBlockersList');
|
|
3188
|
+
if (!wrap || !list) return;
|
|
3189
|
+
|
|
3190
|
+
var sel = $('kanbanFilterProject');
|
|
3191
|
+
var filter = sel ? sel.value : '';
|
|
3192
|
+
var blockers = _kanbanData.blockers;
|
|
3193
|
+
if (filter) blockers = blockers.filter(function(b) { return b.projectSlug === filter; });
|
|
3194
|
+
|
|
3195
|
+
if (blockers.length === 0) { wrap.style.display = 'none'; return; }
|
|
3196
|
+
wrap.style.display = 'block';
|
|
3197
|
+
list.innerHTML = '';
|
|
3198
|
+
|
|
3199
|
+
blockers.forEach(function(b) {
|
|
3200
|
+
var card = document.createElement('div');
|
|
3201
|
+
card.className = 'kanban-blocker-card';
|
|
3202
|
+
var color = sevColor(b.severity);
|
|
3203
|
+
card.innerHTML = '<span class="kanban-blocker-sev" style="background:' + color + '22; color:' + color + '; border-color:' + color + '44;">' + escapeHtml(b.severity || 'BLOCKER') + '</span>'
|
|
3204
|
+
+ '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
|
|
3205
|
+
+ (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
|
|
3206
|
+
+ (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
|
|
3207
|
+
list.appendChild(card);
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
window.loadKanban = loadKanban;
|
|
3212
|
+
window.filterKanban = filterKanban;
|
|
3213
|
+
window.loadDelta = loadDelta;
|
|
2910
3214
|
})();
|