dinorex 1.0.0 → 1.0.1
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/package.json +1 -1
- package/src/public/index.html +356 -396
package/package.json
CHANGED
package/src/public/index.html
CHANGED
|
@@ -5,140 +5,134 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Dinorex</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
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
9
|
<style>
|
|
10
10
|
:root {
|
|
11
|
-
--bg: #
|
|
12
|
-
--panel: #
|
|
13
|
-
--surface: #
|
|
14
|
-
--surface2: #
|
|
15
|
-
--border: #
|
|
16
|
-
--border2: #
|
|
17
|
-
--accent: #
|
|
18
|
-
--accent-dim: rgba(0,
|
|
19
|
-
--accent2: #
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--text: #
|
|
23
|
-
--text-
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--header-h: 56px;
|
|
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;
|
|
32
31
|
}
|
|
33
32
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
34
|
-
|
|
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; }
|
|
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; }
|
|
39
34
|
|
|
40
35
|
/* ── Header ── */
|
|
41
36
|
header {
|
|
42
37
|
height: var(--header-h);
|
|
43
38
|
background: var(--panel);
|
|
44
39
|
border-bottom: 1px solid var(--border);
|
|
45
|
-
display: flex;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
gap: 1rem;
|
|
49
|
-
flex-shrink: 0;
|
|
50
|
-
position: relative;
|
|
51
|
-
z-index: 100;
|
|
40
|
+
display: flex; align-items: center;
|
|
41
|
+
padding: 0 1rem; gap: 0.75rem;
|
|
42
|
+
flex-shrink: 0; z-index: 100;
|
|
52
43
|
}
|
|
53
|
-
.logo { display: flex; align-items: center; gap:
|
|
54
|
-
.logo-dino { font-size: 1.
|
|
55
|
-
.logo-name { font-size:
|
|
56
|
-
.logo-
|
|
57
|
-
|
|
58
|
-
.header-divider { width: 1px; height: 24px; background: var(--border); margin: 0 0.25rem; }
|
|
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); }
|
|
59
48
|
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
.
|
|
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; }
|
|
63
52
|
|
|
64
53
|
.status-pill {
|
|
65
|
-
display: flex; align-items: center; gap:
|
|
66
|
-
font-size: 0.72rem; font-
|
|
54
|
+
display: flex; align-items: center; gap: 5px;
|
|
55
|
+
font-size: 0.72rem; font-weight: 500;
|
|
67
56
|
color: var(--text-muted); padding: 3px 10px;
|
|
68
|
-
background: var(--
|
|
57
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 20px;
|
|
69
58
|
}
|
|
70
|
-
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); }
|
|
59
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
|
71
60
|
.status-pill.ready .status-dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
|
|
72
|
-
.status-pill.
|
|
61
|
+
.status-pill.ready { color: var(--text-mid); }
|
|
62
|
+
.status-pill.analyzing .status-dot { background: var(--gold); animation: blink 1s ease-in-out infinite; }
|
|
73
63
|
.status-pill.error .status-dot { background: var(--DELETE); }
|
|
74
|
-
@keyframes
|
|
64
|
+
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
|
|
75
65
|
|
|
76
66
|
.hdr-btn {
|
|
77
|
-
display: flex; align-items: center; gap:
|
|
78
|
-
background: var(--
|
|
79
|
-
color: var(--text-mid); font-family: '
|
|
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;
|
|
80
71
|
padding: 5px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
|
81
|
-
white-space: nowrap;
|
|
82
72
|
}
|
|
83
|
-
.hdr-btn:hover { border-color: var(--
|
|
84
|
-
.hdr-btn
|
|
85
|
-
.hdr-btn.primary:hover { opacity: 0.85; }
|
|
73
|
+
.hdr-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
|
|
74
|
+
.hdr-btn svg { width: 13px; height: 13px; }
|
|
86
75
|
|
|
87
|
-
/* ──
|
|
76
|
+
/* ── Layout ── */
|
|
88
77
|
.app-body { display: flex; flex: 1; overflow: hidden; }
|
|
89
78
|
|
|
90
79
|
/* ── Sidebar ── */
|
|
91
80
|
nav {
|
|
92
|
-
width: var(--sidebar-w);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
border-right: 1px solid var(--border);
|
|
96
|
-
display: flex;
|
|
97
|
-
flex-direction: column;
|
|
98
|
-
overflow: hidden;
|
|
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;
|
|
99
84
|
}
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
|
|
86
|
+
.nav-header {
|
|
87
|
+
padding: 0.6rem 0.75rem;
|
|
102
88
|
border-bottom: 1px solid var(--border);
|
|
103
89
|
flex-shrink: 0;
|
|
104
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; }
|
|
105
94
|
.search-input {
|
|
106
|
-
width: 100%; background: var(--
|
|
107
|
-
border-radius: 6px; padding:
|
|
108
|
-
color: var(--text); font-family: '
|
|
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;
|
|
109
98
|
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
99
|
}
|
|
113
|
-
.search-input:focus { border-color: var(--
|
|
100
|
+
.search-input:focus { border-color: var(--accent2); }
|
|
114
101
|
.search-input::placeholder { color: var(--text-muted); }
|
|
115
102
|
|
|
116
103
|
.nav-list { overflow-y: auto; flex: 1; padding: 0.5rem 0 2rem; }
|
|
117
104
|
.nav-list::-webkit-scrollbar { width: 3px; }
|
|
118
105
|
.nav-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
119
106
|
|
|
120
|
-
.nav-group-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
padding:
|
|
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;
|
|
124
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; }
|
|
125
122
|
|
|
126
123
|
.nav-item {
|
|
127
|
-
display: flex; align-items: center; gap:
|
|
128
|
-
padding: 0.
|
|
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;
|
|
129
127
|
border-left: 2px solid transparent;
|
|
130
|
-
transition: all 0.12s;
|
|
131
|
-
border-radius: 0 6px 6px 0;
|
|
132
|
-
margin-right: 8px;
|
|
133
128
|
}
|
|
134
|
-
.nav-item:hover { background: var(--
|
|
129
|
+
.nav-item:hover { background: var(--surface); }
|
|
135
130
|
.nav-item.active { background: var(--accent-dim); border-left-color: var(--accent); }
|
|
136
131
|
.nav-item.active .nav-path { color: var(--text); }
|
|
137
132
|
|
|
138
133
|
.m-pill {
|
|
139
134
|
font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 600;
|
|
140
|
-
padding:
|
|
141
|
-
letter-spacing: 0.03em; flex-shrink: 0;
|
|
135
|
+
padding: 1px 5px; border-radius: 3px; min-width: 42px; text-align: center; flex-shrink: 0;
|
|
142
136
|
}
|
|
143
137
|
.m-GET { color: var(--GET); background: var(--GET-bg); }
|
|
144
138
|
.m-POST { color: var(--POST); background: var(--POST-bg); }
|
|
@@ -148,160 +142,172 @@
|
|
|
148
142
|
|
|
149
143
|
.nav-path { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
150
144
|
|
|
151
|
-
/* ── Main
|
|
152
|
-
main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
145
|
+
/* ── Main ── */
|
|
146
|
+
main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; background: var(--bg); }
|
|
153
147
|
main::-webkit-scrollbar { width: 4px; }
|
|
154
148
|
main::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
155
149
|
|
|
156
|
-
/* Overview
|
|
157
|
-
.overview {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
.ov-
|
|
162
|
-
.ov-
|
|
163
|
-
.ov-
|
|
164
|
-
.ov-
|
|
165
|
-
.ov-
|
|
166
|
-
|
|
167
|
-
.ov-
|
|
168
|
-
|
|
169
|
-
.ov-
|
|
170
|
-
.ov-col-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 ── */
|
|
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 ── */
|
|
181
170
|
.ep-view { display: flex; flex-direction: column; height: 100%; }
|
|
182
171
|
|
|
183
|
-
.ep-topbar {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
.ep-
|
|
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); }
|
|
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; }
|
|
197
179
|
|
|
198
|
-
.ep-tabs {
|
|
199
|
-
|
|
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
|
-
}
|
|
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; }
|
|
209
182
|
.tab:hover { color: var(--text-mid); }
|
|
210
|
-
.tab.on { color: var(--
|
|
183
|
+
.tab.on { color: var(--text); border-bottom-color: var(--accent2); }
|
|
211
184
|
|
|
212
|
-
.ep-content { flex: 1; overflow-y: auto; padding: 2rem; }
|
|
185
|
+
.ep-content { flex: 1; overflow-y: auto; padding: 1.5rem 2rem; }
|
|
213
186
|
.ep-content::-webkit-scrollbar { width: 4px; }
|
|
214
187
|
.ep-content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
215
188
|
|
|
216
|
-
|
|
217
|
-
.
|
|
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; }
|
|
218
211
|
|
|
219
|
-
/*
|
|
212
|
+
/* Table */
|
|
220
213
|
.ptable { width: 100%; border-collapse: collapse; }
|
|
221
|
-
.ptable th { text-align: left; font-size: 0.
|
|
222
|
-
.ptable td { padding: 0.55rem
|
|
223
|
-
.pname { font-family: 'JetBrains Mono', monospace; color: var(--
|
|
224
|
-
.ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--
|
|
225
|
-
.pdesc { color: var(--text-mid);
|
|
226
|
-
.
|
|
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; }
|
|
227
221
|
|
|
228
222
|
/* Code */
|
|
229
|
-
pre
|
|
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; }
|
|
230
231
|
|
|
231
232
|
/* Response rows */
|
|
232
|
-
.res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.
|
|
233
|
-
.sc { font-family: 'JetBrains Mono', monospace; font-size: 0.
|
|
234
|
-
.s2 { background: rgba(
|
|
235
|
-
.s4 { background: rgba(
|
|
236
|
-
.s5 { background: rgba(
|
|
237
|
-
.res-
|
|
238
|
-
.res-desc { font-size: 0.83rem; color: var(--text-mid); margin-bottom: 0.35rem; }
|
|
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
239
|
|
|
240
240
|
/* Try it */
|
|
241
241
|
.try-layout { display: flex; flex-direction: column; gap: 1rem; }
|
|
242
|
-
.try-url-row {
|
|
243
|
-
|
|
244
|
-
|
|
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; }
|
|
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; }
|
|
249
245
|
.send-btn:hover { opacity: 0.85; }
|
|
250
|
-
.
|
|
251
|
-
.try-textarea {
|
|
252
|
-
.try-
|
|
253
|
-
.try-token-row {
|
|
254
|
-
.try-token-row
|
|
255
|
-
.
|
|
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; }
|
|
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; }
|
|
257
252
|
.res-output.show { display: block; }
|
|
258
|
-
.res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; }
|
|
259
|
-
.ok { color: var(--
|
|
260
|
-
|
|
261
|
-
/* Loading
|
|
262
|
-
.state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap:
|
|
263
|
-
.dino-anim { font-size:
|
|
264
|
-
@keyframes walk { 0%
|
|
265
|
-
.state-title { font-size:
|
|
266
|
-
.state-sub { font-size: 0.
|
|
267
|
-
.progress-bar { width:
|
|
268
|
-
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--
|
|
269
|
-
@keyframes prog { 0%
|
|
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%)} }
|
|
270
265
|
|
|
271
266
|
/* Toast */
|
|
272
267
|
.toasts { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9000; }
|
|
273
|
-
.toast { background: var(--
|
|
274
|
-
@keyframes tin { from
|
|
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} }
|
|
275
270
|
</style>
|
|
276
271
|
</head>
|
|
277
272
|
<body>
|
|
278
273
|
<header>
|
|
279
|
-
<a class="logo" href="#" onclick="showOverview()">
|
|
274
|
+
<a class="logo" href="#" onclick="showOverview();return false;">
|
|
280
275
|
<span class="logo-dino">🦕</span>
|
|
281
|
-
<span class="logo-name">
|
|
276
|
+
<span class="logo-name">Dino<span>rex</span></span>
|
|
282
277
|
</a>
|
|
283
|
-
<div class="
|
|
284
|
-
<span class="project
|
|
285
|
-
<div class="
|
|
286
|
-
<div class="status-pill" id="statusPill"
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
<button class="hdr-btn" onclick="
|
|
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>
|
|
291
293
|
</header>
|
|
292
294
|
|
|
293
295
|
<div class="app-body">
|
|
294
296
|
<nav>
|
|
295
|
-
<div class="nav-
|
|
296
|
-
<
|
|
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>
|
|
297
302
|
</div>
|
|
298
303
|
<div class="nav-list" id="navList"></div>
|
|
299
304
|
</nav>
|
|
305
|
+
|
|
300
306
|
<main id="mainPanel">
|
|
301
307
|
<div class="state-center">
|
|
302
308
|
<div class="dino-anim">🦕</div>
|
|
303
309
|
<div class="state-title">Analyzing your API…</div>
|
|
304
|
-
<div class="state-sub">
|
|
310
|
+
<div class="state-sub">Scanning routes, controllers, services and models. This takes about 15 seconds.</div>
|
|
305
311
|
<div class="progress-bar"><div class="progress-fill"></div></div>
|
|
306
312
|
</div>
|
|
307
313
|
</main>
|
|
@@ -314,83 +320,82 @@
|
|
|
314
320
|
let activeId = null;
|
|
315
321
|
let pollTimer = null;
|
|
316
322
|
|
|
317
|
-
|
|
318
|
-
async function init() {
|
|
319
|
-
pollStatus();
|
|
320
|
-
}
|
|
323
|
+
async function init() { pollStatus(); }
|
|
321
324
|
|
|
322
325
|
async function pollStatus() {
|
|
323
326
|
try {
|
|
324
|
-
const [
|
|
325
|
-
const status = await
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (!specData._loading && !specData.error) {
|
|
331
|
-
spec = specData;
|
|
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;
|
|
332
333
|
renderNav();
|
|
333
334
|
if (!activeId) showOverview();
|
|
334
335
|
else renderEndpoint(activeId);
|
|
335
|
-
} else if (
|
|
336
|
-
showError(
|
|
336
|
+
} else if (data.error) {
|
|
337
|
+
showError(data.error);
|
|
337
338
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
pollTimer = setTimeout(pollStatus, 2000);
|
|
341
|
-
} else if (status.state === 'ready' && specData._loading) {
|
|
342
|
-
pollTimer = setTimeout(pollStatus, 1000);
|
|
339
|
+
if (status.state === 'analyzing' || status.state === 'pending' || (status.state === 'ready' && data._loading)) {
|
|
340
|
+
pollTimer = setTimeout(pollStatus, status.state === 'ready' ? 1000 : 2000);
|
|
343
341
|
}
|
|
344
|
-
} catch (
|
|
345
|
-
setTimeout(pollStatus, 3000);
|
|
346
|
-
}
|
|
342
|
+
} catch { setTimeout(pollStatus, 3000); }
|
|
347
343
|
}
|
|
348
344
|
|
|
349
|
-
function
|
|
345
|
+
function updateStatus(s) {
|
|
350
346
|
const pill = document.getElementById('statusPill');
|
|
351
347
|
const txt = document.getElementById('statusText');
|
|
352
348
|
pill.className = 'status-pill ' + s.state;
|
|
353
|
-
txt.textContent = s.state === 'ready' ? 'Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
|
|
349
|
+
txt.textContent = s.state === 'ready' ? 'System Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
|
|
354
350
|
}
|
|
355
351
|
|
|
356
352
|
// ── Nav ──────────────────────────────────────────────────────────────────
|
|
353
|
+
const colIcons = ['📁','📂','🗂️','📋','🔧','⚡','🛡️','👤','🏪','💳'];
|
|
354
|
+
function colIcon(i) { return colIcons[i % colIcons.length]; }
|
|
355
|
+
|
|
357
356
|
function renderNav(filter = '') {
|
|
358
357
|
if (!spec) return;
|
|
359
358
|
const q = filter.toLowerCase();
|
|
360
359
|
let html = '';
|
|
361
|
-
|
|
360
|
+
spec.collections.forEach((col, ci) => {
|
|
362
361
|
const eps = col.endpoints.filter(e =>
|
|
363
362
|
!q || e.path.toLowerCase().includes(q) || e.summary.toLowerCase().includes(q) || e.method.toLowerCase().includes(q)
|
|
364
363
|
);
|
|
365
|
-
if (!eps.length)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<span class="
|
|
370
|
-
<span
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
});
|
|
374
382
|
document.getElementById('navList').innerHTML = html || `<div style="padding:1rem;font-size:.8rem;color:var(--text-muted)">No results</div>`;
|
|
375
383
|
}
|
|
376
384
|
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
function selectEndpoint(id) {
|
|
380
|
-
activeId = id;
|
|
381
|
-
renderNav(document.querySelector('.search-input')?.value || '');
|
|
382
|
-
renderEndpoint(id);
|
|
385
|
+
function toggleGroup(ci) {
|
|
386
|
+
document.getElementById(`ng-${ci}`)?.classList.toggle('open');
|
|
383
387
|
}
|
|
384
388
|
|
|
385
|
-
|
|
389
|
+
function filterNav(v) { renderNav(v); }
|
|
390
|
+
function selectEndpoint(id) { activeId = id; renderNav(document.querySelector('.search-input')?.value||''); renderEndpoint(id); }
|
|
391
|
+
|
|
392
|
+
// ── Overview ──────────────────────────────────────────────────────────────
|
|
386
393
|
function showOverview() {
|
|
387
|
-
activeId = null;
|
|
388
|
-
renderNav();
|
|
394
|
+
activeId = null; renderNav();
|
|
389
395
|
if (!spec) return;
|
|
390
396
|
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
391
|
-
const totalEps = spec.collections.reduce((a,
|
|
392
|
-
|
|
393
|
-
main.innerHTML = `
|
|
397
|
+
const totalEps = spec.collections.reduce((a,c) => a+c.endpoints.length, 0);
|
|
398
|
+
document.getElementById('mainPanel').innerHTML = `
|
|
394
399
|
<div class="overview">
|
|
395
400
|
<div class="ov-eyebrow">API Documentation</div>
|
|
396
401
|
<div class="ov-title">${spec.projectName}</div>
|
|
@@ -399,22 +404,25 @@
|
|
|
399
404
|
<div class="ov-stat"><div class="ov-stat-n">${totalEps}</div><div class="ov-stat-l">Endpoints</div></div>
|
|
400
405
|
<div class="ov-stat"><div class="ov-stat-n">${spec.collections.length}</div><div class="ov-stat-l">Collections</div></div>
|
|
401
406
|
<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"
|
|
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>
|
|
403
408
|
</div>
|
|
404
|
-
<div class="ov-
|
|
405
|
-
|
|
406
|
-
|
|
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>
|
|
407
415
|
<div class="ov-col-name">${c.name}</div>
|
|
408
|
-
<div class="ov-col-desc">${c.description
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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('')}
|
|
412
420
|
</div>`;
|
|
413
421
|
}
|
|
414
422
|
|
|
415
|
-
// ── Endpoint
|
|
423
|
+
// ── Endpoint ──────────────────────────────────────────────────────────────
|
|
416
424
|
function findEndpoint(id) {
|
|
417
|
-
for (const col of (spec?.collections
|
|
425
|
+
for (const col of (spec?.collections||[])) {
|
|
418
426
|
const ep = col.endpoints.find(e => e.id === id);
|
|
419
427
|
if (ep) return ep;
|
|
420
428
|
}
|
|
@@ -426,20 +434,23 @@
|
|
|
426
434
|
if (!ep || !spec) return;
|
|
427
435
|
document.getElementById('hdrProject').textContent = spec.projectName;
|
|
428
436
|
|
|
429
|
-
const hasPath
|
|
430
|
-
const hasQuery
|
|
431
|
-
const hasBody
|
|
432
|
-
const hasRes
|
|
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;
|
|
433
441
|
|
|
434
442
|
const tabs = [
|
|
435
|
-
hasPath
|
|
443
|
+
(hasPath||hasQuery) ? 'params' : null,
|
|
436
444
|
hasBody ? 'body' : null,
|
|
437
445
|
hasRes ? 'responses' : null,
|
|
446
|
+
'examples',
|
|
438
447
|
'try'
|
|
439
448
|
].filter(Boolean);
|
|
440
449
|
|
|
441
|
-
const
|
|
442
|
-
|
|
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 = `
|
|
443
454
|
<div class="ep-view">
|
|
444
455
|
<div class="ep-topbar">
|
|
445
456
|
<div class="ep-topbar-row1">
|
|
@@ -447,128 +458,104 @@
|
|
|
447
458
|
<span class="ep-path-text">${ep.path}</span>
|
|
448
459
|
${ep.requiresAuth ? `<span class="ep-auth-tag">🔑 Auth required</span>` : ''}
|
|
449
460
|
</div>
|
|
450
|
-
<div class="ep-
|
|
461
|
+
<div class="ep-title">${ep.summary}</div>
|
|
462
|
+
<div class="ep-desc">${ep.description||''}</div>
|
|
451
463
|
</div>
|
|
452
464
|
<div class="ep-tabs">
|
|
453
|
-
${tabs.map((t,
|
|
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('')}
|
|
454
466
|
</div>
|
|
455
467
|
<div class="ep-content">
|
|
456
|
-
${tabs.map((t,
|
|
468
|
+
${tabs.map((t,i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${buildPanel(ep,t,bodyEx)}</div>`).join('')}
|
|
457
469
|
</div>
|
|
458
470
|
</div>`;
|
|
459
471
|
}
|
|
460
472
|
|
|
461
473
|
function switchTab(btn, panelId) {
|
|
462
|
-
btn.closest('.ep-view').querySelectorAll('.tab').forEach(b
|
|
474
|
+
btn.closest('.ep-view').querySelectorAll('.tab').forEach(b=>b.classList.remove('on'));
|
|
463
475
|
btn.classList.add('on');
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
document.getElementById(panelId).style.display = 'block';
|
|
476
|
+
btn.closest('.ep-view').querySelector('.ep-content').querySelectorAll(':scope>div').forEach(d=>d.style.display='none');
|
|
477
|
+
document.getElementById(panelId).style.display='block';
|
|
467
478
|
}
|
|
468
479
|
|
|
469
|
-
function
|
|
480
|
+
function buildPanel(ep, tab, bodyEx) {
|
|
470
481
|
if (tab === 'params') {
|
|
471
|
-
let
|
|
482
|
+
let h = '';
|
|
472
483
|
if (ep.pathParams?.length) {
|
|
473
|
-
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
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>`;
|
|
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>`;
|
|
486
488
|
}
|
|
487
489
|
if (ep.queryParams?.length) {
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
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>`;
|
|
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>`;
|
|
501
494
|
}
|
|
502
|
-
return
|
|
495
|
+
return h;
|
|
503
496
|
}
|
|
504
497
|
|
|
505
498
|
if (tab === 'body') {
|
|
506
|
-
const schema = ep.requestBody?.schema
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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>`;
|
|
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>`;
|
|
528
505
|
}
|
|
529
506
|
|
|
530
507
|
if (tab === 'responses') {
|
|
531
|
-
return `<div class="panel-section">${Object.entries(ep.responses
|
|
532
|
-
const cls = code.startsWith('2')
|
|
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>`;
|
|
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>`;
|
|
540
511
|
}).join('')}</div>`;
|
|
541
512
|
}
|
|
542
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
|
+
|
|
543
539
|
if (tab === 'try') {
|
|
544
|
-
const
|
|
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
|
-
);
|
|
540
|
+
const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
|
|
550
541
|
return `<div class="try-layout">
|
|
551
542
|
<div class="panel-section">
|
|
552
543
|
<div class="section-label">URL</div>
|
|
553
544
|
<div class="try-url-row">
|
|
554
|
-
<span class="m-pill m-${ep.method}"
|
|
545
|
+
<span class="m-pill m-${ep.method}">${ep.method}</span>
|
|
555
546
|
<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})"
|
|
547
|
+
<button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">▶ Send</button>
|
|
557
548
|
</div>
|
|
558
549
|
</div>
|
|
559
|
-
${ep.requiresAuth
|
|
560
|
-
<div class="
|
|
561
|
-
|
|
562
|
-
|
|
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>` : ''}
|
|
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>`:''}
|
|
567
554
|
<div class="panel-section">
|
|
568
555
|
<div class="section-label">Response</div>
|
|
569
556
|
<div class="res-output" id="try-res-${ep.id}">
|
|
570
557
|
<div class="res-stat" id="try-stat-${ep.id}"></div>
|
|
571
|
-
<div id="try-
|
|
558
|
+
<div id="try-out-${ep.id}"></div>
|
|
572
559
|
</div>
|
|
573
560
|
</div>
|
|
574
561
|
</div>`;
|
|
@@ -576,76 +563,49 @@
|
|
|
576
563
|
return '';
|
|
577
564
|
}
|
|
578
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
|
+
|
|
579
574
|
async function doSend(id, method, hasBody) {
|
|
580
575
|
const url = document.getElementById(`try-url-${id}`)?.value;
|
|
581
576
|
const tok = document.getElementById(`try-tok-${id}`)?.value;
|
|
582
577
|
const bodyEl = document.getElementById(`try-body-${id}`);
|
|
583
578
|
const resBox = document.getElementById(`try-res-${id}`);
|
|
584
579
|
const statEl = document.getElementById(`try-stat-${id}`);
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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;} }
|
|
596
586
|
try {
|
|
597
|
-
const r = await fetch(url,
|
|
587
|
+
const r = await fetch(url,opts);
|
|
598
588
|
const txt = await r.text();
|
|
599
|
-
let display; try
|
|
600
|
-
statEl.innerHTML
|
|
601
|
-
|
|
602
|
-
} catch(e) {
|
|
603
|
-
statEl.innerHTML = `<span class="bad">● Network Error</span>`;
|
|
604
|
-
bodyOut.textContent = e.message;
|
|
605
|
-
}
|
|
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; }
|
|
606
593
|
}
|
|
607
594
|
|
|
608
|
-
// ── Rescan ───────────────────────────────────────────────────────────────
|
|
609
595
|
async function triggerRescan(full) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
clearTimeout(pollTimer);
|
|
614
|
-
pollStatus();
|
|
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();
|
|
615
599
|
}
|
|
616
600
|
|
|
617
|
-
|
|
618
|
-
|
|
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!');
|
|
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>`;
|
|
631
603
|
}
|
|
632
|
-
function dl(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); }
|
|
633
604
|
|
|
634
|
-
// ── Toast ────────────────────────────────────────────────────────────────
|
|
635
605
|
function toast(msg) {
|
|
636
|
-
const t
|
|
637
|
-
t.className = 'toast'; t.textContent = msg;
|
|
606
|
+
const t=document.createElement('div'); t.className='toast'; t.textContent=msg;
|
|
638
607
|
document.getElementById('toastContainer').appendChild(t);
|
|
639
|
-
setTimeout(()
|
|
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>`;
|
|
608
|
+
setTimeout(()=>t.remove(),3000);
|
|
649
609
|
}
|
|
650
610
|
|
|
651
611
|
init();
|