dinorex 1.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.
- package/README.md +126 -0
- package/package.json +40 -0
- package/src/agent.groq.js +279 -0
- package/src/agent.js +155 -0
- package/src/cli.js +198 -0
- package/src/generators/postman.js +84 -0
- package/src/generators/swagger.js +121 -0
- package/src/public/index.html +654 -0
- package/src/scanner.js +119 -0
- package/src/server.js +136 -0
- package/src/store.js +80 -0
|
@@ -0,0 +1,654 @@
|
|
|
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=Syne:wght@400;600;700;800&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #07070d;
|
|
12
|
+
--panel: #0e0e18;
|
|
13
|
+
--surface: #12121e;
|
|
14
|
+
--surface2: #18182a;
|
|
15
|
+
--border: #1f1f33;
|
|
16
|
+
--border2: #2a2a44;
|
|
17
|
+
--accent: #00e87a;
|
|
18
|
+
--accent-dim: rgba(0,232,122,0.12);
|
|
19
|
+
--accent2: #ff5c35;
|
|
20
|
+
--accent3: #8b7cf8;
|
|
21
|
+
--gold: #f5c842;
|
|
22
|
+
--text: #e2e2f0;
|
|
23
|
+
--text-mid: #9090b8;
|
|
24
|
+
--text-muted: #4a4a6a;
|
|
25
|
+
--GET: #00d4aa; --GET-bg: rgba(0,212,170,0.1);
|
|
26
|
+
--POST: #ff6b35; --POST-bg: rgba(255,107,53,0.1);
|
|
27
|
+
--PUT: #f7c948; --PUT-bg: rgba(247,201,72,0.1);
|
|
28
|
+
--PATCH: #a78bfa; --PATCH-bg: rgba(167,139,250,0.1);
|
|
29
|
+
--DELETE: #ff4d6d; --DELETE-bg: rgba(255,77,109,0.1);
|
|
30
|
+
--sidebar-w: 300px;
|
|
31
|
+
--header-h: 56px;
|
|
32
|
+
}
|
|
33
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
34
|
+
html { scroll-behavior: smooth; }
|
|
35
|
+
body { font-family: 'Syne', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
|
36
|
+
|
|
37
|
+
/* ── Noise overlay ── */
|
|
38
|
+
body::after { content: ''; position: fixed; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); pointer-events: none; z-index: 9999; opacity: 0.4; }
|
|
39
|
+
|
|
40
|
+
/* ── Header ── */
|
|
41
|
+
header {
|
|
42
|
+
height: var(--header-h);
|
|
43
|
+
background: var(--panel);
|
|
44
|
+
border-bottom: 1px solid var(--border);
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
padding: 0 1.25rem;
|
|
48
|
+
gap: 1rem;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
position: relative;
|
|
51
|
+
z-index: 100;
|
|
52
|
+
}
|
|
53
|
+
.logo { display: flex; align-items: center; gap: 9px; text-decoration: none; }
|
|
54
|
+
.logo-dino { font-size: 1.3rem; line-height: 1; }
|
|
55
|
+
.logo-name { font-size: 1.05rem; font-weight: 800; letter-spacing: -0.01em; background: linear-gradient(100deg, var(--accent) 0%, var(--accent3) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
56
|
+
.logo-ver { font-size: 0.65rem; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; margin-left: 4px; -webkit-text-fill-color: var(--text-muted); }
|
|
57
|
+
|
|
58
|
+
.header-divider { width: 1px; height: 24px; background: var(--border); margin: 0 0.25rem; }
|
|
59
|
+
|
|
60
|
+
.project-name-badge { font-size: 0.82rem; color: var(--text-mid); font-weight: 600; }
|
|
61
|
+
|
|
62
|
+
.header-spacer { flex: 1; }
|
|
63
|
+
|
|
64
|
+
.status-pill {
|
|
65
|
+
display: flex; align-items: center; gap: 6px;
|
|
66
|
+
font-size: 0.72rem; font-family: 'JetBrains Mono', monospace;
|
|
67
|
+
color: var(--text-muted); padding: 3px 10px;
|
|
68
|
+
background: var(--surface2); border: 1px solid var(--border); border-radius: 20px;
|
|
69
|
+
}
|
|
70
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); }
|
|
71
|
+
.status-pill.ready .status-dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
|
|
72
|
+
.status-pill.analyzing .status-dot { background: var(--gold); animation: pulse 1s ease-in-out infinite; }
|
|
73
|
+
.status-pill.error .status-dot { background: var(--DELETE); }
|
|
74
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
75
|
+
|
|
76
|
+
.hdr-btn {
|
|
77
|
+
display: flex; align-items: center; gap: 6px;
|
|
78
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
79
|
+
color: var(--text-mid); font-family: 'Syne', sans-serif; font-size: 0.75rem; font-weight: 600;
|
|
80
|
+
padding: 5px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
.hdr-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
84
|
+
.hdr-btn.primary { background: var(--accent); border-color: var(--accent); color: #07070d; }
|
|
85
|
+
.hdr-btn.primary:hover { opacity: 0.85; }
|
|
86
|
+
|
|
87
|
+
/* ── Body layout ── */
|
|
88
|
+
.app-body { display: flex; flex: 1; overflow: hidden; }
|
|
89
|
+
|
|
90
|
+
/* ── Sidebar ── */
|
|
91
|
+
nav {
|
|
92
|
+
width: var(--sidebar-w);
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
background: var(--panel);
|
|
95
|
+
border-right: 1px solid var(--border);
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
.nav-search {
|
|
101
|
+
padding: 0.75rem;
|
|
102
|
+
border-bottom: 1px solid var(--border);
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
}
|
|
105
|
+
.search-input {
|
|
106
|
+
width: 100%; background: var(--surface2); border: 1px solid var(--border);
|
|
107
|
+
border-radius: 6px; padding: 6px 10px 6px 30px;
|
|
108
|
+
color: var(--text); font-family: 'Syne', sans-serif; font-size: 0.8rem;
|
|
109
|
+
outline: none; transition: border-color 0.15s;
|
|
110
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%234a4a6a' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
|
|
111
|
+
background-repeat: no-repeat; background-position: 9px center;
|
|
112
|
+
}
|
|
113
|
+
.search-input:focus { border-color: var(--accent3); }
|
|
114
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
115
|
+
|
|
116
|
+
.nav-list { overflow-y: auto; flex: 1; padding: 0.5rem 0 2rem; }
|
|
117
|
+
.nav-list::-webkit-scrollbar { width: 3px; }
|
|
118
|
+
.nav-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
119
|
+
|
|
120
|
+
.nav-group-label {
|
|
121
|
+
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.12em;
|
|
122
|
+
text-transform: uppercase; color: var(--text-muted);
|
|
123
|
+
padding: 1rem 1rem 0.35rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.nav-item {
|
|
127
|
+
display: flex; align-items: center; gap: 8px;
|
|
128
|
+
padding: 0.45rem 1rem; cursor: pointer;
|
|
129
|
+
border-left: 2px solid transparent;
|
|
130
|
+
transition: all 0.12s;
|
|
131
|
+
border-radius: 0 6px 6px 0;
|
|
132
|
+
margin-right: 8px;
|
|
133
|
+
}
|
|
134
|
+
.nav-item:hover { background: var(--surface2); }
|
|
135
|
+
.nav-item.active { background: var(--accent-dim); border-left-color: var(--accent); }
|
|
136
|
+
.nav-item.active .nav-path { color: var(--text); }
|
|
137
|
+
|
|
138
|
+
.m-pill {
|
|
139
|
+
font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 600;
|
|
140
|
+
padding: 2px 5px; border-radius: 3px; min-width: 44px; text-align: center;
|
|
141
|
+
letter-spacing: 0.03em; flex-shrink: 0;
|
|
142
|
+
}
|
|
143
|
+
.m-GET { color: var(--GET); background: var(--GET-bg); }
|
|
144
|
+
.m-POST { color: var(--POST); background: var(--POST-bg); }
|
|
145
|
+
.m-PUT { color: var(--PUT); background: var(--PUT-bg); }
|
|
146
|
+
.m-PATCH { color: var(--PATCH); background: var(--PATCH-bg); }
|
|
147
|
+
.m-DELETE { color: var(--DELETE); background: var(--DELETE-bg); }
|
|
148
|
+
|
|
149
|
+
.nav-path { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
150
|
+
|
|
151
|
+
/* ── Main panel ── */
|
|
152
|
+
main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
153
|
+
main::-webkit-scrollbar { width: 4px; }
|
|
154
|
+
main::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
155
|
+
|
|
156
|
+
/* Overview page */
|
|
157
|
+
.overview {
|
|
158
|
+
padding: 3rem;
|
|
159
|
+
max-width: 860px;
|
|
160
|
+
}
|
|
161
|
+
.ov-eyebrow { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.75rem; }
|
|
162
|
+
.ov-title { font-size: 2.8rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1.05; margin-bottom: 0.75rem; }
|
|
163
|
+
.ov-desc { color: var(--text-mid); font-size: 0.92rem; line-height: 1.65; max-width: 560px; margin-bottom: 2rem; }
|
|
164
|
+
.ov-stats { display: flex; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 2.5rem; max-width: 520px; }
|
|
165
|
+
.ov-stat { flex: 1; background: var(--surface); padding: 1.25rem 1.5rem; }
|
|
166
|
+
.ov-stat-n { font-size: 1.8rem; font-weight: 800; letter-spacing: -0.03em; background: linear-gradient(135deg, var(--accent), var(--accent3)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
167
|
+
.ov-stat-l { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 2px; }
|
|
168
|
+
|
|
169
|
+
.ov-collections { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
170
|
+
.ov-col-row {
|
|
171
|
+
display: flex; align-items: center; gap: 1rem;
|
|
172
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
173
|
+
padding: 0.9rem 1.25rem; cursor: pointer; transition: all 0.15s;
|
|
174
|
+
}
|
|
175
|
+
.ov-col-row:hover { border-color: var(--border2); background: var(--surface2); }
|
|
176
|
+
.ov-col-name { font-weight: 700; font-size: 0.9rem; flex: 1; }
|
|
177
|
+
.ov-col-desc { font-size: 0.8rem; color: var(--text-mid); flex: 2; }
|
|
178
|
+
.ov-col-count { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--accent); background: var(--accent-dim); padding: 2px 9px; border-radius: 20px; }
|
|
179
|
+
|
|
180
|
+
/* ── Endpoint detail view ── */
|
|
181
|
+
.ep-view { display: flex; flex-direction: column; height: 100%; }
|
|
182
|
+
|
|
183
|
+
.ep-topbar {
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
border-bottom: 1px solid var(--border);
|
|
186
|
+
padding: 1.25rem 2rem;
|
|
187
|
+
background: var(--panel);
|
|
188
|
+
}
|
|
189
|
+
.ep-topbar-row1 { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.4rem; }
|
|
190
|
+
.ep-method-badge {
|
|
191
|
+
font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; font-weight: 700;
|
|
192
|
+
padding: 4px 12px; border-radius: 5px; letter-spacing: 0.05em;
|
|
193
|
+
}
|
|
194
|
+
.ep-path-text { font-family: 'JetBrains Mono', monospace; font-size: 1rem; color: var(--text); }
|
|
195
|
+
.ep-auth-tag { background: rgba(245,200,66,0.1); color: var(--gold); border: 1px solid rgba(245,200,66,0.2); font-size: 0.68rem; font-family: 'JetBrains Mono', monospace; padding: 2px 9px; border-radius: 20px; }
|
|
196
|
+
.ep-summary { font-size: 0.85rem; color: var(--text-mid); }
|
|
197
|
+
|
|
198
|
+
.ep-tabs {
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
display: flex; gap: 0; border-bottom: 1px solid var(--border);
|
|
201
|
+
padding: 0 2rem; background: var(--panel);
|
|
202
|
+
}
|
|
203
|
+
.tab {
|
|
204
|
+
font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 600;
|
|
205
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
206
|
+
color: var(--text-muted); padding: 0.7rem 1rem; cursor: pointer;
|
|
207
|
+
transition: all 0.15s; margin-bottom: -1px;
|
|
208
|
+
}
|
|
209
|
+
.tab:hover { color: var(--text-mid); }
|
|
210
|
+
.tab.on { color: var(--accent); border-bottom-color: var(--accent); }
|
|
211
|
+
|
|
212
|
+
.ep-content { flex: 1; overflow-y: auto; padding: 2rem; }
|
|
213
|
+
.ep-content::-webkit-scrollbar { width: 4px; }
|
|
214
|
+
.ep-content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
215
|
+
|
|
216
|
+
.panel-section { margin-bottom: 1.75rem; }
|
|
217
|
+
.section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-muted); margin-bottom: 0.6rem; }
|
|
218
|
+
|
|
219
|
+
/* Params table */
|
|
220
|
+
.ptable { width: 100%; border-collapse: collapse; }
|
|
221
|
+
.ptable th { text-align: left; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); padding: 0 0.75rem 0.5rem 0; border-bottom: 1px solid var(--border); }
|
|
222
|
+
.ptable td { padding: 0.55rem 0.75rem 0.55rem 0; font-size: 0.83rem; border-bottom: 1px solid rgba(31,31,51,0.6); vertical-align: middle; }
|
|
223
|
+
.pname { font-family: 'JetBrains Mono', monospace; color: var(--accent3); font-size: 0.8rem; }
|
|
224
|
+
.ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-muted); }
|
|
225
|
+
.pdesc { color: var(--text-mid); font-size: 0.8rem; }
|
|
226
|
+
.req-tag { background: rgba(255,77,109,0.1); color: #ff4d6d; font-size: 0.62rem; padding: 1px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; }
|
|
227
|
+
|
|
228
|
+
/* Code */
|
|
229
|
+
pre, .code { background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: #9090b8; overflow-x: auto; white-space: pre; line-height: 1.65; }
|
|
230
|
+
|
|
231
|
+
/* Response rows */
|
|
232
|
+
.res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.65rem 0; border-bottom: 1px solid rgba(31,31,51,0.5); }
|
|
233
|
+
.sc { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; padding: 3px 9px; border-radius: 4px; min-width: 48px; text-align: center; }
|
|
234
|
+
.s2 { background: rgba(0,212,170,0.1); color: var(--GET); }
|
|
235
|
+
.s4 { background: rgba(247,201,72,0.1); color: var(--PUT); }
|
|
236
|
+
.s5 { background: rgba(255,77,109,0.1); color: var(--DELETE); }
|
|
237
|
+
.res-info { flex: 1; }
|
|
238
|
+
.res-desc { font-size: 0.83rem; color: var(--text-mid); margin-bottom: 0.35rem; }
|
|
239
|
+
|
|
240
|
+
/* Try it */
|
|
241
|
+
.try-layout { display: flex; flex-direction: column; gap: 1rem; }
|
|
242
|
+
.try-url-row {
|
|
243
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
244
|
+
background: #050509; border: 1px solid var(--border); border-radius: 8px;
|
|
245
|
+
padding: 0.6rem 0.75rem;
|
|
246
|
+
}
|
|
247
|
+
.try-url-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; flex: 1; outline: none; }
|
|
248
|
+
.send-btn { background: var(--accent); border: none; border-radius: 6px; color: #07070d; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 0.8rem; padding: 6px 18px; cursor: pointer; transition: opacity 0.15s; white-space: nowrap; }
|
|
249
|
+
.send-btn:hover { opacity: 0.85; }
|
|
250
|
+
.send-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
251
|
+
.try-textarea { width: 100%; background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 0.9rem 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 120px; resize: vertical; outline: none; line-height: 1.65; }
|
|
252
|
+
.try-textarea:focus { border-color: var(--accent3); }
|
|
253
|
+
.try-token-row { display: flex; align-items: center; gap: 0.75rem; background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.75rem; }
|
|
254
|
+
.try-token-row span { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-muted); white-space: nowrap; }
|
|
255
|
+
.try-token-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; flex: 1; outline: none; }
|
|
256
|
+
.res-output { background: #050509; 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; }
|
|
257
|
+
.res-output.show { display: block; }
|
|
258
|
+
.res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; }
|
|
259
|
+
.ok { color: var(--accent); } .bad { color: var(--DELETE); }
|
|
260
|
+
|
|
261
|
+
/* Loading / empty states */
|
|
262
|
+
.state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 1.25rem; padding: 4rem; text-align: center; }
|
|
263
|
+
.dino-anim { font-size: 3.5rem; animation: walk 0.6s steps(2) infinite; }
|
|
264
|
+
@keyframes walk { 0% { transform: scaleX(1); } 50% { transform: scaleX(-1); } }
|
|
265
|
+
.state-title { font-size: 1.1rem; font-weight: 700; color: var(--text-mid); }
|
|
266
|
+
.state-sub { font-size: 0.83rem; color: var(--text-muted); max-width: 340px; line-height: 1.5; }
|
|
267
|
+
.progress-bar { width: 240px; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
|
|
268
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent3)); width: 30%; border-radius: 2px; animation: prog 1.4s ease-in-out infinite; }
|
|
269
|
+
@keyframes prog { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
|
|
270
|
+
|
|
271
|
+
/* Toast */
|
|
272
|
+
.toasts { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9000; }
|
|
273
|
+
.toast { background: var(--surface2); border: 1px solid var(--border2); border-radius: 8px; padding: 0.65rem 1.1rem; font-size: 0.82rem; color: var(--text); animation: tin 0.25s ease; }
|
|
274
|
+
@keyframes tin { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<header>
|
|
279
|
+
<a class="logo" href="#" onclick="showOverview()">
|
|
280
|
+
<span class="logo-dino">🦕</span>
|
|
281
|
+
<span class="logo-name">Dinorex</span>
|
|
282
|
+
</a>
|
|
283
|
+
<div class="header-divider"></div>
|
|
284
|
+
<span class="project-name-badge" id="hdrProject">—</span>
|
|
285
|
+
<div class="header-spacer"></div>
|
|
286
|
+
<div class="status-pill" id="statusPill"><span class="status-dot"></span><span id="statusText">Loading</span></div>
|
|
287
|
+
<button class="hdr-btn" onclick="triggerRescan(false)" title="Check for new/changed endpoints">↻ Rescan</button>
|
|
288
|
+
<button class="hdr-btn" onclick="triggerRescan(true)">⟳ Full Rescan</button>
|
|
289
|
+
<button class="hdr-btn" onclick="exportPostman()">⬇ Postman</button>
|
|
290
|
+
<button class="hdr-btn" onclick="exportSwagger()">⬇ Swagger</button>
|
|
291
|
+
</header>
|
|
292
|
+
|
|
293
|
+
<div class="app-body">
|
|
294
|
+
<nav>
|
|
295
|
+
<div class="nav-search">
|
|
296
|
+
<input class="search-input" type="text" placeholder="Search endpoints..." oninput="filterNav(this.value)" />
|
|
297
|
+
</div>
|
|
298
|
+
<div class="nav-list" id="navList"></div>
|
|
299
|
+
</nav>
|
|
300
|
+
<main id="mainPanel">
|
|
301
|
+
<div class="state-center">
|
|
302
|
+
<div class="dino-anim">🦕</div>
|
|
303
|
+
<div class="state-title">Analyzing your API…</div>
|
|
304
|
+
<div class="state-sub">Dinorex is scanning your project and generating docs. This takes about 15 seconds.</div>
|
|
305
|
+
<div class="progress-bar"><div class="progress-fill"></div></div>
|
|
306
|
+
</div>
|
|
307
|
+
</main>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div class="toasts" id="toastContainer"></div>
|
|
311
|
+
|
|
312
|
+
<script>
|
|
313
|
+
let spec = null;
|
|
314
|
+
let activeId = null;
|
|
315
|
+
let pollTimer = null;
|
|
316
|
+
|
|
317
|
+
// ── Bootstrap ────────────────────────────────────────────────────────────
|
|
318
|
+
async function init() {
|
|
319
|
+
pollStatus();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function pollStatus() {
|
|
323
|
+
try {
|
|
324
|
+
const [statusRes, specRes] = await Promise.all([fetch('/api/status'), fetch('/api/spec')]);
|
|
325
|
+
const status = await statusRes.json();
|
|
326
|
+
const specData = await specRes.json();
|
|
327
|
+
|
|
328
|
+
updateStatusPill(status);
|
|
329
|
+
|
|
330
|
+
if (!specData._loading && !specData.error) {
|
|
331
|
+
spec = specData;
|
|
332
|
+
renderNav();
|
|
333
|
+
if (!activeId) showOverview();
|
|
334
|
+
else renderEndpoint(activeId);
|
|
335
|
+
} else if (specData.error) {
|
|
336
|
+
showError(specData.error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (status.state === 'analyzing' || status.state === 'pending') {
|
|
340
|
+
pollTimer = setTimeout(pollStatus, 2000);
|
|
341
|
+
} else if (status.state === 'ready' && specData._loading) {
|
|
342
|
+
pollTimer = setTimeout(pollStatus, 1000);
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
setTimeout(pollStatus, 3000);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function updateStatusPill(s) {
|
|
350
|
+
const pill = document.getElementById('statusPill');
|
|
351
|
+
const txt = document.getElementById('statusText');
|
|
352
|
+
pill.className = 'status-pill ' + s.state;
|
|
353
|
+
txt.textContent = s.state === 'ready' ? 'Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Nav ──────────────────────────────────────────────────────────────────
|
|
357
|
+
function renderNav(filter = '') {
|
|
358
|
+
if (!spec) return;
|
|
359
|
+
const q = filter.toLowerCase();
|
|
360
|
+
let html = '';
|
|
361
|
+
for (const col of spec.collections) {
|
|
362
|
+
const eps = col.endpoints.filter(e =>
|
|
363
|
+
!q || e.path.toLowerCase().includes(q) || e.summary.toLowerCase().includes(q) || e.method.toLowerCase().includes(q)
|
|
364
|
+
);
|
|
365
|
+
if (!eps.length) continue;
|
|
366
|
+
html += `<div class="nav-group-label">${col.name}</div>`;
|
|
367
|
+
for (const ep of eps) {
|
|
368
|
+
html += `<div class="nav-item ${ep.id === activeId ? 'active' : ''}" onclick="selectEndpoint('${ep.id}')">
|
|
369
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
370
|
+
<span class="nav-path">${ep.path}</span>
|
|
371
|
+
</div>`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
document.getElementById('navList').innerHTML = html || `<div style="padding:1rem;font-size:.8rem;color:var(--text-muted)">No results</div>`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function filterNav(v) { renderNav(v); }
|
|
378
|
+
|
|
379
|
+
function selectEndpoint(id) {
|
|
380
|
+
activeId = id;
|
|
381
|
+
renderNav(document.querySelector('.search-input')?.value || '');
|
|
382
|
+
renderEndpoint(id);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Overview ─────────────────────────────────────────────────────────────
|
|
386
|
+
function showOverview() {
|
|
387
|
+
activeId = null;
|
|
388
|
+
renderNav();
|
|
389
|
+
if (!spec) return;
|
|
390
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
391
|
+
const totalEps = spec.collections.reduce((a, c) => a + c.endpoints.length, 0);
|
|
392
|
+
const main = document.getElementById('mainPanel');
|
|
393
|
+
main.innerHTML = `
|
|
394
|
+
<div class="overview">
|
|
395
|
+
<div class="ov-eyebrow">API Documentation</div>
|
|
396
|
+
<div class="ov-title">${spec.projectName}</div>
|
|
397
|
+
<div class="ov-desc">${spec.description || 'Auto-generated API documentation by Dinorex.'}</div>
|
|
398
|
+
<div class="ov-stats">
|
|
399
|
+
<div class="ov-stat"><div class="ov-stat-n">${totalEps}</div><div class="ov-stat-l">Endpoints</div></div>
|
|
400
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.length}</div><div class="ov-stat-l">Collections</div></div>
|
|
401
|
+
<div class="ov-stat"><div class="ov-stat-n">${spec.version}</div><div class="ov-stat-l">Version</div></div>
|
|
402
|
+
<div class="ov-stat"><div class="ov-stat-n" style="font-size:1rem;padding-top:4px">${spec.baseUrl}</div><div class="ov-stat-l">Base URL</div></div>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="ov-collections">
|
|
405
|
+
${spec.collections.map(c => `
|
|
406
|
+
<div class="ov-col-row" onclick="selectEndpoint('${c.endpoints[0]?.id}')">
|
|
407
|
+
<div class="ov-col-name">${c.name}</div>
|
|
408
|
+
<div class="ov-col-desc">${c.description || ''}</div>
|
|
409
|
+
<div class="ov-col-count">${c.endpoints.length} endpoints</div>
|
|
410
|
+
</div>`).join('')}
|
|
411
|
+
</div>
|
|
412
|
+
</div>`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Endpoint detail ───────────────────────────────────────────────────────
|
|
416
|
+
function findEndpoint(id) {
|
|
417
|
+
for (const col of (spec?.collections || [])) {
|
|
418
|
+
const ep = col.endpoints.find(e => e.id === id);
|
|
419
|
+
if (ep) return ep;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderEndpoint(id) {
|
|
425
|
+
const ep = findEndpoint(id);
|
|
426
|
+
if (!ep || !spec) return;
|
|
427
|
+
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
428
|
+
|
|
429
|
+
const hasPath = ep.pathParams?.length;
|
|
430
|
+
const hasQuery = ep.queryParams?.length;
|
|
431
|
+
const hasBody = ep.requestBody && Object.keys(ep.requestBody.schema || {}).length;
|
|
432
|
+
const hasRes = ep.responses && Object.keys(ep.responses).length;
|
|
433
|
+
|
|
434
|
+
const tabs = [
|
|
435
|
+
hasPath || hasQuery ? 'params' : null,
|
|
436
|
+
hasBody ? 'body' : null,
|
|
437
|
+
hasRes ? 'responses' : null,
|
|
438
|
+
'try'
|
|
439
|
+
].filter(Boolean);
|
|
440
|
+
|
|
441
|
+
const main = document.getElementById('mainPanel');
|
|
442
|
+
main.innerHTML = `
|
|
443
|
+
<div class="ep-view">
|
|
444
|
+
<div class="ep-topbar">
|
|
445
|
+
<div class="ep-topbar-row1">
|
|
446
|
+
<span class="ep-method-badge m-${ep.method}">${ep.method}</span>
|
|
447
|
+
<span class="ep-path-text">${ep.path}</span>
|
|
448
|
+
${ep.requiresAuth ? `<span class="ep-auth-tag">🔑 Auth required</span>` : ''}
|
|
449
|
+
</div>
|
|
450
|
+
<div class="ep-summary">${ep.description || ep.summary}</div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="ep-tabs">
|
|
453
|
+
${tabs.map((t, i) => `<button class="tab ${i===0?'on':''}" onclick="switchTab(this,'${id}-${t}')">${{params:'Parameters',body:'Request Body',responses:'Responses',try:'Try It ▶'}[t]}</button>`).join('')}
|
|
454
|
+
</div>
|
|
455
|
+
<div class="ep-content">
|
|
456
|
+
${tabs.map((t, i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${renderPanel(ep, t)}</div>`).join('')}
|
|
457
|
+
</div>
|
|
458
|
+
</div>`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function switchTab(btn, panelId) {
|
|
462
|
+
btn.closest('.ep-view').querySelectorAll('.tab').forEach(b => b.classList.remove('on'));
|
|
463
|
+
btn.classList.add('on');
|
|
464
|
+
const content = btn.closest('.ep-view').querySelector('.ep-content');
|
|
465
|
+
content.querySelectorAll(':scope > div').forEach(d => d.style.display = 'none');
|
|
466
|
+
document.getElementById(panelId).style.display = 'block';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function renderPanel(ep, tab) {
|
|
470
|
+
if (tab === 'params') {
|
|
471
|
+
let html = '';
|
|
472
|
+
if (ep.pathParams?.length) {
|
|
473
|
+
html += `<div class="panel-section">
|
|
474
|
+
<div class="section-label">Path Parameters</div>
|
|
475
|
+
<table class="ptable">
|
|
476
|
+
<thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Example</th></tr></thead>
|
|
477
|
+
<tbody>${ep.pathParams.map(p => `
|
|
478
|
+
<tr>
|
|
479
|
+
<td><span class="pname">${p.name}</span> <span class="req-tag">required</span></td>
|
|
480
|
+
<td><span class="ptype">${p.type}</span></td>
|
|
481
|
+
<td class="pdesc">${p.description}</td>
|
|
482
|
+
<td><span class="ptype">${p.example}</span></td>
|
|
483
|
+
</tr>`).join('')}
|
|
484
|
+
</tbody>
|
|
485
|
+
</table></div>`;
|
|
486
|
+
}
|
|
487
|
+
if (ep.queryParams?.length) {
|
|
488
|
+
html += `<div class="panel-section">
|
|
489
|
+
<div class="section-label">Query Parameters</div>
|
|
490
|
+
<table class="ptable">
|
|
491
|
+
<thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Example</th></tr></thead>
|
|
492
|
+
<tbody>${ep.queryParams.map(q => `
|
|
493
|
+
<tr>
|
|
494
|
+
<td><span class="pname">${q.name}</span></td>
|
|
495
|
+
<td><span class="ptype">${q.type}</span></td>
|
|
496
|
+
<td class="pdesc">${q.description}</td>
|
|
497
|
+
<td><span class="ptype">${q.example}</span></td>
|
|
498
|
+
</tr>`).join('')}
|
|
499
|
+
</tbody>
|
|
500
|
+
</table></div>`;
|
|
501
|
+
}
|
|
502
|
+
return html;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (tab === 'body') {
|
|
506
|
+
const schema = ep.requestBody?.schema || {};
|
|
507
|
+
const example = JSON.stringify(
|
|
508
|
+
Object.fromEntries(Object.entries(schema).map(([k, v]) => [k, v.example ?? ''])),
|
|
509
|
+
null, 2
|
|
510
|
+
);
|
|
511
|
+
return `<div class="panel-section">
|
|
512
|
+
<div class="section-label">Schema</div>
|
|
513
|
+
<table class="ptable">
|
|
514
|
+
<thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Example</th></tr></thead>
|
|
515
|
+
<tbody>${Object.entries(schema).map(([k, v]) => `
|
|
516
|
+
<tr>
|
|
517
|
+
<td><span class="pname">${k}</span></td>
|
|
518
|
+
<td><span class="ptype">${v.type}</span></td>
|
|
519
|
+
<td>${v.required ? '<span class="req-tag">required</span>' : '<span style="color:var(--text-muted)">—</span>'}</td>
|
|
520
|
+
<td><span class="ptype">${v.example ?? ''}</span></td>
|
|
521
|
+
</tr>`).join('')}
|
|
522
|
+
</tbody>
|
|
523
|
+
</table></div>
|
|
524
|
+
<div class="panel-section">
|
|
525
|
+
<div class="section-label">Example JSON</div>
|
|
526
|
+
<pre>${example}</pre>
|
|
527
|
+
</div>`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (tab === 'responses') {
|
|
531
|
+
return `<div class="panel-section">${Object.entries(ep.responses || {}).map(([code, r]) => {
|
|
532
|
+
const cls = code.startsWith('2') ? 's2' : code.startsWith('4') ? 's4' : 's5';
|
|
533
|
+
return `<div class="res-row">
|
|
534
|
+
<span class="sc ${cls}">${code}</span>
|
|
535
|
+
<div class="res-info">
|
|
536
|
+
<div class="res-desc">${r.description}</div>
|
|
537
|
+
${r.example ? `<pre style="margin-top:.5rem;font-size:.75rem">${JSON.stringify(r.example,null,2)}</pre>` : ''}
|
|
538
|
+
</div>
|
|
539
|
+
</div>`;
|
|
540
|
+
}).join('')}</div>`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (tab === 'try') {
|
|
544
|
+
const schema = ep.requestBody?.schema || {};
|
|
545
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(schema).length;
|
|
546
|
+
const bodyEx = JSON.stringify(
|
|
547
|
+
Object.fromEntries(Object.entries(schema).map(([k,v]) => [k, v.example ?? ''])),
|
|
548
|
+
null, 2
|
|
549
|
+
);
|
|
550
|
+
return `<div class="try-layout">
|
|
551
|
+
<div class="panel-section">
|
|
552
|
+
<div class="section-label">URL</div>
|
|
553
|
+
<div class="try-url-row">
|
|
554
|
+
<span class="m-pill m-${ep.method}" style="font-size:.7rem">${ep.method}</span>
|
|
555
|
+
<input type="text" id="try-url-${ep.id}" value="${spec.baseUrl}${ep.path}" />
|
|
556
|
+
<button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">Send</button>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
${ep.requiresAuth ? `<div class="panel-section">
|
|
560
|
+
<div class="section-label">Bearer Token</div>
|
|
561
|
+
<div class="try-token-row"><span>Bearer</span><input type="text" id="try-tok-${ep.id}" placeholder="Paste your token…" /></div>
|
|
562
|
+
</div>` : ''}
|
|
563
|
+
${hasBody ? `<div class="panel-section">
|
|
564
|
+
<div class="section-label">Request Body</div>
|
|
565
|
+
<textarea class="try-textarea" id="try-body-${ep.id}">${bodyEx}</textarea>
|
|
566
|
+
</div>` : ''}
|
|
567
|
+
<div class="panel-section">
|
|
568
|
+
<div class="section-label">Response</div>
|
|
569
|
+
<div class="res-output" id="try-res-${ep.id}">
|
|
570
|
+
<div class="res-stat" id="try-stat-${ep.id}"></div>
|
|
571
|
+
<div id="try-body-out-${ep.id}"></div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>`;
|
|
575
|
+
}
|
|
576
|
+
return '';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function doSend(id, method, hasBody) {
|
|
580
|
+
const url = document.getElementById(`try-url-${id}`)?.value;
|
|
581
|
+
const tok = document.getElementById(`try-tok-${id}`)?.value;
|
|
582
|
+
const bodyEl = document.getElementById(`try-body-${id}`);
|
|
583
|
+
const resBox = document.getElementById(`try-res-${id}`);
|
|
584
|
+
const statEl = document.getElementById(`try-stat-${id}`);
|
|
585
|
+
const bodyOut = document.getElementById(`try-body-out-${id}`);
|
|
586
|
+
|
|
587
|
+
resBox.classList.add('show');
|
|
588
|
+
bodyOut.textContent = 'Sending…';
|
|
589
|
+
statEl.innerHTML = '';
|
|
590
|
+
|
|
591
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
592
|
+
if (tok) headers['Authorization'] = `Bearer ${tok}`;
|
|
593
|
+
const opts = { method, headers };
|
|
594
|
+
if (hasBody && bodyEl) { try { opts.body = JSON.stringify(JSON.parse(bodyEl.value)); } catch { opts.body = bodyEl.value; } }
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const r = await fetch(url, opts);
|
|
598
|
+
const txt = await r.text();
|
|
599
|
+
let display; try { display = JSON.stringify(JSON.parse(txt), null, 2); } catch { display = txt; }
|
|
600
|
+
statEl.innerHTML = `<span class="${r.ok?'ok':'bad'}">● ${r.status} ${r.statusText}</span>`;
|
|
601
|
+
bodyOut.textContent = display;
|
|
602
|
+
} catch(e) {
|
|
603
|
+
statEl.innerHTML = `<span class="bad">● Network Error</span>`;
|
|
604
|
+
bodyOut.textContent = e.message;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ── Rescan ───────────────────────────────────────────────────────────────
|
|
609
|
+
async function triggerRescan(full) {
|
|
610
|
+
const url = full ? '/api/rescan/full' : '/api/rescan';
|
|
611
|
+
await fetch(url, { method: 'POST' });
|
|
612
|
+
toast(full ? 'Full rescan started…' : 'Checking for new endpoints…');
|
|
613
|
+
clearTimeout(pollTimer);
|
|
614
|
+
pollStatus();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Exports ──────────────────────────────────────────────────────────────
|
|
618
|
+
async function exportPostman() {
|
|
619
|
+
if (!spec) return toast('Not ready yet');
|
|
620
|
+
const r = await fetch('/api/export/postman');
|
|
621
|
+
const blob = await r.blob();
|
|
622
|
+
dl(blob, `${spec.projectName.replace(/\s+/g,'-')}-postman.json`);
|
|
623
|
+
toast('Postman collection downloaded!');
|
|
624
|
+
}
|
|
625
|
+
async function exportSwagger() {
|
|
626
|
+
if (!spec) return toast('Not ready yet');
|
|
627
|
+
const r = await fetch('/api/export/swagger');
|
|
628
|
+
const blob = await r.blob();
|
|
629
|
+
dl(blob, `${spec.projectName.replace(/\s+/g,'-')}-openapi.yaml`);
|
|
630
|
+
toast('Swagger YAML downloaded!');
|
|
631
|
+
}
|
|
632
|
+
function dl(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); }
|
|
633
|
+
|
|
634
|
+
// ── Toast ────────────────────────────────────────────────────────────────
|
|
635
|
+
function toast(msg) {
|
|
636
|
+
const t = document.createElement('div');
|
|
637
|
+
t.className = 'toast'; t.textContent = msg;
|
|
638
|
+
document.getElementById('toastContainer').appendChild(t);
|
|
639
|
+
setTimeout(() => t.remove(), 3000);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function showError(msg) {
|
|
643
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
644
|
+
<div class="state-center">
|
|
645
|
+
<div style="font-size:2.5rem">⚠️</div>
|
|
646
|
+
<div class="state-title" style="color:var(--DELETE)">Analysis Error</div>
|
|
647
|
+
<div class="state-sub">${msg}</div>
|
|
648
|
+
</div>`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
init();
|
|
652
|
+
</script>
|
|
653
|
+
</body>
|
|
654
|
+
</html>
|