tina4ruby 0.5.2 → 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +360 -559
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +242 -77
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +43 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1336 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +484 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +337 -31
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +40 -4
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +314 -23
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +134 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +57 -21
- metadata +51 -19
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
let currentTab = 'routes';
|
|
2
|
+
let queueFilter = '';
|
|
3
|
+
let mailboxFolder = '';
|
|
4
|
+
function showTab(tab, e) {
|
|
5
|
+
currentTab = tab;
|
|
6
|
+
document.querySelectorAll('.dev-tab').forEach(t => t.classList.remove('active'));
|
|
7
|
+
document.querySelectorAll('.dev-panel').forEach(p => p.classList.add('hidden'));
|
|
8
|
+
if (e) e.target.closest('.dev-tab').classList.add('active');
|
|
9
|
+
document.getElementById('panel-' + tab).classList.remove('hidden');
|
|
10
|
+
const loaders = {routes:loadRoutes, queue:loadQueue, mailbox:loadMailbox, messages:loadMessages, database:loadTables, requests:loadRequests, errors:loadErrors, websockets:loadWebSockets, system:loadSystem, tools:function(){}};
|
|
11
|
+
if (loaders[tab]) loaders[tab]();
|
|
12
|
+
}
|
|
13
|
+
function api(path, method, body) {
|
|
14
|
+
const opts = { method: method || 'GET', headers: {} };
|
|
15
|
+
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
|
16
|
+
return fetch(path, opts).then(r => r.json());
|
|
17
|
+
}
|
|
18
|
+
function loadRoutes() {
|
|
19
|
+
api('/__dev/api/routes').then(d => {
|
|
20
|
+
document.getElementById('routes-count').textContent = d.count;
|
|
21
|
+
document.getElementById('routes-body').innerHTML = d.routes.map(r => `
|
|
22
|
+
<tr>
|
|
23
|
+
<td><span class="method method-${r.method.toLowerCase()}">${r.method}</span></td>
|
|
24
|
+
<td class="path">${r.path || r.pattern || ''}</td>
|
|
25
|
+
<td>${r.auth_required || r.secure ? '<span class="badge-pill bg-reserved">auth</span>' : '<span class="badge-pill bg-success">open</span>'}</td>
|
|
26
|
+
<td class="text-sm text-muted">${r.handler || ''} ${r.module ? '<small>(' + r.module + ')</small>' : ''}</td>
|
|
27
|
+
</tr>`).join('');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function loadQueue() {
|
|
31
|
+
const qs = queueFilter ? '?status=' + queueFilter : '';
|
|
32
|
+
api('/__dev/api/queue' + qs).then(d => {
|
|
33
|
+
['pending','completed','failed','reserved'].forEach(s => {
|
|
34
|
+
const el = document.getElementById('q-' + s);
|
|
35
|
+
if (el) el.textContent = d.stats[s] || 0;
|
|
36
|
+
});
|
|
37
|
+
document.getElementById('queue-count').textContent = Object.values(d.stats).reduce((a,b) => a+b, 0);
|
|
38
|
+
const tbody = document.getElementById('queue-body');
|
|
39
|
+
const empty = document.getElementById('queue-empty');
|
|
40
|
+
if (!d.jobs.length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
41
|
+
empty.classList.add('hidden');
|
|
42
|
+
tbody.innerHTML = d.jobs.map(j => `
|
|
43
|
+
<tr>
|
|
44
|
+
<td>${j.id}</td>
|
|
45
|
+
<td class="path">${j.topic}</td>
|
|
46
|
+
<td><span class="badge-pill bg-${j.status}">${j.status}</span></td>
|
|
47
|
+
<td>${j.attempts}</td>
|
|
48
|
+
<td class="text-sm text-muted">${j.created_at || ''}</td>
|
|
49
|
+
<td class="text-mono text-sm" style="max-width:250px;overflow:hidden;text-overflow:ellipsis">${typeof j.data === 'object' ? JSON.stringify(j.data) : j.data}</td>
|
|
50
|
+
<td><button class="btn btn-sm" onclick="replayJob(${j.id},'${j.topic}')">Replay</button></td>
|
|
51
|
+
</tr>`).join('');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function filterQueue(status, e) {
|
|
55
|
+
queueFilter = status;
|
|
56
|
+
document.querySelectorAll('#panel-queue .filter-btn').forEach(b => b.classList.remove('active'));
|
|
57
|
+
if (e) e.target.classList.add('active');
|
|
58
|
+
loadQueue();
|
|
59
|
+
}
|
|
60
|
+
function retryQueue() { api('/__dev/api/queue/retry', 'POST', {}).then(() => loadQueue()); }
|
|
61
|
+
function purgeQueue() { api('/__dev/api/queue/purge', 'POST', {}).then(() => loadQueue()); }
|
|
62
|
+
function replayJob(id, topic) { api('/__dev/api/queue/replay', 'POST', {job_id: id, topic: topic}).then(() => { loadQueue(); }); }
|
|
63
|
+
function loadMailbox() {
|
|
64
|
+
const qs = mailboxFolder ? '?folder=' + mailboxFolder : '';
|
|
65
|
+
api('/__dev/api/mailbox' + qs).then(d => {
|
|
66
|
+
document.getElementById('mailbox-count').textContent = d.unread;
|
|
67
|
+
document.getElementById('mail-detail').classList.add('hidden');
|
|
68
|
+
const list = document.getElementById('mailbox-list');
|
|
69
|
+
if (!d.messages.length) { list.innerHTML = '<div class="empty">No messages. Click "Seed 5" to generate test emails.</div>'; return; }
|
|
70
|
+
list.innerHTML = d.messages.map(m => `
|
|
71
|
+
<div class="mail-item ${m.read ? '' : 'unread'}" onclick="readMail('${m.id}')">
|
|
72
|
+
<span class="text-sm text-muted" style="float:right">${(m.date||'').substring(0,16)}</span>
|
|
73
|
+
<div class="text-sm text-muted">${m.from} → ${(m.to||[]).join(', ')}</div>
|
|
74
|
+
<div style="font-weight:600;font-size:0.8rem">${m.subject}</div>
|
|
75
|
+
<span class="badge-pill bg-${m.type === 'inbox' ? 'success' : 'primary'}" style="margin-top:0.2rem">${m.type}</span>
|
|
76
|
+
</div>`).join('');
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function filterMailbox(folder, e) {
|
|
80
|
+
mailboxFolder = folder;
|
|
81
|
+
document.querySelectorAll('#panel-mailbox .filter-btn').forEach(b => b.classList.remove('active'));
|
|
82
|
+
if (e) e.target.classList.add('active');
|
|
83
|
+
loadMailbox();
|
|
84
|
+
}
|
|
85
|
+
function readMail(id) {
|
|
86
|
+
api('/__dev/api/mailbox/read?id=' + id).then(m => {
|
|
87
|
+
const det = document.getElementById('mail-detail');
|
|
88
|
+
det.classList.remove('hidden');
|
|
89
|
+
det.innerHTML = `<h3 style="font-size:0.9rem">${m.subject}</h3>
|
|
90
|
+
<p class="text-sm text-muted">From: ${m.from} | To: ${(m.to||[]).join(', ')} | ${m.date}</p>
|
|
91
|
+
<div style="background:var(--bg);padding:0.75rem;border-radius:var(--radius);margin-top:0.5rem;font-size:0.8rem">${m.html ? m.body : '<pre>' + (m.body||'') + '</pre>'}</div>`;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function seedMailbox() { api('/__dev/api/mailbox/seed', 'POST', {count:5}).then(() => loadMailbox()); }
|
|
95
|
+
function clearMailbox() { api('/__dev/api/mailbox/clear', 'POST', {}).then(() => loadMailbox()); }
|
|
96
|
+
function loadMessages() {
|
|
97
|
+
api('/__dev/api/messages').then(d => {
|
|
98
|
+
document.getElementById('messages-count').textContent = d.counts.total || 0;
|
|
99
|
+
renderMessages(d.messages);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function searchMessages() {
|
|
103
|
+
const q = document.getElementById('msg-search').value.trim();
|
|
104
|
+
if (!q) { loadMessages(); return; }
|
|
105
|
+
api('/__dev/api/messages/search?q=' + encodeURIComponent(q)).then(d => renderMessages(d.messages));
|
|
106
|
+
}
|
|
107
|
+
function renderMessages(messages) {
|
|
108
|
+
const list = document.getElementById('messages-list');
|
|
109
|
+
const empty = document.getElementById('messages-empty');
|
|
110
|
+
if (!messages.length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
111
|
+
empty.classList.add('hidden');
|
|
112
|
+
list.innerHTML = messages.map(m => `
|
|
113
|
+
<div class="msg-entry">
|
|
114
|
+
<span class="time">${(m.timestamp||'').substring(11,19)}</span>
|
|
115
|
+
<span class="cat">${m.category}</span>
|
|
116
|
+
<span class="level-${m.level}">[${m.level}]</span>
|
|
117
|
+
${esc(m.message)}
|
|
118
|
+
${m.data ? '<code class="text-sm text-muted">' + JSON.stringify(m.data) + '</code>' : ''}
|
|
119
|
+
</div>`).join('');
|
|
120
|
+
}
|
|
121
|
+
function clearMessages() { api('/__dev/api/messages/clear', 'POST', {}).then(() => loadMessages()); }
|
|
122
|
+
function loadTables() {
|
|
123
|
+
api('/__dev/api/tables').then(d => {
|
|
124
|
+
const tables = d.tables || [];
|
|
125
|
+
document.getElementById('db-count').textContent = tables.length;
|
|
126
|
+
document.getElementById('table-list').innerHTML = tables.map(t =>
|
|
127
|
+
`<div style="padding:0.2rem 0.4rem;cursor:pointer;border-radius:0.25rem" onclick="browseTable('${t}')" onmouseover="this.style.background='rgba(198,40,40,0.1)'" onmouseout="this.style.background=''">${t}</div>`
|
|
128
|
+
).join('');
|
|
129
|
+
const sel = document.getElementById('seed-table');
|
|
130
|
+
sel.innerHTML = '<option value="">Pick table...</option>' + tables.map(t => `<option value="${t}">${t}</option>`).join('');
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function browseTable(name) { document.getElementById('query-input').value = 'SELECT * FROM ' + name + ' LIMIT 20'; runQuery(); }
|
|
134
|
+
function seedTable() {
|
|
135
|
+
const table = document.getElementById('seed-table').value;
|
|
136
|
+
const count = parseInt(document.getElementById('seed-count').value) || 10;
|
|
137
|
+
if (!table) return;
|
|
138
|
+
api('/__dev/api/seed', 'POST', {table, count}).then(d => {
|
|
139
|
+
if (d.error) { alert(d.error); return; }
|
|
140
|
+
browseTable(table);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function runQuery() {
|
|
144
|
+
const query = document.getElementById('query-input').value.trim();
|
|
145
|
+
const type = document.getElementById('query-type').value;
|
|
146
|
+
const errorEl = document.getElementById('query-error');
|
|
147
|
+
errorEl.classList.add('hidden');
|
|
148
|
+
if (!query) return;
|
|
149
|
+
api('/__dev/api/query', 'POST', {query, type}).then(d => {
|
|
150
|
+
if (d.error) { errorEl.textContent = d.error; errorEl.classList.remove('hidden'); return; }
|
|
151
|
+
const results = document.getElementById('query-results');
|
|
152
|
+
if (d.rows && d.rows.length) {
|
|
153
|
+
const cols = d.columns || Object.keys(d.rows[0]);
|
|
154
|
+
results.innerHTML = `<div class="text-sm text-muted p-sm">${d.count||d.rows.length} rows</div>
|
|
155
|
+
<table><thead><tr>${cols.map(c => '<th>'+c+'</th>').join('')}</tr></thead>
|
|
156
|
+
<tbody>${d.rows.map(r => '<tr>' + cols.map(c => '<td class="text-mono text-sm">' + (r[c]===null?'<span class="text-muted">NULL</span>':esc(String(r[c]))) + '</td>').join('') + '</tr>').join('')}</tbody></table>`;
|
|
157
|
+
} else if (d.data) {
|
|
158
|
+
results.innerHTML = '<pre class="p-md text-mono text-sm">' + JSON.stringify(d.data, null, 2) + '</pre>';
|
|
159
|
+
} else if (d.success) {
|
|
160
|
+
results.innerHTML = '<div class="empty">Query executed. ' + (d.affected||0) + ' rows affected.</div>';
|
|
161
|
+
} else {
|
|
162
|
+
results.innerHTML = '<div class="empty">No results</div>';
|
|
163
|
+
}
|
|
164
|
+
}).catch(e => { errorEl.textContent = e.message; errorEl.classList.remove('hidden'); });
|
|
165
|
+
}
|
|
166
|
+
function loadRequests() {
|
|
167
|
+
api('/__dev/api/requests').then(d => {
|
|
168
|
+
const stats = d.stats || {};
|
|
169
|
+
document.getElementById('req-count').textContent = stats.total || 0;
|
|
170
|
+
document.getElementById('req-stats').innerHTML = `Total: ${stats.total||0} | Avg: ${stats.avg_ms||0}ms | Errors: ${stats.errors||0} | Slowest: ${stats.slowest_ms||0}ms`;
|
|
171
|
+
const tbody = document.getElementById('req-body');
|
|
172
|
+
const empty = document.getElementById('req-empty');
|
|
173
|
+
if (!(d.requests||[]).length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
174
|
+
empty.classList.add('hidden');
|
|
175
|
+
tbody.innerHTML = d.requests.map(r => {
|
|
176
|
+
const sc = r.status >= 500 ? 'status-err' : r.status >= 400 ? 'status-warn' : 'status-ok';
|
|
177
|
+
return `<tr>
|
|
178
|
+
<td class="text-sm text-muted text-mono">${(r.timestamp||'').substring(11,19)}</td>
|
|
179
|
+
<td><span class="method method-${r.method.toLowerCase()}">${r.method}</span></td>
|
|
180
|
+
<td class="path">${r.path}</td>
|
|
181
|
+
<td class="${sc}" style="font-weight:600">${r.status}</td>
|
|
182
|
+
<td class="text-mono text-sm">${r.duration_ms}ms</td>
|
|
183
|
+
<td class="text-sm text-muted">${r.body_size ? r.body_size + 'B' : ''}</td>
|
|
184
|
+
</tr>`;
|
|
185
|
+
}).join('');
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function clearRequests() { api('/__dev/api/requests/clear', 'POST', {}).then(() => loadRequests()); }
|
|
189
|
+
function loadErrors() {
|
|
190
|
+
api('/__dev/api/broken').then(d => {
|
|
191
|
+
const health = d.health || {};
|
|
192
|
+
document.getElementById('err-count').textContent = health.unresolved || 0;
|
|
193
|
+
const list = document.getElementById('errors-list');
|
|
194
|
+
const empty = document.getElementById('errors-empty');
|
|
195
|
+
if (!(d.errors||[]).length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
196
|
+
empty.classList.add('hidden');
|
|
197
|
+
list.innerHTML = d.errors.map(e => `
|
|
198
|
+
<div style="padding:0.6rem 0.75rem;border-bottom:1px solid var(--border)">
|
|
199
|
+
<div class="flex justify-between items-center">
|
|
200
|
+
<span class="badge-pill ${e.resolved ? 'bg-success' : 'bg-danger'}">${e.resolved ? 'resolved' : 'unresolved'}</span>
|
|
201
|
+
<span class="text-sm text-muted">x${e.count} | ${(e.last_seen||'').substring(0,19)}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div style="font-weight:600;font-size:0.8rem;margin-top:0.25rem">${esc(e.error_type)}: ${esc(e.message)}</div>
|
|
204
|
+
${e.traceback ? '<pre class="text-sm text-muted" style="margin-top:0.25rem;max-height:100px;overflow:auto">' + esc(e.traceback) + '</pre>' : ''}
|
|
205
|
+
${!e.resolved ? '<button class="btn btn-sm btn-success" style="margin-top:0.25rem" onclick="resolveError(\'' + e.id + '\')">Resolve</button>' : ''}
|
|
206
|
+
<button class="btn btn-sm btn-primary" style="margin-top:0.25rem;margin-left:0.25rem" data-err="${btoa(e.error_type + ': ' + e.message)}" data-tb="${btoa((e.traceback||'').substring(0,500))}" onclick="askAboutError(this)">Ask Tina4</button>
|
|
207
|
+
</div>`).join('');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function resolveError(id) { api('/__dev/api/broken/resolve', 'POST', {id}).then(() => loadErrors()); }
|
|
211
|
+
function clearResolvedErrors() { api('/__dev/api/broken/clear', 'POST', {}).then(() => loadErrors()); }
|
|
212
|
+
function loadWebSockets() {
|
|
213
|
+
api('/__dev/api/websockets').then(d => {
|
|
214
|
+
document.getElementById('ws-count').textContent = d.count || 0;
|
|
215
|
+
const tbody = document.getElementById('ws-body');
|
|
216
|
+
const empty = document.getElementById('ws-empty');
|
|
217
|
+
if (!(d.connections||[]).length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
218
|
+
empty.classList.add('hidden');
|
|
219
|
+
tbody.innerHTML = d.connections.map(c => `
|
|
220
|
+
<tr>
|
|
221
|
+
<td class="text-mono text-sm">${c.id}</td>
|
|
222
|
+
<td class="path">${c.path}</td>
|
|
223
|
+
<td class="text-sm text-muted">${c.ip}</td>
|
|
224
|
+
<td class="text-sm text-muted">${(c.connected_at||'').substring(11,19)}</td>
|
|
225
|
+
<td><span class="badge-pill ${c.closed ? 'bg-danger' : 'bg-success'}">${c.closed ? 'closed' : 'active'}</span></td>
|
|
226
|
+
<td>${!c.closed ? '<button class="btn btn-sm btn-danger" onclick="wsDisconnect(\'' + c.id + '\')">Disconnect</button>' : ''}</td>
|
|
227
|
+
</tr>`).join('');
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function wsDisconnect(id) { api('/__dev/api/websockets/disconnect', 'POST', {id}).then(() => loadWebSockets()); }
|
|
231
|
+
function loadSystem() {
|
|
232
|
+
api('/__dev/api/system').then(d => {
|
|
233
|
+
const rubyVersion = d.ruby_version || '';
|
|
234
|
+
const phpVersion = d.php_version || '';
|
|
235
|
+
const pythonVersion = (d.python_version || '').split(' ')[0] || '';
|
|
236
|
+
const runtimeLabel = rubyVersion ? 'Ruby' : phpVersion ? 'PHP' : 'Python';
|
|
237
|
+
const runtimeVersion = rubyVersion || phpVersion || pythonVersion || 'N/A';
|
|
238
|
+
const platform = d.os || d.platform || '';
|
|
239
|
+
const arch = d.architecture || '';
|
|
240
|
+
const memCurrent = d.memory ? d.memory.current_mb + ' MB' : (d.memory_mb ? d.memory_mb + ' MB' : 'N/A');
|
|
241
|
+
const memPeak = d.memory ? d.memory.peak_mb + ' MB' : 'N/A';
|
|
242
|
+
const memLimit = d.memory ? d.memory.limit : 'N/A';
|
|
243
|
+
const sapi = d.php_sapi || '';
|
|
244
|
+
const hostname = d.server ? d.server.hostname : '';
|
|
245
|
+
const fwName = d.framework ? (typeof d.framework === 'object' ? d.framework.name : d.framework) : '';
|
|
246
|
+
const fwVersion = d.framework ? (typeof d.framework === 'object' ? d.framework.version : '') : '';
|
|
247
|
+
const routeCount = d.framework ? (typeof d.framework === 'object' ? d.framework.route_count : '') : (d.route_count || '');
|
|
248
|
+
const extensions = d.extensions || [];
|
|
249
|
+
let html = `
|
|
250
|
+
<div class="sys-card"><div class="label">${runtimeLabel}</div><div class="value text-sm">${runtimeVersion}</div></div>
|
|
251
|
+
<div class="sys-card"><div class="label">Platform</div><div class="value text-sm">${platform}</div></div>
|
|
252
|
+
<div class="sys-card"><div class="label">Architecture</div><div class="value text-sm">${arch}</div></div>
|
|
253
|
+
<div class="sys-card"><div class="label">Memory (Current)</div><div class="value">${memCurrent}</div></div>
|
|
254
|
+
<div class="sys-card"><div class="label">Memory (Peak)</div><div class="value">${memPeak}</div></div>
|
|
255
|
+
<div class="sys-card"><div class="label">Memory Limit</div><div class="value text-sm">${memLimit}</div></div>`;
|
|
256
|
+
if (sapi) html += `<div class="sys-card"><div class="label">SAPI</div><div class="value text-sm">${sapi}</div></div>`;
|
|
257
|
+
if (hostname) html += `<div class="sys-card"><div class="label">Hostname</div><div class="value text-sm">${hostname}</div></div>`;
|
|
258
|
+
if (fwName) html += `<div class="sys-card"><div class="label">Framework</div><div class="value text-sm">${fwName}</div></div>`;
|
|
259
|
+
if (fwVersion) html += `<div class="sys-card"><div class="label">Version</div><div class="value text-sm">${fwVersion}</div></div>`;
|
|
260
|
+
if (routeCount !== '') html += `<div class="sys-card"><div class="label">Routes</div><div class="value">${routeCount}</div></div>`;
|
|
261
|
+
if (d.ini) {
|
|
262
|
+
html += `<div class="sys-card"><div class="label">Max Exec Time</div><div class="value text-sm">${d.ini.max_execution_time}s</div></div>`;
|
|
263
|
+
html += `<div class="sys-card"><div class="label">Upload Max</div><div class="value text-sm">${d.ini.upload_max_filesize}</div></div>`;
|
|
264
|
+
html += `<div class="sys-card"><div class="label">Post Max</div><div class="value text-sm">${d.ini.post_max_size}</div></div>`;
|
|
265
|
+
}
|
|
266
|
+
if (d.ruby_engine) html += `<div class="sys-card"><div class="label">Engine</div><div class="value text-sm">${d.ruby_engine}</div></div>`;
|
|
267
|
+
if (d.gc) {
|
|
268
|
+
html += `<div class="sys-card"><div class="label">GC Runs</div><div class="value text-sm">${d.gc.count}</div></div>`;
|
|
269
|
+
html += `<div class="sys-card"><div class="label">Heap Live Slots</div><div class="value text-sm">${d.gc.heap_live_slots}</div></div>`;
|
|
270
|
+
}
|
|
271
|
+
if (d.thread_count) html += `<div class="sys-card"><div class="label">Threads</div><div class="value text-sm">${d.thread_count}</div></div>`;
|
|
272
|
+
if (d.pid) html += `<div class="sys-card"><div class="label">PID</div><div class="value text-sm">${d.pid}</div></div>`;
|
|
273
|
+
if (d.debug_level) html += `<div class="sys-card"><div class="label">Debug Level</div><div class="value text-sm">${d.debug_level}</div></div>`;
|
|
274
|
+
if (d.db_tables !== undefined) html += `<div class="sys-card"><div class="label">DB Tables</div><div class="value">${d.db_tables}</div></div>`;
|
|
275
|
+
if (d.db_connected !== undefined) html += `<div class="sys-card"><div class="label">DB Connected</div><div class="value">${d.db_connected ? '<span style="color:var(--success)">Yes</span>' : '<span style="color:var(--danger)">No</span>'}</div></div>`;
|
|
276
|
+
if (d.loaded_modules) html += `<div class="sys-card"><div class="label">Modules</div><div class="value">${d.loaded_modules}</div></div>`;
|
|
277
|
+
document.getElementById('sys-cards').innerHTML = html;
|
|
278
|
+
const extContainer = document.getElementById('sys-extensions');
|
|
279
|
+
if (extContainer && extensions.length) {
|
|
280
|
+
extContainer.classList.remove('hidden');
|
|
281
|
+
extContainer.innerHTML = '<div class="dev-panel-header"><h2>Loaded Extensions (' + extensions.length + ')</h2></div><div style="display:flex;flex-wrap:wrap;gap:0.35rem;padding:0.75rem">' +
|
|
282
|
+
extensions.sort().map(e => '<span style="background:rgba(198,40,40,0.15);color:#ef9a9a;padding:0.15rem 0.5rem;border-radius:0.75rem;font-size:0.7rem">' + e + '</span>').join('') + '</div>';
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
let _aiKey = '';
|
|
287
|
+
let _aiProvider = 'anthropic';
|
|
288
|
+
function setAiKey() {
|
|
289
|
+
_aiKey = document.getElementById('ai-key').value.trim();
|
|
290
|
+
_aiProvider = document.getElementById('ai-provider').value;
|
|
291
|
+
document.getElementById('ai-key').value = '';
|
|
292
|
+
document.getElementById('ai-status').textContent = _aiKey ? (_aiProvider === 'anthropic' ? 'Claude key set' : 'OpenAI key set') : 'No key set';
|
|
293
|
+
document.getElementById('ai-status').style.color = _aiKey ? 'var(--success)' : 'var(--muted)';
|
|
294
|
+
}
|
|
295
|
+
function sendChat() {
|
|
296
|
+
const input = document.getElementById('chat-input');
|
|
297
|
+
const msg = input.value.trim();
|
|
298
|
+
if (!msg) return;
|
|
299
|
+
input.value = '';
|
|
300
|
+
const container = document.getElementById('chat-messages');
|
|
301
|
+
container.innerHTML += `<div class="chat-msg chat-user">${esc(msg)}</div>`;
|
|
302
|
+
container.innerHTML += `<div class="chat-msg chat-bot" id="chat-loading" style="color:var(--muted)">Thinking...</div>`;
|
|
303
|
+
container.scrollTop = container.scrollHeight;
|
|
304
|
+
const body = {message: msg, provider: _aiProvider};
|
|
305
|
+
if (_aiKey) body.api_key = _aiKey;
|
|
306
|
+
api('/__dev/api/chat', 'POST', body).then(d => {
|
|
307
|
+
const loading = document.getElementById('chat-loading');
|
|
308
|
+
if (loading) loading.remove();
|
|
309
|
+
container.innerHTML += `<div class="chat-msg chat-bot">${formatChat(d.reply||'No response')}</div>`;
|
|
310
|
+
container.scrollTop = container.scrollHeight;
|
|
311
|
+
}).catch(() => {
|
|
312
|
+
const loading = document.getElementById('chat-loading');
|
|
313
|
+
if (loading) { loading.textContent = 'Error connecting to API'; loading.id = ''; }
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function formatChat(text) {
|
|
317
|
+
return text.replace(/`([^`]+)`/g, '<code style="background:var(--surface);padding:0.1rem 0.25rem;border-radius:0.2rem;font-size:0.8em">$1</code>')
|
|
318
|
+
.replace(/\n/g, '<br>');
|
|
319
|
+
}
|
|
320
|
+
function askAboutError(btn) {
|
|
321
|
+
const error = atob(btn.dataset.err);
|
|
322
|
+
const trace = atob(btn.dataset.tb);
|
|
323
|
+
currentTab = 'chat';
|
|
324
|
+
document.querySelectorAll('.dev-tab').forEach(t => t.classList.remove('active'));
|
|
325
|
+
document.querySelectorAll('.dev-panel').forEach(p => p.classList.add('hidden'));
|
|
326
|
+
document.querySelectorAll('.dev-tab').forEach(t => { if(t.textContent.includes('Tina4')) t.classList.add('active'); });
|
|
327
|
+
document.getElementById('panel-chat').classList.remove('hidden');
|
|
328
|
+
const msg = 'I have this error in my Tina4 app, help me fix it:\n\n' + error + '\n\nStack trace:\n' + trace;
|
|
329
|
+
document.getElementById('chat-input').value = msg;
|
|
330
|
+
sendChat();
|
|
331
|
+
}
|
|
332
|
+
function runTool(tool) {
|
|
333
|
+
const titles = {carbon:'Carbon Benchmark',test:'Test Suite',routes:'Routes',migrate:'Migrations',seed:'Seeders',ai:'AI Detection'};
|
|
334
|
+
document.getElementById('tool-title').textContent = titles[tool] || tool;
|
|
335
|
+
document.getElementById('tool-result').textContent = 'Running...';
|
|
336
|
+
document.getElementById('tool-output').classList.remove('hidden');
|
|
337
|
+
api('/__dev/api/tool', 'POST', {tool}).then(d => {
|
|
338
|
+
document.getElementById('tool-result').textContent = d.output || d.error || JSON.stringify(d, null, 2);
|
|
339
|
+
}).catch(e => {
|
|
340
|
+
document.getElementById('tool-result').textContent = 'Error: ' + e.message;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function exitDevAdmin() {
|
|
344
|
+
if (document.referrer && !document.referrer.includes('/__dev')) {
|
|
345
|
+
window.location.href = document.referrer;
|
|
346
|
+
} else if (window.history.length > 1) {
|
|
347
|
+
window.history.back();
|
|
348
|
+
} else {
|
|
349
|
+
window.location.href = '/';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
353
|
+
document.addEventListener('keydown', e => {
|
|
354
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && currentTab === 'database') { e.preventDefault(); runQuery(); }
|
|
355
|
+
});
|
|
356
|
+
function updateTimestamp() { document.getElementById('timestamp').textContent = new Date().toLocaleTimeString(); }
|
|
357
|
+
setInterval(updateTimestamp, 1000);
|
|
358
|
+
updateTimestamp();
|
|
359
|
+
loadRoutes();
|
|
360
|
+
api('/__dev/api/status').then(d => {
|
|
361
|
+
if (d.mailbox) document.getElementById('mailbox-count').textContent = d.mailbox.total || 0;
|
|
362
|
+
if (d.messages) document.getElementById('messages-count').textContent = d.messages.total || 0;
|
|
363
|
+
if (d.health) document.getElementById('err-count').textContent = d.health.unresolved || 0;
|
|
364
|
+
if (d.requests) document.getElementById('req-count').textContent = d.requests.total || 0;
|
|
365
|
+
if (d.message_counts) document.getElementById('messages-count').textContent = d.message_counts.total || 0;
|
|
366
|
+
if (d.request_stats) document.getElementById('req-count').textContent = d.request_stats.total || 0;
|
|
367
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
function getModalEl(selector) {
|
|
4
|
+
if (!selector) return null;
|
|
5
|
+
return document.querySelector(selector);
|
|
6
|
+
}
|
|
7
|
+
function openModal(modal) {
|
|
8
|
+
if (!modal) return;
|
|
9
|
+
var backdrop = modal._t4Backdrop;
|
|
10
|
+
if (!backdrop) {
|
|
11
|
+
backdrop = document.createElement("div");
|
|
12
|
+
backdrop.className = "modal-backdrop";
|
|
13
|
+
document.body.appendChild(backdrop);
|
|
14
|
+
modal._t4Backdrop = backdrop;
|
|
15
|
+
backdrop.addEventListener("click", function () {
|
|
16
|
+
closeModal(modal);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
modal.style.display = "block";
|
|
20
|
+
backdrop.style.display = "block";
|
|
21
|
+
void modal.offsetHeight;
|
|
22
|
+
modal.classList.add("show");
|
|
23
|
+
backdrop.classList.add("show");
|
|
24
|
+
document.body.style.overflow = "hidden";
|
|
25
|
+
var focusable = modal.querySelector("input, select, textarea, button, [tabindex]");
|
|
26
|
+
if (focusable) focusable.focus();
|
|
27
|
+
}
|
|
28
|
+
function closeModal(modal) {
|
|
29
|
+
if (!modal) return;
|
|
30
|
+
modal.classList.remove("show");
|
|
31
|
+
var backdrop = modal._t4Backdrop;
|
|
32
|
+
if (backdrop) backdrop.classList.remove("show");
|
|
33
|
+
setTimeout(function () {
|
|
34
|
+
modal.style.display = "none";
|
|
35
|
+
if (backdrop) backdrop.style.display = "none";
|
|
36
|
+
document.body.style.overflow = "";
|
|
37
|
+
}, 150);
|
|
38
|
+
}
|
|
39
|
+
document.addEventListener("click", function (e) {
|
|
40
|
+
var trigger = e.target.closest("[data-t4-toggle='modal'], [data-bs-toggle='modal']");
|
|
41
|
+
if (trigger) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
var target = trigger.getAttribute("data-t4-target") || trigger.getAttribute("data-bs-target") || trigger.getAttribute("href");
|
|
44
|
+
var modal = getModalEl(target);
|
|
45
|
+
if (modal) openModal(modal);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
var dismiss = e.target.closest("[data-t4-dismiss='modal'], [data-bs-dismiss='modal'], .btn-close");
|
|
49
|
+
if (dismiss) {
|
|
50
|
+
var modal = dismiss.closest(".modal");
|
|
51
|
+
if (modal) closeModal(modal);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
document.addEventListener("keydown", function (e) {
|
|
56
|
+
if (e.key === "Escape") {
|
|
57
|
+
var modals = document.querySelectorAll(".modal.show");
|
|
58
|
+
if (modals.length > 0) closeModal(modals[modals.length - 1]);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
document.addEventListener("click", function (e) {
|
|
62
|
+
var dismiss = e.target.closest("[data-t4-dismiss='alert'], [data-bs-dismiss='alert']");
|
|
63
|
+
if (dismiss) {
|
|
64
|
+
var alert = dismiss.closest(".alert");
|
|
65
|
+
if (alert) {
|
|
66
|
+
alert.style.opacity = "0";
|
|
67
|
+
setTimeout(function () { alert.remove(); }, 150);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
document.addEventListener("click", function (e) {
|
|
72
|
+
var toggler = e.target.closest("[data-t4-toggle='collapse'], [data-bs-toggle='collapse']");
|
|
73
|
+
if (toggler) {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
var target = toggler.getAttribute("data-t4-target") || toggler.getAttribute("data-bs-target") || toggler.getAttribute("href");
|
|
76
|
+
var el = document.querySelector(target);
|
|
77
|
+
if (el) {
|
|
78
|
+
el.classList.toggle("show");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
window.tina4 = window.tina4 || {};
|
|
83
|
+
window.tina4.modal = {
|
|
84
|
+
open: function (selector) {
|
|
85
|
+
var modal = typeof selector === "string" ? document.querySelector(selector) : selector;
|
|
86
|
+
openModal(modal);
|
|
87
|
+
},
|
|
88
|
+
close: function (selector) {
|
|
89
|
+
var modal = typeof selector === "string" ? document.querySelector(selector) : selector;
|
|
90
|
+
closeModal(modal);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
})();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Swagger UI</title>
|
|
6
|
+
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
|
|
7
|
+
rel="stylesheet">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.min.css" integrity="sha512-+9UD8YSD9GF7FzOH38L9S6y56aYNx3R4dYbOCgvTJ2ZHpJScsahNdaMQJU/8osUiz9FPu0YZ8wdKf4evUbsGSg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
9
|
+
<style>
|
|
10
|
+
html {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
overflow: -moz-scrollbars-vertical;
|
|
13
|
+
overflow-y: scroll;
|
|
14
|
+
}
|
|
15
|
+
*,
|
|
16
|
+
*:before,
|
|
17
|
+
*:after {
|
|
18
|
+
box-sizing: inherit;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
background: #fafafa;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
|
|
28
|
+
<body>
|
|
29
|
+
|
|
30
|
+
<svg style="position:absolute;width:0;height:0"
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
32
|
+
<defs>
|
|
33
|
+
<symbol id="unlocked" viewBox="0 0 20 20">
|
|
34
|
+
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
|
|
35
|
+
</symbol>
|
|
36
|
+
|
|
37
|
+
<symbol id="locked" viewBox="0 0 20 20">
|
|
38
|
+
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
|
|
39
|
+
</symbol>
|
|
40
|
+
|
|
41
|
+
<symbol id="close" viewBox="0 0 20 20">
|
|
42
|
+
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
|
|
43
|
+
</symbol>
|
|
44
|
+
|
|
45
|
+
<symbol id="large-arrow" viewBox="0 0 20 20">
|
|
46
|
+
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
|
|
47
|
+
</symbol>
|
|
48
|
+
|
|
49
|
+
<symbol id="large-arrow-down" viewBox="0 0 20 20">
|
|
50
|
+
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
|
51
|
+
</symbol>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<symbol id="jump-to" viewBox="0 0 24 24">
|
|
55
|
+
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
|
56
|
+
</symbol>
|
|
57
|
+
|
|
58
|
+
<symbol id="expand" viewBox="0 0 24 24">
|
|
59
|
+
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
|
|
60
|
+
</symbol>
|
|
61
|
+
|
|
62
|
+
</defs>
|
|
63
|
+
</svg>
|
|
64
|
+
|
|
65
|
+
<div id="swagger-ui"></div>
|
|
66
|
+
|
|
67
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.js" integrity="sha512-mVvFSCxt0sK0FeL8C7n8BcHh10quzdwfxQbjRaw9pRdKNNep3YQusJS5e2/q4GYt4Ma5yWXSJraoQzXPgZd2EQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
68
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.min.js" integrity="sha512-UrYi+60Ci3WWWcoDXbMmzpoi1xpERbwjPGij6wTh8fXl81qNdioNNHExr9ttnBebKF0ZbVnPlTPlw+zECUK1Xw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
69
|
+
<script>
|
|
70
|
+
window.onload = function () {
|
|
71
|
+
|
|
72
|
+
// Build a system
|
|
73
|
+
const ui = SwaggerUIBundle({
|
|
74
|
+
url: "{SWAGGER_ROUTE}/swagger.json",
|
|
75
|
+
dom_id: '#swagger-ui',
|
|
76
|
+
deepLinking: true,
|
|
77
|
+
presets: [
|
|
78
|
+
SwaggerUIBundle.presets.apis
|
|
79
|
+
],
|
|
80
|
+
plugins: [
|
|
81
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
window.ui = ui
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
</body>
|
|
89
|
+
|
|
90
|
+
</html>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<body onload="run()">
|
|
4
|
+
</body>
|
|
5
|
+
</html>
|
|
6
|
+
<script>
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
function run() {
|
|
10
|
+
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
|
11
|
+
var sentState = oauth2.state;
|
|
12
|
+
var redirectUrl = oauth2.redirectUrl;
|
|
13
|
+
var isValid, qp, arr;
|
|
14
|
+
|
|
15
|
+
if (/code|token|error/.test(window.location.hash)) {
|
|
16
|
+
qp = window.location.hash.substring(1);
|
|
17
|
+
} else {
|
|
18
|
+
qp = location.search.substring(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
arr = qp.split("&")
|
|
22
|
+
arr.forEach(function (v, i, _arr) {
|
|
23
|
+
_arr[i] = '"' + v.replace('=', '":"') + '"';
|
|
24
|
+
})
|
|
25
|
+
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
|
26
|
+
function (key, value) {
|
|
27
|
+
return key === "" ? value : decodeURIComponent(value)
|
|
28
|
+
}
|
|
29
|
+
) : {}
|
|
30
|
+
|
|
31
|
+
isValid = qp.state === sentState
|
|
32
|
+
|
|
33
|
+
if ((
|
|
34
|
+
oauth2.auth.schema.get("flow") === "accessCode" ||
|
|
35
|
+
oauth2.auth.schema.get("flow") === "authorizationCode"
|
|
36
|
+
) && !oauth2.auth.code) {
|
|
37
|
+
if (!isValid) {
|
|
38
|
+
oauth2.errCb({
|
|
39
|
+
authId: oauth2.auth.name,
|
|
40
|
+
source: "auth",
|
|
41
|
+
level: "warning",
|
|
42
|
+
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (qp.code) {
|
|
47
|
+
delete oauth2.state;
|
|
48
|
+
oauth2.auth.code = qp.code;
|
|
49
|
+
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
|
50
|
+
} else {
|
|
51
|
+
oauth2.errCb({
|
|
52
|
+
authId: oauth2.auth.name,
|
|
53
|
+
source: "auth",
|
|
54
|
+
level: "error",
|
|
55
|
+
message: "Authorization failed: no accessCode received from the server"
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
|
60
|
+
}
|
|
61
|
+
window.close();
|
|
62
|
+
}
|
|
63
|
+
</script>
|