dinorex 1.0.2 → 1.0.3
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/dist/cli.js +0 -0
- package/dist/public/index.html +614 -0
- package/dist/public/public/index.html +614 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Dinorex</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0f1117;
|
|
12
|
+
--panel: #161b27;
|
|
13
|
+
--surface: #1c2333;
|
|
14
|
+
--surface2: #212840;
|
|
15
|
+
--border: #2a3149;
|
|
16
|
+
--border2: #323d5c;
|
|
17
|
+
--accent: #00c46a;
|
|
18
|
+
--accent-dim: rgba(0,196,106,0.1);
|
|
19
|
+
--accent2: #3b82f6;
|
|
20
|
+
--gold: #f59e0b;
|
|
21
|
+
--text: #e2e8f0;
|
|
22
|
+
--text-mid: #94a3b8;
|
|
23
|
+
--text-muted: #475569;
|
|
24
|
+
--GET: #10b981; --GET-bg: rgba(16,185,129,0.12);
|
|
25
|
+
--POST: #f97316; --POST-bg: rgba(249,115,22,0.12);
|
|
26
|
+
--PUT: #eab308; --PUT-bg: rgba(234,179,8,0.12);
|
|
27
|
+
--PATCH: #a78bfa; --PATCH-bg: rgba(167,139,250,0.12);
|
|
28
|
+
--DELETE: #ef4444; --DELETE-bg: rgba(239,68,68,0.12);
|
|
29
|
+
--sidebar-w: 260px;
|
|
30
|
+
--header-h: 52px;
|
|
31
|
+
}
|
|
32
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
+
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; font-size: 13px; }
|
|
34
|
+
|
|
35
|
+
/* ── Header ── */
|
|
36
|
+
header {
|
|
37
|
+
height: var(--header-h);
|
|
38
|
+
background: var(--panel);
|
|
39
|
+
border-bottom: 1px solid var(--border);
|
|
40
|
+
display: flex; align-items: center;
|
|
41
|
+
padding: 0 1rem; gap: 0.75rem;
|
|
42
|
+
flex-shrink: 0; z-index: 100;
|
|
43
|
+
}
|
|
44
|
+
.logo { display: flex; align-items: center; gap: 8px; cursor: pointer; text-decoration: none; }
|
|
45
|
+
.logo-dino { font-size: 1.2rem; }
|
|
46
|
+
.logo-name { font-size: 0.95rem; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
|
|
47
|
+
.logo-name span { color: var(--accent); }
|
|
48
|
+
|
|
49
|
+
.hdr-divider { width: 1px; height: 20px; background: var(--border); }
|
|
50
|
+
.hdr-project { font-size: 0.8rem; color: var(--text-mid); font-weight: 500; }
|
|
51
|
+
.hdr-spacer { flex: 1; }
|
|
52
|
+
|
|
53
|
+
.status-pill {
|
|
54
|
+
display: flex; align-items: center; gap: 5px;
|
|
55
|
+
font-size: 0.72rem; font-weight: 500;
|
|
56
|
+
color: var(--text-muted); padding: 3px 10px;
|
|
57
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 20px;
|
|
58
|
+
}
|
|
59
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
|
60
|
+
.status-pill.ready .status-dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
|
|
61
|
+
.status-pill.ready { color: var(--text-mid); }
|
|
62
|
+
.status-pill.analyzing .status-dot { background: var(--gold); animation: blink 1s ease-in-out infinite; }
|
|
63
|
+
.status-pill.error .status-dot { background: var(--DELETE); }
|
|
64
|
+
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
|
|
65
|
+
|
|
66
|
+
.hdr-btn {
|
|
67
|
+
display: flex; align-items: center; gap: 5px;
|
|
68
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
69
|
+
color: var(--text-mid); font-family: 'Inter', sans-serif;
|
|
70
|
+
font-size: 0.75rem; font-weight: 500;
|
|
71
|
+
padding: 5px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
|
72
|
+
}
|
|
73
|
+
.hdr-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
|
|
74
|
+
.hdr-btn svg { width: 13px; height: 13px; }
|
|
75
|
+
|
|
76
|
+
/* ── Layout ── */
|
|
77
|
+
.app-body { display: flex; flex: 1; overflow: hidden; }
|
|
78
|
+
|
|
79
|
+
/* ── Sidebar ── */
|
|
80
|
+
nav {
|
|
81
|
+
width: var(--sidebar-w); flex-shrink: 0;
|
|
82
|
+
background: var(--panel); border-right: 1px solid var(--border);
|
|
83
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-header {
|
|
87
|
+
padding: 0.6rem 0.75rem;
|
|
88
|
+
border-bottom: 1px solid var(--border);
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.search-wrap { position: relative; }
|
|
93
|
+
.search-icon { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; }
|
|
94
|
+
.search-input {
|
|
95
|
+
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
|
96
|
+
border-radius: 6px; padding: 5px 8px 5px 28px;
|
|
97
|
+
color: var(--text); font-family: 'Inter', sans-serif; font-size: 0.78rem;
|
|
98
|
+
outline: none; transition: border-color 0.15s;
|
|
99
|
+
}
|
|
100
|
+
.search-input:focus { border-color: var(--accent2); }
|
|
101
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
102
|
+
|
|
103
|
+
.nav-list { overflow-y: auto; flex: 1; padding: 0.5rem 0 2rem; }
|
|
104
|
+
.nav-list::-webkit-scrollbar { width: 3px; }
|
|
105
|
+
.nav-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
106
|
+
|
|
107
|
+
.nav-group { margin-bottom: 0.25rem; }
|
|
108
|
+
.nav-group-header {
|
|
109
|
+
display: flex; align-items: center; gap: 6px;
|
|
110
|
+
padding: 0.4rem 0.75rem; cursor: pointer;
|
|
111
|
+
color: var(--text-mid); font-size: 0.78rem; font-weight: 600;
|
|
112
|
+
user-select: none;
|
|
113
|
+
}
|
|
114
|
+
.nav-group-header:hover { color: var(--text); }
|
|
115
|
+
.nav-group-icon { color: var(--text-muted); font-size: 0.65rem; transition: transform 0.15s; }
|
|
116
|
+
.nav-group.open .nav-group-icon { transform: rotate(90deg); }
|
|
117
|
+
.nav-group-name { flex: 1; }
|
|
118
|
+
.nav-group-count { font-size: 0.65rem; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 10px; }
|
|
119
|
+
|
|
120
|
+
.nav-group-items { display: none; }
|
|
121
|
+
.nav-group.open .nav-group-items { display: block; }
|
|
122
|
+
|
|
123
|
+
.nav-item {
|
|
124
|
+
display: flex; align-items: center; gap: 7px;
|
|
125
|
+
padding: 0.35rem 0.75rem 0.35rem 1.5rem;
|
|
126
|
+
cursor: pointer; transition: all 0.1s;
|
|
127
|
+
border-left: 2px solid transparent;
|
|
128
|
+
}
|
|
129
|
+
.nav-item:hover { background: var(--surface); }
|
|
130
|
+
.nav-item.active { background: var(--accent-dim); border-left-color: var(--accent); }
|
|
131
|
+
.nav-item.active .nav-path { color: var(--text); }
|
|
132
|
+
|
|
133
|
+
.m-pill {
|
|
134
|
+
font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 600;
|
|
135
|
+
padding: 1px 5px; border-radius: 3px; min-width: 42px; text-align: center; flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
.m-GET { color: var(--GET); background: var(--GET-bg); }
|
|
138
|
+
.m-POST { color: var(--POST); background: var(--POST-bg); }
|
|
139
|
+
.m-PUT { color: var(--PUT); background: var(--PUT-bg); }
|
|
140
|
+
.m-PATCH { color: var(--PATCH); background: var(--PATCH-bg); }
|
|
141
|
+
.m-DELETE { color: var(--DELETE); background: var(--DELETE-bg); }
|
|
142
|
+
|
|
143
|
+
.nav-path { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
144
|
+
|
|
145
|
+
/* ── Main ── */
|
|
146
|
+
main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; background: var(--bg); }
|
|
147
|
+
main::-webkit-scrollbar { width: 4px; }
|
|
148
|
+
main::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
149
|
+
|
|
150
|
+
/* ── Overview ── */
|
|
151
|
+
.overview { padding: 2.5rem 3rem; max-width: 800px; }
|
|
152
|
+
.ov-eyebrow { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.5rem; }
|
|
153
|
+
.ov-title { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.5rem; }
|
|
154
|
+
.ov-desc { color: var(--text-mid); font-size: 0.85rem; line-height: 1.6; margin-bottom: 2rem; }
|
|
155
|
+
.ov-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 2rem; }
|
|
156
|
+
.ov-stat { background: var(--panel); padding: 1rem 1.25rem; }
|
|
157
|
+
.ov-stat-n { font-size: 1.5rem; font-weight: 700; color: var(--accent); font-family: 'JetBrains Mono', monospace; }
|
|
158
|
+
.ov-stat-l { font-size: 0.68rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 2px; }
|
|
159
|
+
.ov-base { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--accent2); background: var(--surface); padding: 0.6rem 1rem; border-radius: 6px; border: 1px solid var(--border); display: inline-block; margin-bottom: 2rem; }
|
|
160
|
+
|
|
161
|
+
.ov-cols-title { font-size: 0.78rem; font-weight: 600; color: var(--text-mid); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.75rem; }
|
|
162
|
+
.ov-col-row { display: flex; align-items: center; gap: 1rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.85rem 1.1rem; cursor: pointer; transition: all 0.15s; margin-bottom: 0.5rem; }
|
|
163
|
+
.ov-col-row:hover { border-color: var(--border2); background: var(--surface); }
|
|
164
|
+
.ov-col-icon { width: 32px; height: 32px; background: var(--surface2); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.9rem; flex-shrink: 0; }
|
|
165
|
+
.ov-col-name { font-weight: 600; font-size: 0.85rem; }
|
|
166
|
+
.ov-col-desc { font-size: 0.78rem; color: var(--text-mid); flex: 1; }
|
|
167
|
+
.ov-col-count { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--accent); background: var(--accent-dim); padding: 2px 8px; border-radius: 10px; flex-shrink: 0; }
|
|
168
|
+
|
|
169
|
+
/* ── Endpoint view ── */
|
|
170
|
+
.ep-view { display: flex; flex-direction: column; height: 100%; }
|
|
171
|
+
|
|
172
|
+
.ep-topbar { flex-shrink: 0; padding: 1.5rem 2rem 0; background: var(--bg); }
|
|
173
|
+
.ep-topbar-row1 { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
|
|
174
|
+
.ep-method-badge { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 700; padding: 4px 10px; border-radius: 5px; }
|
|
175
|
+
.ep-path-text { font-family: 'JetBrains Mono', monospace; font-size: 1rem; font-weight: 500; color: var(--text); }
|
|
176
|
+
.ep-auth-tag { background: rgba(245,158,11,0.1); color: var(--gold); border: 1px solid rgba(245,158,11,0.2); font-size: 0.68rem; padding: 2px 8px; border-radius: 20px; display: flex; align-items: center; gap: 4px; }
|
|
177
|
+
.ep-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.25rem; }
|
|
178
|
+
.ep-desc { font-size: 0.83rem; color: var(--text-mid); line-height: 1.55; margin-bottom: 1rem; }
|
|
179
|
+
|
|
180
|
+
.ep-tabs { flex-shrink: 0; display: flex; border-bottom: 1px solid var(--border); padding: 0 2rem; background: var(--bg); }
|
|
181
|
+
.tab { font-size: 0.78rem; font-weight: 500; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); padding: 0.65rem 1rem; cursor: pointer; transition: all 0.15s; margin-bottom: -1px; }
|
|
182
|
+
.tab:hover { color: var(--text-mid); }
|
|
183
|
+
.tab.on { color: var(--text); border-bottom-color: var(--accent2); }
|
|
184
|
+
|
|
185
|
+
.ep-content { flex: 1; overflow-y: auto; padding: 1.5rem 2rem; }
|
|
186
|
+
.ep-content::-webkit-scrollbar { width: 4px; }
|
|
187
|
+
.ep-content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
188
|
+
|
|
189
|
+
/* ── Env bar (like the screenshot) ── */
|
|
190
|
+
.env-bar {
|
|
191
|
+
position: sticky; top: 0; z-index: 10;
|
|
192
|
+
background: var(--bg); border-bottom: 1px solid var(--border);
|
|
193
|
+
padding: 0.6rem 2rem; display: flex; align-items: center; gap: 0.75rem;
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
}
|
|
196
|
+
.env-label { font-size: 0.72rem; color: var(--text-muted); font-weight: 500; }
|
|
197
|
+
.env-select {
|
|
198
|
+
background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
|
199
|
+
font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 4px 8px;
|
|
200
|
+
border-radius: 5px; outline: none; cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.env-spacer { flex: 1; }
|
|
203
|
+
.try-btn { background: var(--accent2); border: none; border-radius: 6px; color: white; font-family: 'Inter', sans-serif; font-weight: 600; font-size: 0.78rem; padding: 5px 14px; cursor: pointer; transition: opacity 0.15s; display: flex; align-items: center; gap: 5px; }
|
|
204
|
+
.try-btn:hover { opacity: 0.85; }
|
|
205
|
+
.curl-btn { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-mid); font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 5px 12px; cursor: pointer; transition: all 0.15s; }
|
|
206
|
+
.curl-btn:hover { border-color: var(--border2); color: var(--text); }
|
|
207
|
+
|
|
208
|
+
/* ── Panels ── */
|
|
209
|
+
.panel-section { margin-bottom: 1.5rem; }
|
|
210
|
+
.section-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.6rem; }
|
|
211
|
+
|
|
212
|
+
/* Table */
|
|
213
|
+
.ptable { width: 100%; border-collapse: collapse; }
|
|
214
|
+
.ptable th { text-align: left; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); padding: 0 1rem 0.5rem 0; border-bottom: 1px solid var(--border); }
|
|
215
|
+
.ptable td { padding: 0.55rem 1rem 0.55rem 0; font-size: 0.82rem; border-bottom: 1px solid rgba(42,49,73,0.5); vertical-align: middle; }
|
|
216
|
+
.pname { font-family: 'JetBrains Mono', monospace; color: var(--text); font-size: 0.8rem; font-weight: 500; }
|
|
217
|
+
.ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent2); background: rgba(59,130,246,0.08); padding: 1px 6px; border-radius: 3px; }
|
|
218
|
+
.pdesc { color: var(--text-mid); }
|
|
219
|
+
.pex { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent); background: var(--surface); padding: 2px 7px; border-radius: 3px; }
|
|
220
|
+
.req-tag { background: rgba(239,68,68,0.1); color: #ef4444; font-size: 0.62rem; padding: 1px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-weight: 600; margin-left: 4px; }
|
|
221
|
+
|
|
222
|
+
/* Code */
|
|
223
|
+
pre { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); overflow-x: auto; white-space: pre; line-height: 1.65; }
|
|
224
|
+
|
|
225
|
+
/* Code tabs (Node.js / Python / Go) */
|
|
226
|
+
.code-lang-tabs { display: flex; gap: 0; margin-bottom: 0; border: 1px solid var(--border); border-bottom: none; border-radius: 8px 8px 0 0; overflow: hidden; }
|
|
227
|
+
.lang-tab { background: var(--surface); border: none; color: var(--text-muted); font-family: 'Inter', sans-serif; font-size: 0.75rem; font-weight: 500; padding: 6px 14px; cursor: pointer; transition: all 0.15s; }
|
|
228
|
+
.lang-tab:hover { color: var(--text-mid); }
|
|
229
|
+
.lang-tab.on { background: var(--panel); color: var(--text); }
|
|
230
|
+
.lang-pre { border-radius: 0 0 8px 8px !important; margin-top: 0 !important; }
|
|
231
|
+
|
|
232
|
+
/* Response rows */
|
|
233
|
+
.res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.6rem 0; border-bottom: 1px solid rgba(42,49,73,0.4); }
|
|
234
|
+
.sc { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 4px; min-width: 44px; text-align: center; flex-shrink: 0; }
|
|
235
|
+
.s2 { background: rgba(16,185,129,0.1); color: var(--GET); }
|
|
236
|
+
.s4 { background: rgba(234,179,8,0.1); color: var(--PUT); }
|
|
237
|
+
.s5 { background: rgba(239,68,68,0.1); color: var(--DELETE); }
|
|
238
|
+
.res-desc { font-size: 0.82rem; color: var(--text-mid); }
|
|
239
|
+
|
|
240
|
+
/* Try it */
|
|
241
|
+
.try-layout { display: flex; flex-direction: column; gap: 1rem; }
|
|
242
|
+
.try-url-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
|
|
243
|
+
.try-url-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; flex: 1; outline: none; }
|
|
244
|
+
.send-btn { background: var(--accent); border: none; border-radius: 5px; color: #0a0f0d; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 0.78rem; padding: 5px 16px; cursor: pointer; transition: opacity 0.15s; white-space: nowrap; }
|
|
245
|
+
.send-btn:hover { opacity: 0.85; }
|
|
246
|
+
.try-textarea { width: 100%; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.8rem 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 110px; resize: vertical; outline: none; line-height: 1.65; }
|
|
247
|
+
.try-textarea:focus { border-color: var(--accent2); }
|
|
248
|
+
.try-token-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
|
|
249
|
+
.try-token-row span { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; background: var(--surface2); padding: 2px 7px; border-radius: 3px; }
|
|
250
|
+
.try-token-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; flex: 1; outline: none; }
|
|
251
|
+
.res-output { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 80px; white-space: pre-wrap; word-break: break-all; display: none; line-height: 1.6; }
|
|
252
|
+
.res-output.show { display: block; }
|
|
253
|
+
.res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; font-weight: 600; }
|
|
254
|
+
.ok { color: var(--GET); } .bad { color: var(--DELETE); }
|
|
255
|
+
|
|
256
|
+
/* Loading */
|
|
257
|
+
.state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 1rem; padding: 4rem; text-align: center; }
|
|
258
|
+
.dino-anim { font-size: 3rem; animation: walk 0.6s steps(2) infinite; }
|
|
259
|
+
@keyframes walk { 0%{transform:scaleX(1)} 50%{transform:scaleX(-1)} }
|
|
260
|
+
.state-title { font-size: 1rem; font-weight: 600; color: var(--text-mid); }
|
|
261
|
+
.state-sub { font-size: 0.82rem; color: var(--text-muted); max-width: 320px; line-height: 1.5; }
|
|
262
|
+
.progress-bar { width: 200px; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
|
|
263
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 2px; animation: prog 1.4s ease-in-out infinite; }
|
|
264
|
+
@keyframes prog { 0%{transform:translateX(-100%)} 100%{transform:translateX(400%)} }
|
|
265
|
+
|
|
266
|
+
/* Toast */
|
|
267
|
+
.toasts { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9000; }
|
|
268
|
+
.toast { background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; padding: 0.6rem 1rem; font-size: 0.8rem; color: var(--text); animation: tin 0.2s ease; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
|
|
269
|
+
@keyframes tin { from{transform:translateY(10px);opacity:0} to{transform:translateY(0);opacity:1} }
|
|
270
|
+
</style>
|
|
271
|
+
</head>
|
|
272
|
+
<body>
|
|
273
|
+
<header>
|
|
274
|
+
<a class="logo" href="#" onclick="showOverview();return false;">
|
|
275
|
+
<span class="logo-dino">🦕</span>
|
|
276
|
+
<span class="logo-name">Dino<span>rex</span></span>
|
|
277
|
+
</a>
|
|
278
|
+
<div class="hdr-divider"></div>
|
|
279
|
+
<span class="hdr-project" id="hdrProject">—</span>
|
|
280
|
+
<div class="hdr-spacer"></div>
|
|
281
|
+
<div class="status-pill" id="statusPill">
|
|
282
|
+
<span class="status-dot"></span>
|
|
283
|
+
<span id="statusText">Loading</span>
|
|
284
|
+
</div>
|
|
285
|
+
<button class="hdr-btn" onclick="triggerRescan(false)" title="Check for new/changed endpoints">
|
|
286
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
287
|
+
Rescan
|
|
288
|
+
</button>
|
|
289
|
+
<button class="hdr-btn" onclick="triggerRescan(true)" title="Force full re-analysis">
|
|
290
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
|
291
|
+
Full Rescan
|
|
292
|
+
</button>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<div class="app-body">
|
|
296
|
+
<nav>
|
|
297
|
+
<div class="nav-header">
|
|
298
|
+
<div class="search-wrap">
|
|
299
|
+
<svg class="search-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
300
|
+
<input class="search-input" type="text" placeholder="Search endpoints..." oninput="filterNav(this.value)" />
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="nav-list" id="navList"></div>
|
|
304
|
+
</nav>
|
|
305
|
+
|
|
306
|
+
<main id="mainPanel">
|
|
307
|
+
<div class="state-center">
|
|
308
|
+
<div class="dino-anim">🦕</div>
|
|
309
|
+
<div class="state-title">Analyzing your API…</div>
|
|
310
|
+
<div class="state-sub">Scanning routes, controllers, services and models. This takes about 15 seconds.</div>
|
|
311
|
+
<div class="progress-bar"><div class="progress-fill"></div></div>
|
|
312
|
+
</div>
|
|
313
|
+
</main>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="toasts" id="toastContainer"></div>
|
|
317
|
+
|
|
318
|
+
<script>
|
|
319
|
+
let spec = null;
|
|
320
|
+
let activeId = null;
|
|
321
|
+
let pollTimer = null;
|
|
322
|
+
|
|
323
|
+
async function init() { pollStatus(); }
|
|
324
|
+
|
|
325
|
+
async function pollStatus() {
|
|
326
|
+
try {
|
|
327
|
+
const [sr, dr] = await Promise.all([fetch('/api/status'), fetch('/api/spec')]);
|
|
328
|
+
const status = await sr.json();
|
|
329
|
+
const data = await dr.json();
|
|
330
|
+
updateStatus(status);
|
|
331
|
+
if (!data._loading && !data.error) {
|
|
332
|
+
spec = data;
|
|
333
|
+
renderNav();
|
|
334
|
+
if (!activeId) showOverview();
|
|
335
|
+
else renderEndpoint(activeId);
|
|
336
|
+
} else if (data.error) {
|
|
337
|
+
showError(data.error);
|
|
338
|
+
}
|
|
339
|
+
if (status.state === 'analyzing' || status.state === 'pending' || (status.state === 'ready' && data._loading)) {
|
|
340
|
+
pollTimer = setTimeout(pollStatus, status.state === 'ready' ? 1000 : 2000);
|
|
341
|
+
}
|
|
342
|
+
} catch { setTimeout(pollStatus, 3000); }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function updateStatus(s) {
|
|
346
|
+
const pill = document.getElementById('statusPill');
|
|
347
|
+
const txt = document.getElementById('statusText');
|
|
348
|
+
pill.className = 'status-pill ' + s.state;
|
|
349
|
+
txt.textContent = s.state === 'ready' ? 'System Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Nav ──────────────────────────────────────────────────────────────────
|
|
353
|
+
const colIcons = ['📁','📂','🗂️','📋','🔧','⚡','🛡️','👤','🏪','💳'];
|
|
354
|
+
function colIcon(i) { return colIcons[i % colIcons.length]; }
|
|
355
|
+
|
|
356
|
+
function renderNav(filter = '') {
|
|
357
|
+
if (!spec) return;
|
|
358
|
+
const q = filter.toLowerCase();
|
|
359
|
+
let html = '';
|
|
360
|
+
spec.collections.forEach((col, ci) => {
|
|
361
|
+
const eps = col.endpoints.filter(e =>
|
|
362
|
+
!q || e.path.toLowerCase().includes(q) || e.summary.toLowerCase().includes(q) || e.method.toLowerCase().includes(q)
|
|
363
|
+
);
|
|
364
|
+
if (!eps.length) return;
|
|
365
|
+
const isOpen = !q ? true : true;
|
|
366
|
+
html += `<div class="nav-group ${isOpen?'open':''}" id="ng-${ci}">
|
|
367
|
+
<div class="nav-group-header" onclick="toggleGroup(${ci})">
|
|
368
|
+
<span class="nav-group-icon">▶</span>
|
|
369
|
+
<span style="font-size:.85rem;margin-right:2px">${colIcon(ci)}</span>
|
|
370
|
+
<span class="nav-group-name">${col.name}</span>
|
|
371
|
+
<span class="nav-group-count">${eps.length}</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="nav-group-items">
|
|
374
|
+
${eps.map(ep => `
|
|
375
|
+
<div class="nav-item ${ep.id === activeId ? 'active' : ''}" onclick="selectEndpoint('${ep.id}')">
|
|
376
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
377
|
+
<span class="nav-path">${ep.path}</span>
|
|
378
|
+
</div>`).join('')}
|
|
379
|
+
</div>
|
|
380
|
+
</div>`;
|
|
381
|
+
});
|
|
382
|
+
document.getElementById('navList').innerHTML = html || `<div style="padding:1rem;font-size:.8rem;color:var(--text-muted)">No results</div>`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function toggleGroup(ci) {
|
|
386
|
+
document.getElementById(`ng-${ci}`)?.classList.toggle('open');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function filterNav(v) { renderNav(v); }
|
|
390
|
+
function selectEndpoint(id) { activeId = id; renderNav(document.querySelector('.search-input')?.value||''); renderEndpoint(id); }
|
|
391
|
+
|
|
392
|
+
// ── Overview ──────────────────────────────────────────────────────────────
|
|
393
|
+
function showOverview() {
|
|
394
|
+
activeId = null; renderNav();
|
|
395
|
+
if (!spec) return;
|
|
396
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
397
|
+
const totalEps = spec.collections.reduce((a,c) => a+c.endpoints.length, 0);
|
|
398
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
399
|
+
<div class="overview">
|
|
400
|
+
<div class="ov-eyebrow">API Documentation</div>
|
|
401
|
+
<div class="ov-title">${spec.projectName}</div>
|
|
402
|
+
<div class="ov-desc">${spec.description || 'Auto-generated API documentation by Dinorex.'}</div>
|
|
403
|
+
<div class="ov-stats">
|
|
404
|
+
<div class="ov-stat"><div class="ov-stat-n">${totalEps}</div><div class="ov-stat-l">Endpoints</div></div>
|
|
405
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.length}</div><div class="ov-stat-l">Collections</div></div>
|
|
406
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.version}</div><div class="ov-stat-l">Version</div></div>
|
|
407
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.reduce((a,c)=>a+c.endpoints.filter(e=>e.requiresAuth).length,0)}</div><div class="ov-stat-l">Auth Required</div></div>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="ov-base">${spec.baseUrl}</div>
|
|
410
|
+
<div class="ov-cols-title">Collections</div>
|
|
411
|
+
${spec.collections.map((c,ci) => `
|
|
412
|
+
<div class="ov-col-row" onclick="selectEndpoint('${c.endpoints[0]?.id}')">
|
|
413
|
+
<div class="ov-col-icon">${colIcon(ci)}</div>
|
|
414
|
+
<div>
|
|
415
|
+
<div class="ov-col-name">${c.name}</div>
|
|
416
|
+
<div class="ov-col-desc" style="font-size:.75rem;margin-top:1px">${c.description||''}</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="ov-col-count">${c.endpoints.length} endpoints</div>
|
|
419
|
+
</div>`).join('')}
|
|
420
|
+
</div>`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Endpoint ──────────────────────────────────────────────────────────────
|
|
424
|
+
function findEndpoint(id) {
|
|
425
|
+
for (const col of (spec?.collections||[])) {
|
|
426
|
+
const ep = col.endpoints.find(e => e.id === id);
|
|
427
|
+
if (ep) return ep;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function renderEndpoint(id) {
|
|
433
|
+
const ep = findEndpoint(id);
|
|
434
|
+
if (!ep || !spec) return;
|
|
435
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
436
|
+
|
|
437
|
+
const hasPath = ep.pathParams?.length;
|
|
438
|
+
const hasQuery = ep.queryParams?.length;
|
|
439
|
+
const hasBody = ep.requestBody && Object.keys(ep.requestBody.schema||{}).length;
|
|
440
|
+
const hasRes = ep.responses && Object.keys(ep.responses).length;
|
|
441
|
+
|
|
442
|
+
const tabs = [
|
|
443
|
+
(hasPath||hasQuery) ? 'params' : null,
|
|
444
|
+
hasBody ? 'body' : null,
|
|
445
|
+
hasRes ? 'responses' : null,
|
|
446
|
+
'examples',
|
|
447
|
+
'try'
|
|
448
|
+
].filter(Boolean);
|
|
449
|
+
|
|
450
|
+
const schema = ep.requestBody?.schema || {};
|
|
451
|
+
const bodyEx = JSON.stringify(Object.fromEntries(Object.entries(schema).map(([k,v])=>[k,v.example??''])), null, 2);
|
|
452
|
+
|
|
453
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
454
|
+
<div class="ep-view">
|
|
455
|
+
<div class="ep-topbar">
|
|
456
|
+
<div class="ep-topbar-row1">
|
|
457
|
+
<span class="ep-method-badge m-${ep.method}">${ep.method}</span>
|
|
458
|
+
<span class="ep-path-text">${ep.path}</span>
|
|
459
|
+
${ep.requiresAuth ? `<span class="ep-auth-tag">🔑 Auth required</span>` : ''}
|
|
460
|
+
</div>
|
|
461
|
+
<div class="ep-title">${ep.summary}</div>
|
|
462
|
+
<div class="ep-desc">${ep.description||''}</div>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="ep-tabs">
|
|
465
|
+
${tabs.map((t,i) => `<button class="tab ${i===0?'on':''}" onclick="switchTab(this,'${id}-${t}')">${{params:'Parameters',body:'Body',responses:'Responses',examples:'Examples',try:'Try It ▶'}[t]}</button>`).join('')}
|
|
466
|
+
</div>
|
|
467
|
+
<div class="ep-content">
|
|
468
|
+
${tabs.map((t,i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${buildPanel(ep,t,bodyEx)}</div>`).join('')}
|
|
469
|
+
</div>
|
|
470
|
+
</div>`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function switchTab(btn, panelId) {
|
|
474
|
+
btn.closest('.ep-view').querySelectorAll('.tab').forEach(b=>b.classList.remove('on'));
|
|
475
|
+
btn.classList.add('on');
|
|
476
|
+
btn.closest('.ep-view').querySelector('.ep-content').querySelectorAll(':scope>div').forEach(d=>d.style.display='none');
|
|
477
|
+
document.getElementById(panelId).style.display='block';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function buildPanel(ep, tab, bodyEx) {
|
|
481
|
+
if (tab === 'params') {
|
|
482
|
+
let h = '';
|
|
483
|
+
if (ep.pathParams?.length) {
|
|
484
|
+
h += `<div class="panel-section"><div class="section-label">Path Parameters <span style="color:#ef4444;margin-left:4px">${ep.pathParams.length} Required</span></div>
|
|
485
|
+
<table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
|
|
486
|
+
${ep.pathParams.map(p=>`<tr><td><span class="pname">${p.name}</span><span class="req-tag">REQUIRED</span></td><td><span class="ptype">${p.type}</span></td><td class="pdesc">${p.description}</td><td><span class="pex">${p.example}</span></td></tr>`).join('')}
|
|
487
|
+
</tbody></table></div>`;
|
|
488
|
+
}
|
|
489
|
+
if (ep.queryParams?.length) {
|
|
490
|
+
h += `<div class="panel-section"><div class="section-label">Query Parameters</div>
|
|
491
|
+
<table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
|
|
492
|
+
${ep.queryParams.map(q=>`<tr><td><span class="pname">${q.name}</span></td><td><span class="ptype">${q.type}</span></td><td class="pdesc">${q.description}</td><td><span class="pex">${q.example}</span></td></tr>`).join('')}
|
|
493
|
+
</tbody></table></div>`;
|
|
494
|
+
}
|
|
495
|
+
return h;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (tab === 'body') {
|
|
499
|
+
const schema = ep.requestBody?.schema||{};
|
|
500
|
+
return `<div class="panel-section"><div class="section-label">Schema</div>
|
|
501
|
+
<table class="ptable"><thead><tr><th>FIELD</th><th>TYPE</th><th>REQUIRED</th><th>DESCRIPTION</th></tr></thead><tbody>
|
|
502
|
+
${Object.entries(schema).map(([k,v])=>`<tr><td><span class="pname">${k}</span></td><td><span class="ptype">${v.type}</span></td><td>${v.required?'<span class="req-tag">YES</span>':'<span style="color:var(--text-muted)">—</span>'}</td><td class="pdesc">${v.description||''}</td></tr>`).join('')}
|
|
503
|
+
</tbody></table></div>
|
|
504
|
+
<div class="panel-section"><div class="section-label">Example Body</div><pre>${bodyEx}</pre></div>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (tab === 'responses') {
|
|
508
|
+
return `<div class="panel-section">${Object.entries(ep.responses||{}).map(([code,r])=>{
|
|
509
|
+
const cls = code.startsWith('2')?'s2':code.startsWith('4')?'s4':'s5';
|
|
510
|
+
return `<div class="res-row"><span class="sc ${cls}">${code}</span><div><div class="res-desc">${r.description}</div>${r.example?`<pre style="margin-top:.5rem;font-size:.73rem">${JSON.stringify(r.example,null,2)}</pre>`:''}</div></div>`;
|
|
511
|
+
}).join('')}</div>`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (tab === 'examples') {
|
|
515
|
+
const url = `${spec.baseUrl}${ep.path}`;
|
|
516
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
|
|
517
|
+
const authHeader = ep.requiresAuth ? `\n "Authorization": "Bearer {YOUR_KEY}",` : '';
|
|
518
|
+
const nodeEx = `import fetch from 'node-fetch';\n\nconst url = "${url}";\nconst headers = {${authHeader}\n "Accept": "application/json"${hasBody?',\n "Content-Type": "application/json"':''}\n};\n${hasBody?`\nconst body = ${bodyEx};\n`:''}
|
|
519
|
+
const response = await fetch(url, {\n method: "${ep.method}",\n headers,${hasBody?'\n body: JSON.stringify(body),':''}\n});\nconst data = await response.json();\nconsole.log(data);`;
|
|
520
|
+
|
|
521
|
+
const pyEx = `import requests\n\nurl = "${url}"\nheaders = {${ep.requiresAuth?`\n "Authorization": "Bearer {YOUR_KEY}",`:''}\n "Accept": "application/json"\n}${hasBody?`\n\nbody = ${bodyEx}`:''}
|
|
522
|
+
\nresponse = requests.${ep.method.toLowerCase()}(url, headers=headers${hasBody?', json=body':''})\nprint(response.json())`;
|
|
523
|
+
|
|
524
|
+
const curlEx = `curl -X ${ep.method} "${url}" \\${ep.requiresAuth?`\n -H "Authorization: Bearer {YOUR_KEY}" \\`:''}\n -H "Accept: application/json"${hasBody?` \\\n -H "Content-Type: application/json" \\\n -d '${bodyEx.replace(/\n/g,'').replace(/ /g,'')}'`:''}`;
|
|
525
|
+
|
|
526
|
+
return `<div class="panel-section">
|
|
527
|
+
<div class="section-label">Request Example</div>
|
|
528
|
+
<div class="code-lang-tabs">
|
|
529
|
+
<button class="lang-tab on" onclick="switchLang(this,'lang-node-${ep.id}')">Node.js</button>
|
|
530
|
+
<button class="lang-tab" onclick="switchLang(this,'lang-py-${ep.id}')">Python</button>
|
|
531
|
+
<button class="lang-tab" onclick="switchLang(this,'lang-curl-${ep.id}')">cURL</button>
|
|
532
|
+
</div>
|
|
533
|
+
<pre class="lang-pre" id="lang-node-${ep.id}">${nodeEx}</pre>
|
|
534
|
+
<pre class="lang-pre" id="lang-py-${ep.id}" style="display:none">${pyEx}</pre>
|
|
535
|
+
<pre class="lang-pre" id="lang-curl-${ep.id}" style="display:none">${curlEx}</pre>
|
|
536
|
+
</div>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (tab === 'try') {
|
|
540
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
|
|
541
|
+
return `<div class="try-layout">
|
|
542
|
+
<div class="panel-section">
|
|
543
|
+
<div class="section-label">URL</div>
|
|
544
|
+
<div class="try-url-row">
|
|
545
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
546
|
+
<input type="text" id="try-url-${ep.id}" value="${spec.baseUrl}${ep.path}" />
|
|
547
|
+
<button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">▶ Send</button>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
${ep.requiresAuth?`<div class="panel-section"><div class="section-label">Bearer Token</div>
|
|
551
|
+
<div class="try-token-row"><span>Bearer</span><input type="text" id="try-tok-${ep.id}" placeholder="Paste your token…"/></div></div>`:''}
|
|
552
|
+
${hasBody?`<div class="panel-section"><div class="section-label">Request Body</div>
|
|
553
|
+
<textarea class="try-textarea" id="try-body-${ep.id}">${bodyEx}</textarea></div>`:''}
|
|
554
|
+
<div class="panel-section">
|
|
555
|
+
<div class="section-label">Response</div>
|
|
556
|
+
<div class="res-output" id="try-res-${ep.id}">
|
|
557
|
+
<div class="res-stat" id="try-stat-${ep.id}"></div>
|
|
558
|
+
<div id="try-out-${ep.id}"></div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>`;
|
|
562
|
+
}
|
|
563
|
+
return '';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function switchLang(btn, panelId) {
|
|
567
|
+
const section = btn.closest('.panel-section');
|
|
568
|
+
section.querySelectorAll('.lang-tab').forEach(b=>b.classList.remove('on'));
|
|
569
|
+
btn.classList.add('on');
|
|
570
|
+
section.querySelectorAll('.lang-pre').forEach(p=>p.style.display='none');
|
|
571
|
+
document.getElementById(panelId).style.display='block';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function doSend(id, method, hasBody) {
|
|
575
|
+
const url = document.getElementById(`try-url-${id}`)?.value;
|
|
576
|
+
const tok = document.getElementById(`try-tok-${id}`)?.value;
|
|
577
|
+
const bodyEl = document.getElementById(`try-body-${id}`);
|
|
578
|
+
const resBox = document.getElementById(`try-res-${id}`);
|
|
579
|
+
const statEl = document.getElementById(`try-stat-${id}`);
|
|
580
|
+
const outEl = document.getElementById(`try-out-${id}`);
|
|
581
|
+
resBox.classList.add('show'); outEl.textContent='Sending…'; statEl.innerHTML='';
|
|
582
|
+
const headers = {'Content-Type':'application/json'};
|
|
583
|
+
if (tok) headers['Authorization']=`Bearer ${tok}`;
|
|
584
|
+
const opts = {method,headers};
|
|
585
|
+
if (hasBody && bodyEl) { try{opts.body=JSON.stringify(JSON.parse(bodyEl.value));}catch{opts.body=bodyEl.value;} }
|
|
586
|
+
try {
|
|
587
|
+
const r = await fetch(url,opts);
|
|
588
|
+
const txt = await r.text();
|
|
589
|
+
let display; try{display=JSON.stringify(JSON.parse(txt),null,2);}catch{display=txt;}
|
|
590
|
+
statEl.innerHTML=`<span class="${r.ok?'ok':'bad'}">● ${r.status} ${r.statusText}</span>`;
|
|
591
|
+
outEl.textContent=display;
|
|
592
|
+
} catch(e) { statEl.innerHTML=`<span class="bad">● Network Error</span>`; outEl.textContent=e.message; }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function triggerRescan(full) {
|
|
596
|
+
await fetch(full?'/api/rescan/full':'/api/rescan', {method:'POST'});
|
|
597
|
+
toast(full?'Full rescan started…':'Checking for new endpoints…');
|
|
598
|
+
clearTimeout(pollTimer); pollStatus();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function showError(msg) {
|
|
602
|
+
document.getElementById('mainPanel').innerHTML=`<div class="state-center"><div style="font-size:2rem">⚠️</div><div class="state-title" style="color:var(--DELETE)">Error</div><div class="state-sub">${msg}</div></div>`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function toast(msg) {
|
|
606
|
+
const t=document.createElement('div'); t.className='toast'; t.textContent=msg;
|
|
607
|
+
document.getElementById('toastContainer').appendChild(t);
|
|
608
|
+
setTimeout(()=>t.remove(),3000);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
init();
|
|
612
|
+
</script>
|
|
613
|
+
</body>
|
|
614
|
+
</html>
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Dinorex</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0f1117;
|
|
12
|
+
--panel: #161b27;
|
|
13
|
+
--surface: #1c2333;
|
|
14
|
+
--surface2: #212840;
|
|
15
|
+
--border: #2a3149;
|
|
16
|
+
--border2: #323d5c;
|
|
17
|
+
--accent: #00c46a;
|
|
18
|
+
--accent-dim: rgba(0,196,106,0.1);
|
|
19
|
+
--accent2: #3b82f6;
|
|
20
|
+
--gold: #f59e0b;
|
|
21
|
+
--text: #e2e8f0;
|
|
22
|
+
--text-mid: #94a3b8;
|
|
23
|
+
--text-muted: #475569;
|
|
24
|
+
--GET: #10b981; --GET-bg: rgba(16,185,129,0.12);
|
|
25
|
+
--POST: #f97316; --POST-bg: rgba(249,115,22,0.12);
|
|
26
|
+
--PUT: #eab308; --PUT-bg: rgba(234,179,8,0.12);
|
|
27
|
+
--PATCH: #a78bfa; --PATCH-bg: rgba(167,139,250,0.12);
|
|
28
|
+
--DELETE: #ef4444; --DELETE-bg: rgba(239,68,68,0.12);
|
|
29
|
+
--sidebar-w: 260px;
|
|
30
|
+
--header-h: 52px;
|
|
31
|
+
}
|
|
32
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
+
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; font-size: 13px; }
|
|
34
|
+
|
|
35
|
+
/* ── Header ── */
|
|
36
|
+
header {
|
|
37
|
+
height: var(--header-h);
|
|
38
|
+
background: var(--panel);
|
|
39
|
+
border-bottom: 1px solid var(--border);
|
|
40
|
+
display: flex; align-items: center;
|
|
41
|
+
padding: 0 1rem; gap: 0.75rem;
|
|
42
|
+
flex-shrink: 0; z-index: 100;
|
|
43
|
+
}
|
|
44
|
+
.logo { display: flex; align-items: center; gap: 8px; cursor: pointer; text-decoration: none; }
|
|
45
|
+
.logo-dino { font-size: 1.2rem; }
|
|
46
|
+
.logo-name { font-size: 0.95rem; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
|
|
47
|
+
.logo-name span { color: var(--accent); }
|
|
48
|
+
|
|
49
|
+
.hdr-divider { width: 1px; height: 20px; background: var(--border); }
|
|
50
|
+
.hdr-project { font-size: 0.8rem; color: var(--text-mid); font-weight: 500; }
|
|
51
|
+
.hdr-spacer { flex: 1; }
|
|
52
|
+
|
|
53
|
+
.status-pill {
|
|
54
|
+
display: flex; align-items: center; gap: 5px;
|
|
55
|
+
font-size: 0.72rem; font-weight: 500;
|
|
56
|
+
color: var(--text-muted); padding: 3px 10px;
|
|
57
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 20px;
|
|
58
|
+
}
|
|
59
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
|
60
|
+
.status-pill.ready .status-dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
|
|
61
|
+
.status-pill.ready { color: var(--text-mid); }
|
|
62
|
+
.status-pill.analyzing .status-dot { background: var(--gold); animation: blink 1s ease-in-out infinite; }
|
|
63
|
+
.status-pill.error .status-dot { background: var(--DELETE); }
|
|
64
|
+
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
|
|
65
|
+
|
|
66
|
+
.hdr-btn {
|
|
67
|
+
display: flex; align-items: center; gap: 5px;
|
|
68
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
69
|
+
color: var(--text-mid); font-family: 'Inter', sans-serif;
|
|
70
|
+
font-size: 0.75rem; font-weight: 500;
|
|
71
|
+
padding: 5px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
|
72
|
+
}
|
|
73
|
+
.hdr-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
|
|
74
|
+
.hdr-btn svg { width: 13px; height: 13px; }
|
|
75
|
+
|
|
76
|
+
/* ── Layout ── */
|
|
77
|
+
.app-body { display: flex; flex: 1; overflow: hidden; }
|
|
78
|
+
|
|
79
|
+
/* ── Sidebar ── */
|
|
80
|
+
nav {
|
|
81
|
+
width: var(--sidebar-w); flex-shrink: 0;
|
|
82
|
+
background: var(--panel); border-right: 1px solid var(--border);
|
|
83
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-header {
|
|
87
|
+
padding: 0.6rem 0.75rem;
|
|
88
|
+
border-bottom: 1px solid var(--border);
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.search-wrap { position: relative; }
|
|
93
|
+
.search-icon { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; }
|
|
94
|
+
.search-input {
|
|
95
|
+
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
|
96
|
+
border-radius: 6px; padding: 5px 8px 5px 28px;
|
|
97
|
+
color: var(--text); font-family: 'Inter', sans-serif; font-size: 0.78rem;
|
|
98
|
+
outline: none; transition: border-color 0.15s;
|
|
99
|
+
}
|
|
100
|
+
.search-input:focus { border-color: var(--accent2); }
|
|
101
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
102
|
+
|
|
103
|
+
.nav-list { overflow-y: auto; flex: 1; padding: 0.5rem 0 2rem; }
|
|
104
|
+
.nav-list::-webkit-scrollbar { width: 3px; }
|
|
105
|
+
.nav-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
106
|
+
|
|
107
|
+
.nav-group { margin-bottom: 0.25rem; }
|
|
108
|
+
.nav-group-header {
|
|
109
|
+
display: flex; align-items: center; gap: 6px;
|
|
110
|
+
padding: 0.4rem 0.75rem; cursor: pointer;
|
|
111
|
+
color: var(--text-mid); font-size: 0.78rem; font-weight: 600;
|
|
112
|
+
user-select: none;
|
|
113
|
+
}
|
|
114
|
+
.nav-group-header:hover { color: var(--text); }
|
|
115
|
+
.nav-group-icon { color: var(--text-muted); font-size: 0.65rem; transition: transform 0.15s; }
|
|
116
|
+
.nav-group.open .nav-group-icon { transform: rotate(90deg); }
|
|
117
|
+
.nav-group-name { flex: 1; }
|
|
118
|
+
.nav-group-count { font-size: 0.65rem; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 10px; }
|
|
119
|
+
|
|
120
|
+
.nav-group-items { display: none; }
|
|
121
|
+
.nav-group.open .nav-group-items { display: block; }
|
|
122
|
+
|
|
123
|
+
.nav-item {
|
|
124
|
+
display: flex; align-items: center; gap: 7px;
|
|
125
|
+
padding: 0.35rem 0.75rem 0.35rem 1.5rem;
|
|
126
|
+
cursor: pointer; transition: all 0.1s;
|
|
127
|
+
border-left: 2px solid transparent;
|
|
128
|
+
}
|
|
129
|
+
.nav-item:hover { background: var(--surface); }
|
|
130
|
+
.nav-item.active { background: var(--accent-dim); border-left-color: var(--accent); }
|
|
131
|
+
.nav-item.active .nav-path { color: var(--text); }
|
|
132
|
+
|
|
133
|
+
.m-pill {
|
|
134
|
+
font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 600;
|
|
135
|
+
padding: 1px 5px; border-radius: 3px; min-width: 42px; text-align: center; flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
.m-GET { color: var(--GET); background: var(--GET-bg); }
|
|
138
|
+
.m-POST { color: var(--POST); background: var(--POST-bg); }
|
|
139
|
+
.m-PUT { color: var(--PUT); background: var(--PUT-bg); }
|
|
140
|
+
.m-PATCH { color: var(--PATCH); background: var(--PATCH-bg); }
|
|
141
|
+
.m-DELETE { color: var(--DELETE); background: var(--DELETE-bg); }
|
|
142
|
+
|
|
143
|
+
.nav-path { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
144
|
+
|
|
145
|
+
/* ── Main ── */
|
|
146
|
+
main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; background: var(--bg); }
|
|
147
|
+
main::-webkit-scrollbar { width: 4px; }
|
|
148
|
+
main::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
149
|
+
|
|
150
|
+
/* ── Overview ── */
|
|
151
|
+
.overview { padding: 2.5rem 3rem; max-width: 800px; }
|
|
152
|
+
.ov-eyebrow { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.5rem; }
|
|
153
|
+
.ov-title { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.5rem; }
|
|
154
|
+
.ov-desc { color: var(--text-mid); font-size: 0.85rem; line-height: 1.6; margin-bottom: 2rem; }
|
|
155
|
+
.ov-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 2rem; }
|
|
156
|
+
.ov-stat { background: var(--panel); padding: 1rem 1.25rem; }
|
|
157
|
+
.ov-stat-n { font-size: 1.5rem; font-weight: 700; color: var(--accent); font-family: 'JetBrains Mono', monospace; }
|
|
158
|
+
.ov-stat-l { font-size: 0.68rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 2px; }
|
|
159
|
+
.ov-base { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--accent2); background: var(--surface); padding: 0.6rem 1rem; border-radius: 6px; border: 1px solid var(--border); display: inline-block; margin-bottom: 2rem; }
|
|
160
|
+
|
|
161
|
+
.ov-cols-title { font-size: 0.78rem; font-weight: 600; color: var(--text-mid); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.75rem; }
|
|
162
|
+
.ov-col-row { display: flex; align-items: center; gap: 1rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.85rem 1.1rem; cursor: pointer; transition: all 0.15s; margin-bottom: 0.5rem; }
|
|
163
|
+
.ov-col-row:hover { border-color: var(--border2); background: var(--surface); }
|
|
164
|
+
.ov-col-icon { width: 32px; height: 32px; background: var(--surface2); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.9rem; flex-shrink: 0; }
|
|
165
|
+
.ov-col-name { font-weight: 600; font-size: 0.85rem; }
|
|
166
|
+
.ov-col-desc { font-size: 0.78rem; color: var(--text-mid); flex: 1; }
|
|
167
|
+
.ov-col-count { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--accent); background: var(--accent-dim); padding: 2px 8px; border-radius: 10px; flex-shrink: 0; }
|
|
168
|
+
|
|
169
|
+
/* ── Endpoint view ── */
|
|
170
|
+
.ep-view { display: flex; flex-direction: column; height: 100%; }
|
|
171
|
+
|
|
172
|
+
.ep-topbar { flex-shrink: 0; padding: 1.5rem 2rem 0; background: var(--bg); }
|
|
173
|
+
.ep-topbar-row1 { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
|
|
174
|
+
.ep-method-badge { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 700; padding: 4px 10px; border-radius: 5px; }
|
|
175
|
+
.ep-path-text { font-family: 'JetBrains Mono', monospace; font-size: 1rem; font-weight: 500; color: var(--text); }
|
|
176
|
+
.ep-auth-tag { background: rgba(245,158,11,0.1); color: var(--gold); border: 1px solid rgba(245,158,11,0.2); font-size: 0.68rem; padding: 2px 8px; border-radius: 20px; display: flex; align-items: center; gap: 4px; }
|
|
177
|
+
.ep-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.25rem; }
|
|
178
|
+
.ep-desc { font-size: 0.83rem; color: var(--text-mid); line-height: 1.55; margin-bottom: 1rem; }
|
|
179
|
+
|
|
180
|
+
.ep-tabs { flex-shrink: 0; display: flex; border-bottom: 1px solid var(--border); padding: 0 2rem; background: var(--bg); }
|
|
181
|
+
.tab { font-size: 0.78rem; font-weight: 500; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); padding: 0.65rem 1rem; cursor: pointer; transition: all 0.15s; margin-bottom: -1px; }
|
|
182
|
+
.tab:hover { color: var(--text-mid); }
|
|
183
|
+
.tab.on { color: var(--text); border-bottom-color: var(--accent2); }
|
|
184
|
+
|
|
185
|
+
.ep-content { flex: 1; overflow-y: auto; padding: 1.5rem 2rem; }
|
|
186
|
+
.ep-content::-webkit-scrollbar { width: 4px; }
|
|
187
|
+
.ep-content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
188
|
+
|
|
189
|
+
/* ── Env bar (like the screenshot) ── */
|
|
190
|
+
.env-bar {
|
|
191
|
+
position: sticky; top: 0; z-index: 10;
|
|
192
|
+
background: var(--bg); border-bottom: 1px solid var(--border);
|
|
193
|
+
padding: 0.6rem 2rem; display: flex; align-items: center; gap: 0.75rem;
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
}
|
|
196
|
+
.env-label { font-size: 0.72rem; color: var(--text-muted); font-weight: 500; }
|
|
197
|
+
.env-select {
|
|
198
|
+
background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
|
199
|
+
font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 4px 8px;
|
|
200
|
+
border-radius: 5px; outline: none; cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.env-spacer { flex: 1; }
|
|
203
|
+
.try-btn { background: var(--accent2); border: none; border-radius: 6px; color: white; font-family: 'Inter', sans-serif; font-weight: 600; font-size: 0.78rem; padding: 5px 14px; cursor: pointer; transition: opacity 0.15s; display: flex; align-items: center; gap: 5px; }
|
|
204
|
+
.try-btn:hover { opacity: 0.85; }
|
|
205
|
+
.curl-btn { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-mid); font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 5px 12px; cursor: pointer; transition: all 0.15s; }
|
|
206
|
+
.curl-btn:hover { border-color: var(--border2); color: var(--text); }
|
|
207
|
+
|
|
208
|
+
/* ── Panels ── */
|
|
209
|
+
.panel-section { margin-bottom: 1.5rem; }
|
|
210
|
+
.section-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.6rem; }
|
|
211
|
+
|
|
212
|
+
/* Table */
|
|
213
|
+
.ptable { width: 100%; border-collapse: collapse; }
|
|
214
|
+
.ptable th { text-align: left; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); padding: 0 1rem 0.5rem 0; border-bottom: 1px solid var(--border); }
|
|
215
|
+
.ptable td { padding: 0.55rem 1rem 0.55rem 0; font-size: 0.82rem; border-bottom: 1px solid rgba(42,49,73,0.5); vertical-align: middle; }
|
|
216
|
+
.pname { font-family: 'JetBrains Mono', monospace; color: var(--text); font-size: 0.8rem; font-weight: 500; }
|
|
217
|
+
.ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent2); background: rgba(59,130,246,0.08); padding: 1px 6px; border-radius: 3px; }
|
|
218
|
+
.pdesc { color: var(--text-mid); }
|
|
219
|
+
.pex { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent); background: var(--surface); padding: 2px 7px; border-radius: 3px; }
|
|
220
|
+
.req-tag { background: rgba(239,68,68,0.1); color: #ef4444; font-size: 0.62rem; padding: 1px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-weight: 600; margin-left: 4px; }
|
|
221
|
+
|
|
222
|
+
/* Code */
|
|
223
|
+
pre { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); overflow-x: auto; white-space: pre; line-height: 1.65; }
|
|
224
|
+
|
|
225
|
+
/* Code tabs (Node.js / Python / Go) */
|
|
226
|
+
.code-lang-tabs { display: flex; gap: 0; margin-bottom: 0; border: 1px solid var(--border); border-bottom: none; border-radius: 8px 8px 0 0; overflow: hidden; }
|
|
227
|
+
.lang-tab { background: var(--surface); border: none; color: var(--text-muted); font-family: 'Inter', sans-serif; font-size: 0.75rem; font-weight: 500; padding: 6px 14px; cursor: pointer; transition: all 0.15s; }
|
|
228
|
+
.lang-tab:hover { color: var(--text-mid); }
|
|
229
|
+
.lang-tab.on { background: var(--panel); color: var(--text); }
|
|
230
|
+
.lang-pre { border-radius: 0 0 8px 8px !important; margin-top: 0 !important; }
|
|
231
|
+
|
|
232
|
+
/* Response rows */
|
|
233
|
+
.res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.6rem 0; border-bottom: 1px solid rgba(42,49,73,0.4); }
|
|
234
|
+
.sc { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 4px; min-width: 44px; text-align: center; flex-shrink: 0; }
|
|
235
|
+
.s2 { background: rgba(16,185,129,0.1); color: var(--GET); }
|
|
236
|
+
.s4 { background: rgba(234,179,8,0.1); color: var(--PUT); }
|
|
237
|
+
.s5 { background: rgba(239,68,68,0.1); color: var(--DELETE); }
|
|
238
|
+
.res-desc { font-size: 0.82rem; color: var(--text-mid); }
|
|
239
|
+
|
|
240
|
+
/* Try it */
|
|
241
|
+
.try-layout { display: flex; flex-direction: column; gap: 1rem; }
|
|
242
|
+
.try-url-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
|
|
243
|
+
.try-url-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; flex: 1; outline: none; }
|
|
244
|
+
.send-btn { background: var(--accent); border: none; border-radius: 5px; color: #0a0f0d; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 0.78rem; padding: 5px 16px; cursor: pointer; transition: opacity 0.15s; white-space: nowrap; }
|
|
245
|
+
.send-btn:hover { opacity: 0.85; }
|
|
246
|
+
.try-textarea { width: 100%; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.8rem 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 110px; resize: vertical; outline: none; line-height: 1.65; }
|
|
247
|
+
.try-textarea:focus { border-color: var(--accent2); }
|
|
248
|
+
.try-token-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
|
|
249
|
+
.try-token-row span { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; background: var(--surface2); padding: 2px 7px; border-radius: 3px; }
|
|
250
|
+
.try-token-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; flex: 1; outline: none; }
|
|
251
|
+
.res-output { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 80px; white-space: pre-wrap; word-break: break-all; display: none; line-height: 1.6; }
|
|
252
|
+
.res-output.show { display: block; }
|
|
253
|
+
.res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; font-weight: 600; }
|
|
254
|
+
.ok { color: var(--GET); } .bad { color: var(--DELETE); }
|
|
255
|
+
|
|
256
|
+
/* Loading */
|
|
257
|
+
.state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 1rem; padding: 4rem; text-align: center; }
|
|
258
|
+
.dino-anim { font-size: 3rem; animation: walk 0.6s steps(2) infinite; }
|
|
259
|
+
@keyframes walk { 0%{transform:scaleX(1)} 50%{transform:scaleX(-1)} }
|
|
260
|
+
.state-title { font-size: 1rem; font-weight: 600; color: var(--text-mid); }
|
|
261
|
+
.state-sub { font-size: 0.82rem; color: var(--text-muted); max-width: 320px; line-height: 1.5; }
|
|
262
|
+
.progress-bar { width: 200px; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
|
|
263
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 2px; animation: prog 1.4s ease-in-out infinite; }
|
|
264
|
+
@keyframes prog { 0%{transform:translateX(-100%)} 100%{transform:translateX(400%)} }
|
|
265
|
+
|
|
266
|
+
/* Toast */
|
|
267
|
+
.toasts { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9000; }
|
|
268
|
+
.toast { background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; padding: 0.6rem 1rem; font-size: 0.8rem; color: var(--text); animation: tin 0.2s ease; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
|
|
269
|
+
@keyframes tin { from{transform:translateY(10px);opacity:0} to{transform:translateY(0);opacity:1} }
|
|
270
|
+
</style>
|
|
271
|
+
</head>
|
|
272
|
+
<body>
|
|
273
|
+
<header>
|
|
274
|
+
<a class="logo" href="#" onclick="showOverview();return false;">
|
|
275
|
+
<span class="logo-dino">🦕</span>
|
|
276
|
+
<span class="logo-name">Dino<span>rex</span></span>
|
|
277
|
+
</a>
|
|
278
|
+
<div class="hdr-divider"></div>
|
|
279
|
+
<span class="hdr-project" id="hdrProject">—</span>
|
|
280
|
+
<div class="hdr-spacer"></div>
|
|
281
|
+
<div class="status-pill" id="statusPill">
|
|
282
|
+
<span class="status-dot"></span>
|
|
283
|
+
<span id="statusText">Loading</span>
|
|
284
|
+
</div>
|
|
285
|
+
<button class="hdr-btn" onclick="triggerRescan(false)" title="Check for new/changed endpoints">
|
|
286
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
287
|
+
Rescan
|
|
288
|
+
</button>
|
|
289
|
+
<button class="hdr-btn" onclick="triggerRescan(true)" title="Force full re-analysis">
|
|
290
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
|
291
|
+
Full Rescan
|
|
292
|
+
</button>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<div class="app-body">
|
|
296
|
+
<nav>
|
|
297
|
+
<div class="nav-header">
|
|
298
|
+
<div class="search-wrap">
|
|
299
|
+
<svg class="search-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
300
|
+
<input class="search-input" type="text" placeholder="Search endpoints..." oninput="filterNav(this.value)" />
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="nav-list" id="navList"></div>
|
|
304
|
+
</nav>
|
|
305
|
+
|
|
306
|
+
<main id="mainPanel">
|
|
307
|
+
<div class="state-center">
|
|
308
|
+
<div class="dino-anim">🦕</div>
|
|
309
|
+
<div class="state-title">Analyzing your API…</div>
|
|
310
|
+
<div class="state-sub">Scanning routes, controllers, services and models. This takes about 15 seconds.</div>
|
|
311
|
+
<div class="progress-bar"><div class="progress-fill"></div></div>
|
|
312
|
+
</div>
|
|
313
|
+
</main>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="toasts" id="toastContainer"></div>
|
|
317
|
+
|
|
318
|
+
<script>
|
|
319
|
+
let spec = null;
|
|
320
|
+
let activeId = null;
|
|
321
|
+
let pollTimer = null;
|
|
322
|
+
|
|
323
|
+
async function init() { pollStatus(); }
|
|
324
|
+
|
|
325
|
+
async function pollStatus() {
|
|
326
|
+
try {
|
|
327
|
+
const [sr, dr] = await Promise.all([fetch('/api/status'), fetch('/api/spec')]);
|
|
328
|
+
const status = await sr.json();
|
|
329
|
+
const data = await dr.json();
|
|
330
|
+
updateStatus(status);
|
|
331
|
+
if (!data._loading && !data.error) {
|
|
332
|
+
spec = data;
|
|
333
|
+
renderNav();
|
|
334
|
+
if (!activeId) showOverview();
|
|
335
|
+
else renderEndpoint(activeId);
|
|
336
|
+
} else if (data.error) {
|
|
337
|
+
showError(data.error);
|
|
338
|
+
}
|
|
339
|
+
if (status.state === 'analyzing' || status.state === 'pending' || (status.state === 'ready' && data._loading)) {
|
|
340
|
+
pollTimer = setTimeout(pollStatus, status.state === 'ready' ? 1000 : 2000);
|
|
341
|
+
}
|
|
342
|
+
} catch { setTimeout(pollStatus, 3000); }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function updateStatus(s) {
|
|
346
|
+
const pill = document.getElementById('statusPill');
|
|
347
|
+
const txt = document.getElementById('statusText');
|
|
348
|
+
pill.className = 'status-pill ' + s.state;
|
|
349
|
+
txt.textContent = s.state === 'ready' ? 'System Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Nav ──────────────────────────────────────────────────────────────────
|
|
353
|
+
const colIcons = ['📁','📂','🗂️','📋','🔧','⚡','🛡️','👤','🏪','💳'];
|
|
354
|
+
function colIcon(i) { return colIcons[i % colIcons.length]; }
|
|
355
|
+
|
|
356
|
+
function renderNav(filter = '') {
|
|
357
|
+
if (!spec) return;
|
|
358
|
+
const q = filter.toLowerCase();
|
|
359
|
+
let html = '';
|
|
360
|
+
spec.collections.forEach((col, ci) => {
|
|
361
|
+
const eps = col.endpoints.filter(e =>
|
|
362
|
+
!q || e.path.toLowerCase().includes(q) || e.summary.toLowerCase().includes(q) || e.method.toLowerCase().includes(q)
|
|
363
|
+
);
|
|
364
|
+
if (!eps.length) return;
|
|
365
|
+
const isOpen = !q ? true : true;
|
|
366
|
+
html += `<div class="nav-group ${isOpen?'open':''}" id="ng-${ci}">
|
|
367
|
+
<div class="nav-group-header" onclick="toggleGroup(${ci})">
|
|
368
|
+
<span class="nav-group-icon">▶</span>
|
|
369
|
+
<span style="font-size:.85rem;margin-right:2px">${colIcon(ci)}</span>
|
|
370
|
+
<span class="nav-group-name">${col.name}</span>
|
|
371
|
+
<span class="nav-group-count">${eps.length}</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="nav-group-items">
|
|
374
|
+
${eps.map(ep => `
|
|
375
|
+
<div class="nav-item ${ep.id === activeId ? 'active' : ''}" onclick="selectEndpoint('${ep.id}')">
|
|
376
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
377
|
+
<span class="nav-path">${ep.path}</span>
|
|
378
|
+
</div>`).join('')}
|
|
379
|
+
</div>
|
|
380
|
+
</div>`;
|
|
381
|
+
});
|
|
382
|
+
document.getElementById('navList').innerHTML = html || `<div style="padding:1rem;font-size:.8rem;color:var(--text-muted)">No results</div>`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function toggleGroup(ci) {
|
|
386
|
+
document.getElementById(`ng-${ci}`)?.classList.toggle('open');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function filterNav(v) { renderNav(v); }
|
|
390
|
+
function selectEndpoint(id) { activeId = id; renderNav(document.querySelector('.search-input')?.value||''); renderEndpoint(id); }
|
|
391
|
+
|
|
392
|
+
// ── Overview ──────────────────────────────────────────────────────────────
|
|
393
|
+
function showOverview() {
|
|
394
|
+
activeId = null; renderNav();
|
|
395
|
+
if (!spec) return;
|
|
396
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
397
|
+
const totalEps = spec.collections.reduce((a,c) => a+c.endpoints.length, 0);
|
|
398
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
399
|
+
<div class="overview">
|
|
400
|
+
<div class="ov-eyebrow">API Documentation</div>
|
|
401
|
+
<div class="ov-title">${spec.projectName}</div>
|
|
402
|
+
<div class="ov-desc">${spec.description || 'Auto-generated API documentation by Dinorex.'}</div>
|
|
403
|
+
<div class="ov-stats">
|
|
404
|
+
<div class="ov-stat"><div class="ov-stat-n">${totalEps}</div><div class="ov-stat-l">Endpoints</div></div>
|
|
405
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.length}</div><div class="ov-stat-l">Collections</div></div>
|
|
406
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.version}</div><div class="ov-stat-l">Version</div></div>
|
|
407
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.reduce((a,c)=>a+c.endpoints.filter(e=>e.requiresAuth).length,0)}</div><div class="ov-stat-l">Auth Required</div></div>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="ov-base">${spec.baseUrl}</div>
|
|
410
|
+
<div class="ov-cols-title">Collections</div>
|
|
411
|
+
${spec.collections.map((c,ci) => `
|
|
412
|
+
<div class="ov-col-row" onclick="selectEndpoint('${c.endpoints[0]?.id}')">
|
|
413
|
+
<div class="ov-col-icon">${colIcon(ci)}</div>
|
|
414
|
+
<div>
|
|
415
|
+
<div class="ov-col-name">${c.name}</div>
|
|
416
|
+
<div class="ov-col-desc" style="font-size:.75rem;margin-top:1px">${c.description||''}</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="ov-col-count">${c.endpoints.length} endpoints</div>
|
|
419
|
+
</div>`).join('')}
|
|
420
|
+
</div>`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Endpoint ──────────────────────────────────────────────────────────────
|
|
424
|
+
function findEndpoint(id) {
|
|
425
|
+
for (const col of (spec?.collections||[])) {
|
|
426
|
+
const ep = col.endpoints.find(e => e.id === id);
|
|
427
|
+
if (ep) return ep;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function renderEndpoint(id) {
|
|
433
|
+
const ep = findEndpoint(id);
|
|
434
|
+
if (!ep || !spec) return;
|
|
435
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
436
|
+
|
|
437
|
+
const hasPath = ep.pathParams?.length;
|
|
438
|
+
const hasQuery = ep.queryParams?.length;
|
|
439
|
+
const hasBody = ep.requestBody && Object.keys(ep.requestBody.schema||{}).length;
|
|
440
|
+
const hasRes = ep.responses && Object.keys(ep.responses).length;
|
|
441
|
+
|
|
442
|
+
const tabs = [
|
|
443
|
+
(hasPath||hasQuery) ? 'params' : null,
|
|
444
|
+
hasBody ? 'body' : null,
|
|
445
|
+
hasRes ? 'responses' : null,
|
|
446
|
+
'examples',
|
|
447
|
+
'try'
|
|
448
|
+
].filter(Boolean);
|
|
449
|
+
|
|
450
|
+
const schema = ep.requestBody?.schema || {};
|
|
451
|
+
const bodyEx = JSON.stringify(Object.fromEntries(Object.entries(schema).map(([k,v])=>[k,v.example??''])), null, 2);
|
|
452
|
+
|
|
453
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
454
|
+
<div class="ep-view">
|
|
455
|
+
<div class="ep-topbar">
|
|
456
|
+
<div class="ep-topbar-row1">
|
|
457
|
+
<span class="ep-method-badge m-${ep.method}">${ep.method}</span>
|
|
458
|
+
<span class="ep-path-text">${ep.path}</span>
|
|
459
|
+
${ep.requiresAuth ? `<span class="ep-auth-tag">🔑 Auth required</span>` : ''}
|
|
460
|
+
</div>
|
|
461
|
+
<div class="ep-title">${ep.summary}</div>
|
|
462
|
+
<div class="ep-desc">${ep.description||''}</div>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="ep-tabs">
|
|
465
|
+
${tabs.map((t,i) => `<button class="tab ${i===0?'on':''}" onclick="switchTab(this,'${id}-${t}')">${{params:'Parameters',body:'Body',responses:'Responses',examples:'Examples',try:'Try It ▶'}[t]}</button>`).join('')}
|
|
466
|
+
</div>
|
|
467
|
+
<div class="ep-content">
|
|
468
|
+
${tabs.map((t,i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${buildPanel(ep,t,bodyEx)}</div>`).join('')}
|
|
469
|
+
</div>
|
|
470
|
+
</div>`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function switchTab(btn, panelId) {
|
|
474
|
+
btn.closest('.ep-view').querySelectorAll('.tab').forEach(b=>b.classList.remove('on'));
|
|
475
|
+
btn.classList.add('on');
|
|
476
|
+
btn.closest('.ep-view').querySelector('.ep-content').querySelectorAll(':scope>div').forEach(d=>d.style.display='none');
|
|
477
|
+
document.getElementById(panelId).style.display='block';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function buildPanel(ep, tab, bodyEx) {
|
|
481
|
+
if (tab === 'params') {
|
|
482
|
+
let h = '';
|
|
483
|
+
if (ep.pathParams?.length) {
|
|
484
|
+
h += `<div class="panel-section"><div class="section-label">Path Parameters <span style="color:#ef4444;margin-left:4px">${ep.pathParams.length} Required</span></div>
|
|
485
|
+
<table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
|
|
486
|
+
${ep.pathParams.map(p=>`<tr><td><span class="pname">${p.name}</span><span class="req-tag">REQUIRED</span></td><td><span class="ptype">${p.type}</span></td><td class="pdesc">${p.description}</td><td><span class="pex">${p.example}</span></td></tr>`).join('')}
|
|
487
|
+
</tbody></table></div>`;
|
|
488
|
+
}
|
|
489
|
+
if (ep.queryParams?.length) {
|
|
490
|
+
h += `<div class="panel-section"><div class="section-label">Query Parameters</div>
|
|
491
|
+
<table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
|
|
492
|
+
${ep.queryParams.map(q=>`<tr><td><span class="pname">${q.name}</span></td><td><span class="ptype">${q.type}</span></td><td class="pdesc">${q.description}</td><td><span class="pex">${q.example}</span></td></tr>`).join('')}
|
|
493
|
+
</tbody></table></div>`;
|
|
494
|
+
}
|
|
495
|
+
return h;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (tab === 'body') {
|
|
499
|
+
const schema = ep.requestBody?.schema||{};
|
|
500
|
+
return `<div class="panel-section"><div class="section-label">Schema</div>
|
|
501
|
+
<table class="ptable"><thead><tr><th>FIELD</th><th>TYPE</th><th>REQUIRED</th><th>DESCRIPTION</th></tr></thead><tbody>
|
|
502
|
+
${Object.entries(schema).map(([k,v])=>`<tr><td><span class="pname">${k}</span></td><td><span class="ptype">${v.type}</span></td><td>${v.required?'<span class="req-tag">YES</span>':'<span style="color:var(--text-muted)">—</span>'}</td><td class="pdesc">${v.description||''}</td></tr>`).join('')}
|
|
503
|
+
</tbody></table></div>
|
|
504
|
+
<div class="panel-section"><div class="section-label">Example Body</div><pre>${bodyEx}</pre></div>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (tab === 'responses') {
|
|
508
|
+
return `<div class="panel-section">${Object.entries(ep.responses||{}).map(([code,r])=>{
|
|
509
|
+
const cls = code.startsWith('2')?'s2':code.startsWith('4')?'s4':'s5';
|
|
510
|
+
return `<div class="res-row"><span class="sc ${cls}">${code}</span><div><div class="res-desc">${r.description}</div>${r.example?`<pre style="margin-top:.5rem;font-size:.73rem">${JSON.stringify(r.example,null,2)}</pre>`:''}</div></div>`;
|
|
511
|
+
}).join('')}</div>`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (tab === 'examples') {
|
|
515
|
+
const url = `${spec.baseUrl}${ep.path}`;
|
|
516
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
|
|
517
|
+
const authHeader = ep.requiresAuth ? `\n "Authorization": "Bearer {YOUR_KEY}",` : '';
|
|
518
|
+
const nodeEx = `import fetch from 'node-fetch';\n\nconst url = "${url}";\nconst headers = {${authHeader}\n "Accept": "application/json"${hasBody?',\n "Content-Type": "application/json"':''}\n};\n${hasBody?`\nconst body = ${bodyEx};\n`:''}
|
|
519
|
+
const response = await fetch(url, {\n method: "${ep.method}",\n headers,${hasBody?'\n body: JSON.stringify(body),':''}\n});\nconst data = await response.json();\nconsole.log(data);`;
|
|
520
|
+
|
|
521
|
+
const pyEx = `import requests\n\nurl = "${url}"\nheaders = {${ep.requiresAuth?`\n "Authorization": "Bearer {YOUR_KEY}",`:''}\n "Accept": "application/json"\n}${hasBody?`\n\nbody = ${bodyEx}`:''}
|
|
522
|
+
\nresponse = requests.${ep.method.toLowerCase()}(url, headers=headers${hasBody?', json=body':''})\nprint(response.json())`;
|
|
523
|
+
|
|
524
|
+
const curlEx = `curl -X ${ep.method} "${url}" \\${ep.requiresAuth?`\n -H "Authorization: Bearer {YOUR_KEY}" \\`:''}\n -H "Accept: application/json"${hasBody?` \\\n -H "Content-Type: application/json" \\\n -d '${bodyEx.replace(/\n/g,'').replace(/ /g,'')}'`:''}`;
|
|
525
|
+
|
|
526
|
+
return `<div class="panel-section">
|
|
527
|
+
<div class="section-label">Request Example</div>
|
|
528
|
+
<div class="code-lang-tabs">
|
|
529
|
+
<button class="lang-tab on" onclick="switchLang(this,'lang-node-${ep.id}')">Node.js</button>
|
|
530
|
+
<button class="lang-tab" onclick="switchLang(this,'lang-py-${ep.id}')">Python</button>
|
|
531
|
+
<button class="lang-tab" onclick="switchLang(this,'lang-curl-${ep.id}')">cURL</button>
|
|
532
|
+
</div>
|
|
533
|
+
<pre class="lang-pre" id="lang-node-${ep.id}">${nodeEx}</pre>
|
|
534
|
+
<pre class="lang-pre" id="lang-py-${ep.id}" style="display:none">${pyEx}</pre>
|
|
535
|
+
<pre class="lang-pre" id="lang-curl-${ep.id}" style="display:none">${curlEx}</pre>
|
|
536
|
+
</div>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (tab === 'try') {
|
|
540
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
|
|
541
|
+
return `<div class="try-layout">
|
|
542
|
+
<div class="panel-section">
|
|
543
|
+
<div class="section-label">URL</div>
|
|
544
|
+
<div class="try-url-row">
|
|
545
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
546
|
+
<input type="text" id="try-url-${ep.id}" value="${spec.baseUrl}${ep.path}" />
|
|
547
|
+
<button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">▶ Send</button>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
${ep.requiresAuth?`<div class="panel-section"><div class="section-label">Bearer Token</div>
|
|
551
|
+
<div class="try-token-row"><span>Bearer</span><input type="text" id="try-tok-${ep.id}" placeholder="Paste your token…"/></div></div>`:''}
|
|
552
|
+
${hasBody?`<div class="panel-section"><div class="section-label">Request Body</div>
|
|
553
|
+
<textarea class="try-textarea" id="try-body-${ep.id}">${bodyEx}</textarea></div>`:''}
|
|
554
|
+
<div class="panel-section">
|
|
555
|
+
<div class="section-label">Response</div>
|
|
556
|
+
<div class="res-output" id="try-res-${ep.id}">
|
|
557
|
+
<div class="res-stat" id="try-stat-${ep.id}"></div>
|
|
558
|
+
<div id="try-out-${ep.id}"></div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>`;
|
|
562
|
+
}
|
|
563
|
+
return '';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function switchLang(btn, panelId) {
|
|
567
|
+
const section = btn.closest('.panel-section');
|
|
568
|
+
section.querySelectorAll('.lang-tab').forEach(b=>b.classList.remove('on'));
|
|
569
|
+
btn.classList.add('on');
|
|
570
|
+
section.querySelectorAll('.lang-pre').forEach(p=>p.style.display='none');
|
|
571
|
+
document.getElementById(panelId).style.display='block';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function doSend(id, method, hasBody) {
|
|
575
|
+
const url = document.getElementById(`try-url-${id}`)?.value;
|
|
576
|
+
const tok = document.getElementById(`try-tok-${id}`)?.value;
|
|
577
|
+
const bodyEl = document.getElementById(`try-body-${id}`);
|
|
578
|
+
const resBox = document.getElementById(`try-res-${id}`);
|
|
579
|
+
const statEl = document.getElementById(`try-stat-${id}`);
|
|
580
|
+
const outEl = document.getElementById(`try-out-${id}`);
|
|
581
|
+
resBox.classList.add('show'); outEl.textContent='Sending…'; statEl.innerHTML='';
|
|
582
|
+
const headers = {'Content-Type':'application/json'};
|
|
583
|
+
if (tok) headers['Authorization']=`Bearer ${tok}`;
|
|
584
|
+
const opts = {method,headers};
|
|
585
|
+
if (hasBody && bodyEl) { try{opts.body=JSON.stringify(JSON.parse(bodyEl.value));}catch{opts.body=bodyEl.value;} }
|
|
586
|
+
try {
|
|
587
|
+
const r = await fetch(url,opts);
|
|
588
|
+
const txt = await r.text();
|
|
589
|
+
let display; try{display=JSON.stringify(JSON.parse(txt),null,2);}catch{display=txt;}
|
|
590
|
+
statEl.innerHTML=`<span class="${r.ok?'ok':'bad'}">● ${r.status} ${r.statusText}</span>`;
|
|
591
|
+
outEl.textContent=display;
|
|
592
|
+
} catch(e) { statEl.innerHTML=`<span class="bad">● Network Error</span>`; outEl.textContent=e.message; }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function triggerRescan(full) {
|
|
596
|
+
await fetch(full?'/api/rescan/full':'/api/rescan', {method:'POST'});
|
|
597
|
+
toast(full?'Full rescan started…':'Checking for new endpoints…');
|
|
598
|
+
clearTimeout(pollTimer); pollStatus();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function showError(msg) {
|
|
602
|
+
document.getElementById('mainPanel').innerHTML=`<div class="state-center"><div style="font-size:2rem">⚠️</div><div class="state-title" style="color:var(--DELETE)">Error</div><div class="state-sub">${msg}</div></div>`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function toast(msg) {
|
|
606
|
+
const t=document.createElement('div'); t.className='toast'; t.textContent=msg;
|
|
607
|
+
document.getElementById('toastContainer').appendChild(t);
|
|
608
|
+
setTimeout(()=>t.remove(),3000);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
init();
|
|
612
|
+
</script>
|
|
613
|
+
</body>
|
|
614
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dinorex",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "AI-powered API documentation generator — one command, full docs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dinorex": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
11
|
+
"build": "tsc && cp -r src/public dist/public",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"start": "node dist/server.js",
|
|
14
14
|
"prepare": "npm run build"
|