Aqso commited on
Commit
822e778
Β·
verified Β·
1 Parent(s): 99d383c

Upload index.html

Browse files
Files changed (1) hide show
  1. public/index.html +1010 -0
public/index.html ADDED
@@ -0,0 +1,1010 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Anichin Scraper β€” Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;700&display=swap" rel="stylesheet" />
8
+ <style>
9
+ /* ── RESET & BASE ── */
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+
12
+ :root {
13
+ --bg: #0a0a0f;
14
+ --surface: #111118;
15
+ --surface2: #16161f;
16
+ --border: #1e1e2e;
17
+ --accent: #e64040;
18
+ --accent2: #ff7043;
19
+ --green: #4ade80;
20
+ --yellow: #fbbf24;
21
+ --text: #e2e2ef;
22
+ --muted: #6b6b8a;
23
+ --font-display: 'Bebas Neue', sans-serif;
24
+ --font-mono: 'DM Mono', monospace;
25
+ --font-body: 'DM Sans', sans-serif;
26
+ }
27
+
28
+ body {
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: var(--font-body);
32
+ min-height: 100vh;
33
+ overflow-x: hidden;
34
+ }
35
+
36
+ /* ── NOISE OVERLAY ── */
37
+ body::before {
38
+ content: '';
39
+ position: fixed;
40
+ inset: 0;
41
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
42
+ pointer-events: none;
43
+ z-index: 9999;
44
+ opacity: 0.4;
45
+ }
46
+
47
+ /* ── SIDEBAR ── */
48
+ .sidebar {
49
+ position: fixed;
50
+ left: 0; top: 0; bottom: 0;
51
+ width: 240px;
52
+ background: var(--surface);
53
+ border-right: 1px solid var(--border);
54
+ display: flex;
55
+ flex-direction: column;
56
+ padding: 0;
57
+ z-index: 100;
58
+ }
59
+
60
+ .sidebar-logo {
61
+ padding: 28px 24px 20px;
62
+ border-bottom: 1px solid var(--border);
63
+ }
64
+
65
+ .sidebar-logo h1 {
66
+ font-family: var(--font-display);
67
+ font-size: 28px;
68
+ letter-spacing: 2px;
69
+ color: var(--accent);
70
+ line-height: 1;
71
+ }
72
+
73
+ .sidebar-logo p {
74
+ font-size: 10px;
75
+ color: var(--muted);
76
+ font-family: var(--font-mono);
77
+ margin-top: 4px;
78
+ letter-spacing: 1px;
79
+ text-transform: uppercase;
80
+ }
81
+
82
+ .status-pill {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ gap: 6px;
86
+ margin-top: 12px;
87
+ padding: 4px 10px;
88
+ border-radius: 20px;
89
+ font-family: var(--font-mono);
90
+ font-size: 10px;
91
+ background: #1a1a2a;
92
+ border: 1px solid var(--border);
93
+ }
94
+
95
+ .status-dot {
96
+ width: 6px; height: 6px;
97
+ border-radius: 50%;
98
+ background: var(--green);
99
+ }
100
+
101
+ .status-dot.running {
102
+ background: var(--yellow);
103
+ animation: pulse 1s infinite;
104
+ }
105
+
106
+ @keyframes pulse {
107
+ 0%, 100% { opacity: 1; }
108
+ 50% { opacity: 0.3; }
109
+ }
110
+
111
+ .nav {
112
+ flex: 1;
113
+ padding: 16px 12px;
114
+ }
115
+
116
+ .nav-item {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 10px;
120
+ padding: 10px 12px;
121
+ border-radius: 8px;
122
+ cursor: pointer;
123
+ font-size: 13px;
124
+ font-weight: 500;
125
+ color: var(--muted);
126
+ transition: all 0.15s;
127
+ margin-bottom: 2px;
128
+ }
129
+
130
+ .nav-item:hover { background: var(--surface2); color: var(--text); }
131
+ .nav-item.active { background: rgba(230, 64, 64, 0.1); color: var(--accent); border-left: 2px solid var(--accent); }
132
+
133
+ .nav-icon { font-size: 16px; width: 20px; text-align: center; }
134
+
135
+ /* ── MAIN CONTENT ── */
136
+ .main {
137
+ margin-left: 240px;
138
+ min-height: 100vh;
139
+ padding: 32px;
140
+ }
141
+
142
+ /* ── HEADER ── */
143
+ .page-header {
144
+ display: flex;
145
+ align-items: flex-start;
146
+ justify-content: space-between;
147
+ margin-bottom: 32px;
148
+ }
149
+
150
+ .page-title {
151
+ font-family: var(--font-display);
152
+ font-size: 48px;
153
+ letter-spacing: 3px;
154
+ line-height: 1;
155
+ background: linear-gradient(135deg, var(--text) 0%, var(--muted) 100%);
156
+ -webkit-background-clip: text;
157
+ -webkit-text-fill-color: transparent;
158
+ }
159
+
160
+ .page-sub {
161
+ font-family: var(--font-mono);
162
+ font-size: 11px;
163
+ color: var(--muted);
164
+ margin-top: 6px;
165
+ letter-spacing: 1px;
166
+ }
167
+
168
+ /* ── STAT CARDS ── */
169
+ .stats-grid {
170
+ display: grid;
171
+ grid-template-columns: repeat(4, 1fr);
172
+ gap: 16px;
173
+ margin-bottom: 28px;
174
+ }
175
+
176
+ .stat-card {
177
+ background: var(--surface);
178
+ border: 1px solid var(--border);
179
+ border-radius: 12px;
180
+ padding: 20px;
181
+ position: relative;
182
+ overflow: hidden;
183
+ }
184
+
185
+ .stat-card::after {
186
+ content: '';
187
+ position: absolute;
188
+ top: 0; left: 0; right: 0;
189
+ height: 2px;
190
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
191
+ }
192
+
193
+ .stat-label {
194
+ font-family: var(--font-mono);
195
+ font-size: 10px;
196
+ color: var(--muted);
197
+ text-transform: uppercase;
198
+ letter-spacing: 1px;
199
+ }
200
+
201
+ .stat-value {
202
+ font-family: var(--font-display);
203
+ font-size: 38px;
204
+ letter-spacing: 2px;
205
+ margin-top: 8px;
206
+ color: var(--text);
207
+ }
208
+
209
+ /* ── PANELS ── */
210
+ .panel {
211
+ background: var(--surface);
212
+ border: 1px solid var(--border);
213
+ border-radius: 12px;
214
+ margin-bottom: 20px;
215
+ }
216
+
217
+ .panel-header {
218
+ padding: 16px 20px;
219
+ border-bottom: 1px solid var(--border);
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: space-between;
223
+ }
224
+
225
+ .panel-title {
226
+ font-family: var(--font-mono);
227
+ font-size: 12px;
228
+ color: var(--muted);
229
+ text-transform: uppercase;
230
+ letter-spacing: 2px;
231
+ }
232
+
233
+ .panel-body { padding: 20px; }
234
+
235
+ /* ── SCRAPE CONTROLS ── */
236
+ .controls-grid {
237
+ display: grid;
238
+ grid-template-columns: 1fr 1fr;
239
+ gap: 16px;
240
+ }
241
+
242
+ .control-card {
243
+ background: var(--surface2);
244
+ border: 1px solid var(--border);
245
+ border-radius: 10px;
246
+ padding: 20px;
247
+ }
248
+
249
+ .control-card h3 {
250
+ font-size: 14px;
251
+ font-weight: 600;
252
+ margin-bottom: 6px;
253
+ }
254
+
255
+ .control-card p {
256
+ font-size: 12px;
257
+ color: var(--muted);
258
+ margin-bottom: 16px;
259
+ line-height: 1.5;
260
+ }
261
+
262
+ .input-row {
263
+ display: flex;
264
+ gap: 10px;
265
+ margin-bottom: 12px;
266
+ }
267
+
268
+ input[type=text], input[type=number], input[type=url] {
269
+ flex: 1;
270
+ background: var(--bg);
271
+ border: 1px solid var(--border);
272
+ border-radius: 6px;
273
+ padding: 8px 12px;
274
+ color: var(--text);
275
+ font-family: var(--font-mono);
276
+ font-size: 12px;
277
+ outline: none;
278
+ transition: border 0.2s;
279
+ }
280
+
281
+ input:focus { border-color: var(--accent); }
282
+
283
+ /* ── BUTTONS ── */
284
+ .btn {
285
+ padding: 9px 18px;
286
+ border-radius: 6px;
287
+ font-family: var(--font-mono);
288
+ font-size: 12px;
289
+ font-weight: 500;
290
+ cursor: pointer;
291
+ border: none;
292
+ transition: all 0.15s;
293
+ white-space: nowrap;
294
+ }
295
+
296
+ .btn-primary {
297
+ background: var(--accent);
298
+ color: white;
299
+ }
300
+
301
+ .btn-primary:hover { background: #c0392b; }
302
+
303
+ .btn-secondary {
304
+ background: transparent;
305
+ border: 1px solid var(--border);
306
+ color: var(--muted);
307
+ }
308
+
309
+ .btn-secondary:hover { border-color: var(--text); color: var(--text); }
310
+
311
+ .btn-green {
312
+ background: rgba(74, 222, 128, 0.1);
313
+ border: 1px solid rgba(74, 222, 128, 0.3);
314
+ color: var(--green);
315
+ }
316
+
317
+ .btn-green:hover { background: rgba(74, 222, 128, 0.2); }
318
+
319
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
320
+
321
+ /* ── LOG TERMINAL ── */
322
+ .terminal {
323
+ background: #060609;
324
+ border: 1px solid var(--border);
325
+ border-radius: 8px;
326
+ padding: 16px;
327
+ height: 220px;
328
+ overflow-y: auto;
329
+ font-family: var(--font-mono);
330
+ font-size: 11px;
331
+ line-height: 1.8;
332
+ }
333
+
334
+ .terminal::-webkit-scrollbar { width: 4px; }
335
+ .terminal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
336
+
337
+ .log-entry { color: var(--muted); }
338
+ .log-entry.info { color: var(--text); }
339
+ .log-entry.success { color: var(--green); }
340
+ .log-entry.error { color: var(--accent); }
341
+ .log-entry.warn { color: var(--yellow); }
342
+
343
+ .log-time {
344
+ color: var(--border);
345
+ margin-right: 8px;
346
+ }
347
+
348
+ /* ── ANIME TABLE ── */
349
+ .anime-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
352
+ gap: 14px;
353
+ }
354
+
355
+ .anime-card {
356
+ background: var(--surface2);
357
+ border: 1px solid var(--border);
358
+ border-radius: 8px;
359
+ overflow: hidden;
360
+ transition: transform 0.15s, border-color 0.15s;
361
+ cursor: pointer;
362
+ }
363
+
364
+ .anime-card:hover {
365
+ transform: translateY(-2px);
366
+ border-color: var(--accent);
367
+ }
368
+
369
+ .anime-thumb {
370
+ width: 100%;
371
+ aspect-ratio: 2/3;
372
+ object-fit: cover;
373
+ background: var(--border);
374
+ display: block;
375
+ }
376
+
377
+ .anime-thumb-placeholder {
378
+ width: 100%;
379
+ aspect-ratio: 2/3;
380
+ background: linear-gradient(135deg, #1a1a2e, #16213e);
381
+ display: flex;
382
+ align-items: center;
383
+ justify-content: center;
384
+ font-size: 32px;
385
+ }
386
+
387
+ .anime-info {
388
+ padding: 10px;
389
+ }
390
+
391
+ .anime-title {
392
+ font-size: 11px;
393
+ font-weight: 600;
394
+ line-height: 1.3;
395
+ margin-bottom: 4px;
396
+ display: -webkit-box;
397
+ -webkit-line-clamp: 2;
398
+ -webkit-box-orient: vertical;
399
+ overflow: hidden;
400
+ }
401
+
402
+ .anime-meta {
403
+ display: flex;
404
+ gap: 4px;
405
+ }
406
+
407
+ .badge {
408
+ font-family: var(--font-mono);
409
+ font-size: 9px;
410
+ padding: 2px 5px;
411
+ border-radius: 3px;
412
+ background: var(--border);
413
+ color: var(--muted);
414
+ }
415
+
416
+ .badge.ongoing { background: rgba(74, 222, 128, 0.1); color: var(--green); }
417
+ .badge.completed { background: rgba(96, 165, 250, 0.1); color: #60a5fa; }
418
+
419
+ /* ── PROGRESS BAR ── */
420
+ .progress-wrap {
421
+ background: var(--border);
422
+ border-radius: 99px;
423
+ height: 4px;
424
+ margin-top: 12px;
425
+ overflow: hidden;
426
+ }
427
+
428
+ .progress-bar {
429
+ height: 100%;
430
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
431
+ border-radius: 99px;
432
+ transition: width 0.5s ease;
433
+ }
434
+
435
+ /* ── TABS ── */
436
+ .tabs {
437
+ display: flex;
438
+ gap: 2px;
439
+ margin-bottom: 20px;
440
+ background: var(--surface2);
441
+ border-radius: 8px;
442
+ padding: 4px;
443
+ width: fit-content;
444
+ }
445
+
446
+ .tab {
447
+ padding: 7px 16px;
448
+ border-radius: 6px;
449
+ font-family: var(--font-mono);
450
+ font-size: 11px;
451
+ cursor: pointer;
452
+ color: var(--muted);
453
+ transition: all 0.15s;
454
+ }
455
+
456
+ .tab.active {
457
+ background: var(--surface);
458
+ color: var(--text);
459
+ }
460
+
461
+ /* ── SEARCH ── */
462
+ .search-bar {
463
+ display: flex;
464
+ gap: 10px;
465
+ margin-bottom: 20px;
466
+ }
467
+
468
+ /* ── VIEWS ── */
469
+ .view { display: none; }
470
+ .view.active { display: block; }
471
+
472
+ /* ── TOAST ── */
473
+ #toast {
474
+ position: fixed;
475
+ bottom: 24px;
476
+ right: 24px;
477
+ padding: 12px 20px;
478
+ border-radius: 8px;
479
+ font-family: var(--font-mono);
480
+ font-size: 12px;
481
+ background: var(--surface);
482
+ border: 1px solid var(--border);
483
+ color: var(--text);
484
+ transform: translateY(80px);
485
+ opacity: 0;
486
+ transition: all 0.3s;
487
+ z-index: 9999;
488
+ }
489
+
490
+ #toast.show { transform: translateY(0); opacity: 1; }
491
+ #toast.success { border-color: var(--green); color: var(--green); }
492
+ #toast.error { border-color: var(--accent); color: var(--accent); }
493
+
494
+ /* ── EMPTY STATE ── */
495
+ .empty {
496
+ text-align: center;
497
+ padding: 60px 20px;
498
+ color: var(--muted);
499
+ }
500
+
501
+ .empty-icon { font-size: 48px; margin-bottom: 16px; }
502
+ .empty h3 { font-size: 16px; margin-bottom: 8px; color: var(--text); }
503
+ .empty p { font-size: 12px; line-height: 1.5; }
504
+
505
+ /* ── PAGINATION ── */
506
+ .pagination {
507
+ display: flex;
508
+ align-items: center;
509
+ justify-content: center;
510
+ gap: 8px;
511
+ margin-top: 24px;
512
+ }
513
+
514
+ /* ── RESPONSIVE ── */
515
+ @media (max-width: 900px) {
516
+ .sidebar { width: 60px; }
517
+ .sidebar-logo h1, .sidebar-logo p, .status-pill, .nav-item span { display: none; }
518
+ .main { margin-left: 60px; }
519
+ .stats-grid { grid-template-columns: 1fr 1fr; }
520
+ }
521
+ </style>
522
+ </head>
523
+ <body>
524
+
525
+ <!-- ── SIDEBAR ── -->
526
+ <aside class="sidebar">
527
+ <div class="sidebar-logo">
528
+ <h1>ANICHIN</h1>
529
+ <p>Scraper Dashboard</p>
530
+ <div class="status-pill">
531
+ <span class="status-dot" id="statusDot"></span>
532
+ <span id="statusText">Ready</span>
533
+ </div>
534
+ </div>
535
+
536
+ <nav class="nav">
537
+ <div class="nav-item active" onclick="switchView('scraper')">
538
+ <span class="nav-icon">⚑</span>
539
+ <span>Scraper</span>
540
+ </div>
541
+ <div class="nav-item" onclick="switchView('library')">
542
+ <span class="nav-icon">πŸ—ƒοΈ</span>
543
+ <span>Library</span>
544
+ </div>
545
+ <div class="nav-item" onclick="switchView('logs')">
546
+ <span class="nav-icon">πŸ“‹</span>
547
+ <span>Logs</span>
548
+ </div>
549
+ </nav>
550
+ </aside>
551
+
552
+ <!-- ── MAIN ── -->
553
+ <main class="main">
554
+
555
+ <!-- ── VIEW: SCRAPER ── -->
556
+ <div class="view active" id="view-scraper">
557
+ <div class="page-header">
558
+ <div>
559
+ <div class="page-title">SCRAPER</div>
560
+ <div class="page-sub">// anichin.cafe β†’ firebase firestore</div>
561
+ </div>
562
+ <button class="btn btn-secondary" onclick="refreshStats()">⟳ Refresh Stats</button>
563
+ </div>
564
+
565
+ <!-- Stats -->
566
+ <div class="stats-grid">
567
+ <div class="stat-card">
568
+ <div class="stat-label">Total Anime</div>
569
+ <div class="stat-value" id="statTotal">β€”</div>
570
+ </div>
571
+ <div class="stat-card">
572
+ <div class="stat-label">Ongoing</div>
573
+ <div class="stat-value" id="statOngoing">β€”</div>
574
+ </div>
575
+ <div class="stat-card">
576
+ <div class="stat-label">Last Scrape</div>
577
+ <div class="stat-value" style="font-size:16px; margin-top:16px;" id="statLastRun">Never</div>
578
+ </div>
579
+ <div class="stat-card">
580
+ <div class="stat-label">Status</div>
581
+ <div class="stat-value" style="font-size:22px; margin-top:12px;" id="statStatus">IDLE</div>
582
+ </div>
583
+ </div>
584
+
585
+ <!-- Controls -->
586
+ <div class="panel">
587
+ <div class="panel-header">
588
+ <span class="panel-title">// Scrape Controls</span>
589
+ <span style="font-family:var(--font-mono);font-size:10px;color:var(--muted)" id="progressLabel"></span>
590
+ </div>
591
+ <div class="panel-body">
592
+ <div class="progress-wrap" id="progressWrap" style="display:none">
593
+ <div class="progress-bar" id="progressBar" style="width:0%"></div>
594
+ </div>
595
+
596
+ <div class="controls-grid" style="margin-top:16px">
597
+ <!-- Full Scrape -->
598
+ <div class="control-card">
599
+ <h3>πŸš€ Full Scrape</h3>
600
+ <p>Scrape semua anime dari halaman list + detail lengkap. Cocok buat pertama kali.</p>
601
+ <div class="input-row">
602
+ <input type="number" id="pagesInput" value="10" min="1" max="100" placeholder="Jumlah halaman" />
603
+ </div>
604
+ <button class="btn btn-primary" id="btnFullScrape" onclick="startFullScrape()">
605
+ β–Ά Mulai Full Scrape
606
+ </button>
607
+ </div>
608
+
609
+ <!-- Incremental Update -->
610
+ <div class="control-card">
611
+ <h3>πŸ”„ Incremental Update</h3>
612
+ <p>Cek episode baru untuk anime yang masih ongoing. Ringan dan cepet.</p>
613
+ <p style="margin-bottom:8px;color:var(--yellow)">⏰ Auto-run setiap hari 06:00 WIB</p>
614
+ <button class="btn btn-green" id="btnUpdate" onclick="startUpdate()">
615
+ ↻ Update Sekarang
616
+ </button>
617
+ </div>
618
+
619
+ <!-- Single Anime -->
620
+ <div class="control-card">
621
+ <h3>🎯 Scrape Anime Tertentu</h3>
622
+ <p>Input URL anime dari anichin.cafe untuk scrape satu anime aja.</p>
623
+ <div class="input-row">
624
+ <input type="url" id="singleUrl" placeholder="https://anichin.cafe/anime/..." />
625
+ </div>
626
+ <button class="btn btn-secondary" onclick="scrapeSingle()">Scrape</button>
627
+ </div>
628
+
629
+ <!-- Search Scrape -->
630
+ <div class="control-card">
631
+ <h3>πŸ” Search & Scrape</h3>
632
+ <p>Cari anime berdasarkan judul dan scrape hasilnya ke database.</p>
633
+ <div class="input-row">
634
+ <input type="text" id="searchQuery" placeholder="Nama anime..." />
635
+ </div>
636
+ <button class="btn btn-secondary" onclick="searchScrape()">Cari & Scrape</button>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ </div>
641
+
642
+ <!-- Live Log -->
643
+ <div class="panel">
644
+ <div class="panel-header">
645
+ <span class="panel-title">// Live Log</span>
646
+ <button class="btn btn-secondary" onclick="clearLog()" style="padding:4px 10px;font-size:10px">Clear</button>
647
+ </div>
648
+ <div class="panel-body" style="padding:12px">
649
+ <div class="terminal" id="terminal">
650
+ <div class="log-entry info">
651
+ <span class="log-time">--:--:--</span>
652
+ Anichin Scraper siap. Tekan tombol scrape untuk mulai.
653
+ </div>
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </div>
658
+
659
+ <!-- ── VIEW: LIBRARY ── -->
660
+ <div class="view" id="view-library">
661
+ <div class="page-header">
662
+ <div>
663
+ <div class="page-title">LIBRARY</div>
664
+ <div class="page-sub">// anime database dari firebase</div>
665
+ </div>
666
+ </div>
667
+
668
+ <div class="search-bar">
669
+ <input type="text" id="libSearch" placeholder="πŸ” Cari anime..." style="max-width:300px" oninput="debounceSearch()" />
670
+ <select id="libStatus" onchange="loadLibrary()" style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:8px 12px;color:var(--text);font-family:var(--font-mono);font-size:12px;outline:none;">
671
+ <option value="">Semua Status</option>
672
+ <option value="Ongoing">Ongoing</option>
673
+ <option value="Completed">Completed</option>
674
+ </select>
675
+ </div>
676
+
677
+ <div class="anime-grid" id="animeGrid">
678
+ <div class="empty" style="grid-column:1/-1">
679
+ <div class="empty-icon">πŸ“¦</div>
680
+ <h3>Library Kosong</h3>
681
+ <p>Belum ada anime. Jalankan scraper dulu!</p>
682
+ </div>
683
+ </div>
684
+
685
+ <div class="pagination" id="pagination"></div>
686
+ </div>
687
+
688
+ <!-- ── VIEW: LOGS ── -->
689
+ <div class="view" id="view-logs">
690
+ <div class="page-header">
691
+ <div>
692
+ <div class="page-title">LOGS</div>
693
+ <div class="page-sub">// riwayat scraping activity</div>
694
+ </div>
695
+ <button class="btn btn-secondary" onclick="loadLogs()">⟳ Refresh</button>
696
+ </div>
697
+
698
+ <div class="panel">
699
+ <div class="panel-body" style="padding:12px">
700
+ <div class="terminal" id="fullLog" style="height:500px"></div>
701
+ </div>
702
+ </div>
703
+ </div>
704
+
705
+ </main>
706
+
707
+ <!-- Toast -->
708
+ <div id="toast"></div>
709
+
710
+ <script>
711
+ // ── STATE ──────────────────────────────────────────────────────────────────
712
+ let currentPage = 1;
713
+ let pollInterval = null;
714
+ let searchTimeout = null;
715
+
716
+ const API = window.location.origin + '/api';
717
+
718
+ // ── INIT ──────────────────────────────────────────────────────────────────
719
+ document.addEventListener('DOMContentLoaded', () => {
720
+ refreshStats();
721
+ startPolling();
722
+ });
723
+
724
+ // ── VIEWS ─────────────────────────────────────────────────────────────────
725
+ function switchView(name) {
726
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
727
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
728
+
729
+ document.getElementById(`view-${name}`).classList.add('active');
730
+ event.currentTarget.classList.add('active');
731
+
732
+ if (name === 'library') { currentPage = 1; loadLibrary(); }
733
+ if (name === 'logs') loadLogs();
734
+ }
735
+
736
+ // ── STATS ─────────────────────────────────────────────────────────────────
737
+ async function refreshStats() {
738
+ try {
739
+ const res = await fetch(`${API}/stats`);
740
+ const d = await res.json();
741
+ document.getElementById('statTotal').textContent = (d.totalAnimes || 0).toLocaleString();
742
+ document.getElementById('statOngoing').textContent = (d.ongoingAnimes || 0).toLocaleString();
743
+ if (d.lastScrape) {
744
+ document.getElementById('statLastRun').textContent =
745
+ new Date(d.lastScrape).toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
746
+ }
747
+ } catch { /* server might not be running in demo */ }
748
+ }
749
+
750
+ // ── SCRAPE ACTIONS ────────────────────────────────────────────────────────
751
+ async function startFullScrape() {
752
+ const pages = parseInt(document.getElementById('pagesInput').value) || 10;
753
+ setScrapingUI(true);
754
+ addLogEntry(`πŸš€ Full scrape dimulai β€” ${pages} halaman`, 'info');
755
+
756
+ try {
757
+ const res = await fetch(`${API}/scrape/full`, {
758
+ method: 'POST',
759
+ headers: { 'Content-Type': 'application/json' },
760
+ body: JSON.stringify({ pages }),
761
+ });
762
+ const d = await res.json();
763
+ toast(d.message || 'Scrape dimulai!', 'success');
764
+ } catch (err) {
765
+ addLogEntry(`❌ Koneksi error: ${err.message}`, 'error');
766
+ setScrapingUI(false);
767
+ }
768
+ }
769
+
770
+ async function startUpdate() {
771
+ setScrapingUI(true);
772
+ addLogEntry('πŸ”„ Incremental update dimulai...', 'info');
773
+
774
+ try {
775
+ const res = await fetch(`${API}/scrape/update`, { method: 'POST' });
776
+ const d = await res.json();
777
+ toast(d.message, 'success');
778
+ } catch (err) {
779
+ addLogEntry(`❌ ${err.message}`, 'error');
780
+ setScrapingUI(false);
781
+ }
782
+ }
783
+
784
+ async function scrapeSingle() {
785
+ const url = document.getElementById('singleUrl').value.trim();
786
+ if (!url) return toast('Input URL dulu!', 'error');
787
+
788
+ addLogEntry(`🎯 Scrape: ${url}`, 'info');
789
+
790
+ try {
791
+ const res = await fetch(`${API}/scrape/single`, {
792
+ method: 'POST',
793
+ headers: { 'Content-Type': 'application/json' },
794
+ body: JSON.stringify({ url }),
795
+ });
796
+ const d = await res.json();
797
+ if (d.success) {
798
+ addLogEntry(`βœ… ${d.data.title} β€” ${d.data.totalEpisodes} eps`, 'success');
799
+ toast('Anime berhasil discrape!', 'success');
800
+ } else {
801
+ addLogEntry(`❌ ${d.error}`, 'error');
802
+ }
803
+ } catch (err) {
804
+ addLogEntry(`❌ ${err.message}`, 'error');
805
+ }
806
+ }
807
+
808
+ async function searchScrape() {
809
+ const q = document.getElementById('searchQuery').value.trim();
810
+ if (!q) return toast('Masukkin query dulu!', 'error');
811
+
812
+ addLogEntry(`πŸ” Search: "${q}"`, 'info');
813
+
814
+ try {
815
+ const res = await fetch(`${API}/scrape/search?q=${encodeURIComponent(q)}`);
816
+ const d = await res.json();
817
+ addLogEntry(`βœ“ Dapet ${d.count} hasil untuk "${q}"`, 'success');
818
+ toast(`${d.count} anime ditemukan`, 'success');
819
+ } catch (err) {
820
+ addLogEntry(`❌ ${err.message}`, 'error');
821
+ }
822
+ }
823
+
824
+ // ── LIBRARY ───────────────────────────────────────────────────────────────
825
+ async function loadLibrary() {
826
+ const status = document.getElementById('libStatus').value;
827
+ const search = document.getElementById('libSearch').value;
828
+ const grid = document.getElementById('animeGrid');
829
+
830
+ grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--muted)">Loading...</div>';
831
+
832
+ try {
833
+ let url = `${API}/animes?limit=30&page=${currentPage}`;
834
+ if (status) url += `&status=${status}`;
835
+
836
+ const res = await fetch(url);
837
+ const d = await res.json();
838
+
839
+ renderAnimes(d.data || [], d.total || 0);
840
+ renderPagination(d.total || 0, 30);
841
+ } catch {
842
+ grid.innerHTML = `
843
+ <div class="empty" style="grid-column:1/-1">
844
+ <div class="empty-icon">⚠️</div>
845
+ <h3>Server Tidak Tersedia</h3>
846
+ <p>Pastikan server Node.js sudah berjalan.<br>Jalankan: <code style="color:var(--accent)">npm start</code></p>
847
+ </div>`;
848
+ }
849
+ }
850
+
851
+ function renderAnimes(animes, total) {
852
+ const grid = document.getElementById('animeGrid');
853
+
854
+ if (!animes.length) {
855
+ grid.innerHTML = `
856
+ <div class="empty" style="grid-column:1/-1">
857
+ <div class="empty-icon">πŸ“­</div>
858
+ <h3>Belum Ada Data</h3>
859
+ <p>Jalankan scraper untuk mulai ngumpulin anime!</p>
860
+ </div>`;
861
+ return;
862
+ }
863
+
864
+ grid.innerHTML = animes.map(a => `
865
+ <div class="anime-card" onclick="showDetail('${a.slug || a.id}')">
866
+ ${a.thumbnail
867
+ ? `<img class="anime-thumb" src="${escHtml(a.thumbnail)}" alt="${escHtml(a.title)}" loading="lazy" onerror="this.style.display='none'" />`
868
+ : `<div class="anime-thumb-placeholder">🎌</div>`}
869
+ <div class="anime-info">
870
+ <div class="anime-title">${escHtml(a.title || 'β€”')}</div>
871
+ <div class="anime-meta">
872
+ ${a.status ? `<span class="badge ${(a.status||'').toLowerCase()}">${escHtml(a.status)}</span>` : ''}
873
+ ${a.totalEpisodes ? `<span class="badge">${a.totalEpisodes} eps</span>` : ''}
874
+ </div>
875
+ </div>
876
+ </div>
877
+ `).join('');
878
+ }
879
+
880
+ function renderPagination(total, limit) {
881
+ const pages = Math.ceil(total / limit);
882
+ const pg = document.getElementById('pagination');
883
+
884
+ if (pages <= 1) { pg.innerHTML = ''; return; }
885
+
886
+ let html = '';
887
+ if (currentPage > 1) html += `<button class="btn btn-secondary" onclick="goPage(${currentPage-1})">← Prev</button>`;
888
+ html += `<span style="font-family:var(--font-mono);font-size:11px;color:var(--muted)">${currentPage} / ${pages}</span>`;
889
+ if (currentPage < pages) html += `<button class="btn btn-secondary" onclick="goPage(${currentPage+1})">Next β†’</button>`;
890
+
891
+ pg.innerHTML = html;
892
+ }
893
+
894
+ function goPage(p) { currentPage = p; loadLibrary(); window.scrollTo(0,0); }
895
+
896
+ function debounceSearch() {
897
+ clearTimeout(searchTimeout);
898
+ searchTimeout = setTimeout(() => { currentPage = 1; loadLibrary(); }, 400);
899
+ }
900
+
901
+ async function showDetail(slug) {
902
+ toast(`Memuat detail ${slug}...`, 'success');
903
+ }
904
+
905
+ // ── LOGS ──────────────────────────────────────────────────────────────────
906
+ async function loadLogs() {
907
+ try {
908
+ const res = await fetch(`${API}/logs`);
909
+ const logs = await res.json();
910
+ const el = document.getElementById('fullLog');
911
+ el.innerHTML = logs.map(l => `
912
+ <div class="log-entry ${classifyLog(l.msg)}">
913
+ <span class="log-time">${new Date(l.time).toLocaleTimeString('id-ID')}</span>
914
+ ${escHtml(l.msg)}
915
+ </div>`).join('') || '<div class="log-entry">Belum ada log.</div>';
916
+ } catch {
917
+ document.getElementById('fullLog').innerHTML = '<div class="log-entry error">Server tidak tersedia.</div>';
918
+ }
919
+ }
920
+
921
+ // ── POLLING ───────────────────────────────────────────────────────────────
922
+ function startPolling() {
923
+ pollInterval = setInterval(async () => {
924
+ try {
925
+ const res = await fetch(`${API}/status`);
926
+ const d = await res.json();
927
+
928
+ const dot = document.getElementById('statusDot');
929
+ const statusTxt = document.getElementById('statusText');
930
+ const statStatus = document.getElementById('statStatus');
931
+
932
+ if (d.scrapeRunning) {
933
+ dot.className = 'status-dot running';
934
+ statusTxt.textContent = 'Running';
935
+ statStatus.textContent = d.progress?.stage?.toUpperCase() || 'RUNNING';
936
+ document.getElementById('progressWrap').style.display = 'block';
937
+
938
+ // Fetch fresh logs
939
+ const logRes = await fetch(`${API}/logs`);
940
+ const logs = await logRes.json();
941
+ if (logs.length) updateTerminalFromServer(logs.slice(0, 20));
942
+ } else {
943
+ dot.className = 'status-dot';
944
+ statusTxt.textContent = 'Ready';
945
+ statStatus.textContent = 'IDLE';
946
+ setScrapingUI(false);
947
+ document.getElementById('progressWrap').style.display = 'none';
948
+ }
949
+ } catch { /* server offline */ }
950
+ }, 2500);
951
+ }
952
+
953
+ function updateTerminalFromServer(logs) {
954
+ const term = document.getElementById('terminal');
955
+ term.innerHTML = logs.map(l => `
956
+ <div class="log-entry ${classifyLog(l.msg)}">
957
+ <span class="log-time">${new Date(l.time).toLocaleTimeString('id-ID')}</span>
958
+ ${escHtml(l.msg)}
959
+ </div>`).join('');
960
+ }
961
+
962
+ // ── UI HELPERS ────────────────────────────────────────────────────────────
963
+ function setScrapingUI(isRunning) {
964
+ const btns = ['btnFullScrape', 'btnUpdate'];
965
+ btns.forEach(id => {
966
+ const el = document.getElementById(id);
967
+ if (el) el.disabled = isRunning;
968
+ });
969
+ }
970
+
971
+ function addLogEntry(msg, type = 'info') {
972
+ const term = document.getElementById('terminal');
973
+ const time = new Date().toLocaleTimeString('id-ID');
974
+ const div = document.createElement('div');
975
+ div.className = `log-entry ${type}`;
976
+ div.innerHTML = `<span class="log-time">${time}</span>${escHtml(msg)}`;
977
+ term.insertBefore(div, term.firstChild);
978
+ if (term.children.length > 50) term.removeChild(term.lastChild);
979
+ }
980
+
981
+ function classifyLog(msg) {
982
+ if (!msg) return '';
983
+ if (msg.includes('βœ…') || msg.includes('βœ“')) return 'success';
984
+ if (msg.includes('❌')) return 'error';
985
+ if (msg.includes('⚠️')) return 'warn';
986
+ return 'info';
987
+ }
988
+
989
+ function clearLog() {
990
+ document.getElementById('terminal').innerHTML = '';
991
+ }
992
+
993
+ function toast(msg, type = '') {
994
+ const t = document.getElementById('toast');
995
+ t.textContent = msg;
996
+ t.className = `show ${type}`;
997
+ setTimeout(() => t.className = '', 3000);
998
+ }
999
+
1000
+ function escHtml(s) {
1001
+ if (!s) return '';
1002
+ return String(s)
1003
+ .replace(/&/g, '&amp;')
1004
+ .replace(/</g, '&lt;')
1005
+ .replace(/>/g, '&gt;')
1006
+ .replace(/"/g, '&quot;');
1007
+ }
1008
+ </script>
1009
+ </body>
1010
+ </html>