luak-express 2.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.
Files changed (48) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +63 -0
  3. package/app/Exceptions/AppException.js +45 -0
  4. package/app/Exceptions/Handler.js +23 -0
  5. package/app/Http/Middleware/LoggerMiddleware.js +16 -0
  6. package/app/Models/Post.js +14 -0
  7. package/app/Models/User.js +14 -0
  8. package/app/Providers/AppServiceProvider.js +24 -0
  9. package/app/Providers/CacheServiceProvider.js +12 -0
  10. package/app/Providers/DatabaseServiceProvider.js +59 -0
  11. package/app/Providers/RouteServiceProvider.js +55 -0
  12. package/bin/luak +85 -0
  13. package/bootstrap/app.js +43 -0
  14. package/config/app.js +32 -0
  15. package/config/cache.js +12 -0
  16. package/config/database.js +42 -0
  17. package/index.js +82 -0
  18. package/package.json +54 -0
  19. package/routes/api.js +9 -0
  20. package/routes/web.js +13 -0
  21. package/src/Cache/CacheManager.js +54 -0
  22. package/src/Cache/Drivers/FileStore.js +67 -0
  23. package/src/Cache/Repository.js +34 -0
  24. package/src/Database/Model.js +86 -0
  25. package/src/Foundation/Application.js +217 -0
  26. package/src/Foundation/Config.js +111 -0
  27. package/src/Foundation/Exceptions/Handler.js +76 -0
  28. package/src/Foundation/Kernel.js +56 -0
  29. package/src/Foundation/helpers/dd.js +357 -0
  30. package/src/Foundation/resources/errors/404.ejs +132 -0
  31. package/src/Foundation/resources/errors/500.ejs +750 -0
  32. package/src/Http/Middleware/MethodOverride.js +26 -0
  33. package/src/Http/Request.js +98 -0
  34. package/src/Log/Logger.js +58 -0
  35. package/src/Routing/Controller.js +50 -0
  36. package/src/Routing/Pipeline.js +67 -0
  37. package/src/Routing/Router.js +163 -0
  38. package/src/Support/Facade.js +69 -0
  39. package/src/Support/Facades/Cache.js +9 -0
  40. package/src/Support/Facades/Config.js +14 -0
  41. package/src/Support/Facades/Log.js +16 -0
  42. package/src/Support/Facades/Request.js +14 -0
  43. package/src/Support/Facades/Route.js +14 -0
  44. package/src/Support/ServiceProvider.js +30 -0
  45. package/src/Support/helpers.js +79 -0
  46. package/src/index.js +21 -0
  47. package/storage/framework/cache/data/00942f4668670f34c5943cf52c7ef3139fe2b8d6 +1 -0
  48. package/storage/logs/luak.log +3 -0
@@ -0,0 +1,750 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>
8
+ <%= locals.errorName || 'Server Error' %> — Luak Express
9
+ </title>
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"
14
+ rel="stylesheet">
15
+
16
+ <style>
17
+ *,
18
+ *::before,
19
+ *::after {
20
+ box-sizing: border-box;
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+
25
+ :root {
26
+ --bg: #06080e;
27
+ --surface: #0c1018;
28
+ --elevated: #131922;
29
+ --raised: #1a2130;
30
+ --border: rgba(255, 255, 255, 0.055);
31
+ --border-hi: rgba(255, 255, 255, 0.12);
32
+ --text-1: #dde6f0;
33
+ --text-2: #8899aa;
34
+ --text-3: #3d4f62;
35
+
36
+ --error: #ff4545;
37
+ --error-bg: rgba(255, 69, 69, 0.07);
38
+ --error-border: rgba(255, 69, 69, 0.2);
39
+
40
+ --app: #00d97e;
41
+ --app-bg: rgba(0, 217, 126, 0.06);
42
+ --app-border: rgba(0, 217, 126, 0.2);
43
+
44
+ --fn: #b79eff;
45
+ --file: #5eaeff;
46
+ --line-num: #ffc145;
47
+
48
+ --r-sm: 6px;
49
+ --r: 12px;
50
+ --r-lg: 18px;
51
+ }
52
+
53
+ html {
54
+ scroll-behavior: smooth;
55
+ }
56
+
57
+ body {
58
+ font-family: 'IBM Plex Sans', sans-serif;
59
+ background: var(--bg);
60
+ color: var(--text-1);
61
+ min-height: 100vh;
62
+ }
63
+
64
+ /* =========================================================
65
+ TOP BAR
66
+ ========================================================= */
67
+ .topbar {
68
+ position: sticky;
69
+ top: 0;
70
+ z-index: 100;
71
+ background: rgba(6, 8, 14, 0.82);
72
+ backdrop-filter: blur(16px);
73
+ -webkit-backdrop-filter: blur(16px);
74
+ border-bottom: 1px solid var(--border);
75
+ height: 54px;
76
+ padding: 0 2rem;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ }
81
+
82
+ .logo {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 10px;
86
+ font-family: 'IBM Plex Sans', sans-serif;
87
+ font-weight: 700;
88
+ font-size: 0.95rem;
89
+ color: var(--text-1);
90
+ letter-spacing: -0.01em;
91
+ text-decoration: none;
92
+ }
93
+
94
+ .logo-mark {
95
+ width: 28px;
96
+ height: 28px;
97
+ border-radius: 8px;
98
+ background: linear-gradient(140deg, #ff4545 0%, #ff8c00 100%);
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ font-size: 13px;
103
+ font-family: 'IBM Plex Sans', sans-serif;
104
+ font-weight: 800;
105
+ color: white;
106
+ letter-spacing: -0.05em;
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ .topbar-right {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 12px;
114
+ }
115
+
116
+ .status-badge {
117
+ font-family: 'IBM Plex Mono', monospace;
118
+ font-size: 0.72rem;
119
+ color: var(--error);
120
+ background: var(--error-bg);
121
+ border: 1px solid var(--error-border);
122
+ padding: 3px 11px;
123
+ border-radius: 99px;
124
+ letter-spacing: 0.04em;
125
+ }
126
+
127
+ /* =========================================================
128
+ LAYOUT
129
+ ========================================================= */
130
+ .main {
131
+ max-width: 1080px;
132
+ margin: 0 auto;
133
+ padding: 3rem 2rem 5rem;
134
+ }
135
+
136
+ /* =========================================================
137
+ HERO
138
+ ========================================================= */
139
+ .hero {
140
+ padding-bottom: 2.5rem;
141
+ border-bottom: 1px solid var(--border);
142
+ margin-bottom: 2.5rem;
143
+ animation: fadeUp 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
144
+ }
145
+
146
+ @keyframes fadeUp {
147
+ from {
148
+ opacity: 0;
149
+ transform: translateY(18px);
150
+ }
151
+
152
+ to {
153
+ opacity: 1;
154
+ transform: translateY(0);
155
+ }
156
+ }
157
+
158
+ .error-eyebrow {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ font-family: 'IBM Plex Mono', monospace;
163
+ font-size: 0.78rem;
164
+ color: var(--error);
165
+ background: var(--error-bg);
166
+ border: 1px solid var(--error-border);
167
+ padding: 5px 14px;
168
+ border-radius: 99px;
169
+ margin-bottom: 1.5rem;
170
+ letter-spacing: 0.04em;
171
+ }
172
+
173
+ .error-eyebrow::before {
174
+ content: '';
175
+ width: 6px;
176
+ height: 6px;
177
+ background: var(--error);
178
+ border-radius: 50%;
179
+ animation: blink 1.6s ease-in-out infinite;
180
+ }
181
+
182
+ @keyframes blink {
183
+
184
+ 0%,
185
+ 100% {
186
+ opacity: 1;
187
+ }
188
+
189
+ 50% {
190
+ opacity: 0.3;
191
+ }
192
+ }
193
+
194
+ .error-title {
195
+ font-family: 'IBM Plex Sans', sans-serif;
196
+ font-size: clamp(2rem, 4.5vw, 3.2rem);
197
+ font-weight: 800;
198
+ letter-spacing: -0.035em;
199
+ line-height: 1.1;
200
+ color: var(--text-1);
201
+ margin-bottom: 1.25rem;
202
+ }
203
+
204
+ .error-message-block {
205
+ display: flex;
206
+ align-items: stretch;
207
+ gap: 0;
208
+ max-width: 780px;
209
+ }
210
+
211
+ .error-message-bar {
212
+ width: 3px;
213
+ border-radius: 99px;
214
+ background: var(--error);
215
+ flex-shrink: 0;
216
+ }
217
+
218
+ .error-message-text {
219
+ padding: 0.9rem 1.25rem;
220
+ font-family: 'IBM Plex Mono', monospace;
221
+ font-size: 0.875rem;
222
+ color: var(--text-2);
223
+ background: var(--elevated);
224
+ border-radius: 0 var(--r-sm) var(--r-sm) 0;
225
+ line-height: 1.6;
226
+ word-break: break-word;
227
+ flex: 1;
228
+ }
229
+
230
+ /* =========================================================
231
+ SECTION
232
+ ========================================================= */
233
+ .section {
234
+ animation: fadeUp 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
235
+ animation-delay: 0.1s;
236
+ }
237
+
238
+ .section-header {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 14px;
242
+ margin-bottom: 1.25rem;
243
+ }
244
+
245
+ .section-label {
246
+ font-family: 'IBM Plex Sans', sans-serif;
247
+ font-size: 0.68rem;
248
+ font-weight: 700;
249
+ text-transform: uppercase;
250
+ letter-spacing: 0.13em;
251
+ color: var(--text-3);
252
+ white-space: nowrap;
253
+ }
254
+
255
+ .section-rule {
256
+ flex: 1;
257
+ height: 1px;
258
+ background: var(--border);
259
+ }
260
+
261
+ .section-meta {
262
+ font-family: 'IBM Plex Mono', monospace;
263
+ font-size: 0.68rem;
264
+ color: var(--text-3);
265
+ background: var(--elevated);
266
+ padding: 3px 10px;
267
+ border-radius: 99px;
268
+ white-space: nowrap;
269
+ }
270
+
271
+ /* =========================================================
272
+ LEGEND
273
+ ========================================================= */
274
+ .legend {
275
+ display: flex;
276
+ flex-wrap: wrap;
277
+ gap: 18px;
278
+ margin-bottom: 1rem;
279
+ }
280
+
281
+ .legend-item {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 7px;
285
+ font-size: 0.75rem;
286
+ color: var(--text-3);
287
+ }
288
+
289
+ .legend-pip {
290
+ width: 8px;
291
+ height: 8px;
292
+ border-radius: 2px;
293
+ }
294
+
295
+ /* =========================================================
296
+ STACK FRAMES
297
+ ========================================================= */
298
+ .frames {
299
+ display: flex;
300
+ flex-direction: column;
301
+ gap: 3px;
302
+ }
303
+
304
+ .frame {
305
+ display: grid;
306
+ grid-template-columns: 48px 1fr auto;
307
+ align-items: stretch;
308
+ border-radius: var(--r-sm);
309
+ border: 1px solid var(--border);
310
+ overflow: hidden;
311
+ transition: border-color 0.15s ease, background 0.15s ease;
312
+ }
313
+
314
+ .frame:hover {
315
+ border-color: var(--border-hi);
316
+ background: var(--elevated);
317
+ }
318
+
319
+ .frame.app-frame {
320
+ border-color: var(--app-border);
321
+ background: var(--app-bg);
322
+ }
323
+
324
+ .frame.app-frame:hover {
325
+ border-color: rgba(0, 217, 126, 0.38);
326
+ }
327
+
328
+ .frame-num {
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ font-family: 'IBM Plex Mono', monospace;
333
+ font-size: 0.65rem;
334
+ color: var(--text-3);
335
+ border-right: 1px solid var(--border);
336
+ flex-shrink: 0;
337
+ min-height: 54px;
338
+ }
339
+
340
+ .frame.app-frame .frame-num {
341
+ color: var(--app);
342
+ border-right-color: var(--app-border);
343
+ font-weight: 600;
344
+ }
345
+
346
+ .frame-body {
347
+ padding: 11px 16px;
348
+ min-width: 0;
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 5px;
352
+ }
353
+
354
+ .frame-fn {
355
+ font-family: 'IBM Plex Mono', monospace;
356
+ font-size: 0.82rem;
357
+ font-weight: 500;
358
+ color: var(--fn);
359
+ white-space: nowrap;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 8px;
365
+ }
366
+
367
+ .frame.app-frame .frame-fn {
368
+ color: var(--app);
369
+ }
370
+
371
+ .frame-pill {
372
+ font-size: 0.6rem;
373
+ font-family: 'IBM Plex Mono', monospace;
374
+ background: var(--app-bg);
375
+ color: var(--app);
376
+ border: 1px solid var(--app-border);
377
+ padding: 1px 7px;
378
+ border-radius: 4px;
379
+ letter-spacing: 0.05em;
380
+ text-transform: uppercase;
381
+ flex-shrink: 0;
382
+ }
383
+
384
+ .frame-path {
385
+ font-family: 'IBM Plex Mono', monospace;
386
+ font-size: 0.73rem;
387
+ color: var(--text-3);
388
+ white-space: nowrap;
389
+ overflow: hidden;
390
+ text-overflow: ellipsis;
391
+ }
392
+
393
+ .frame.app-frame .frame-path {
394
+ color: var(--file);
395
+ }
396
+
397
+ .frame-path .fname {
398
+ font-weight: 600;
399
+ }
400
+
401
+ .frame.app-frame .frame-path .fname {
402
+ color: var(--text-1);
403
+ }
404
+
405
+ .frame-loc {
406
+ padding: 0 16px;
407
+ display: flex;
408
+ flex-direction: column;
409
+ align-items: flex-end;
410
+ justify-content: center;
411
+ gap: 3px;
412
+ flex-shrink: 0;
413
+ }
414
+
415
+ .frame-line {
416
+ font-family: 'IBM Plex Mono', monospace;
417
+ font-size: 0.78rem;
418
+ color: var(--line-num);
419
+ font-weight: 500;
420
+ }
421
+
422
+ .frame-col {
423
+ font-family: 'IBM Plex Mono', monospace;
424
+ font-size: 0.65rem;
425
+ color: var(--text-3);
426
+ }
427
+
428
+ /* raw fallback */
429
+ .raw-stack {
430
+ background: var(--elevated);
431
+ border: 1px solid var(--border);
432
+ border-radius: var(--r);
433
+ padding: 1.25rem 1.5rem;
434
+ }
435
+
436
+ .raw-stack pre {
437
+ font-family: 'IBM Plex Mono', monospace;
438
+ font-size: 0.78rem;
439
+ color: var(--text-2);
440
+ white-space: pre-wrap;
441
+ word-break: break-all;
442
+ line-height: 1.8;
443
+ }
444
+
445
+ /* =========================================================
446
+ ACTIONS
447
+ ========================================================= */
448
+ .actions {
449
+ margin-top: 3rem;
450
+ display: flex;
451
+ gap: 10px;
452
+ flex-wrap: wrap;
453
+ animation: fadeUp 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
454
+ animation-delay: 0.2s;
455
+ }
456
+
457
+ .btn {
458
+ display: inline-flex;
459
+ align-items: center;
460
+ gap: 7px;
461
+ padding: 0.65rem 1.35rem;
462
+ border-radius: var(--r-sm);
463
+ font-size: 0.875rem;
464
+ font-weight: 500;
465
+ text-decoration: none;
466
+ letter-spacing: -0.01em;
467
+ transition: all 0.14s ease;
468
+ }
469
+
470
+ .btn-primary {
471
+ background: var(--error);
472
+ color: #fff;
473
+ }
474
+
475
+ .btn-primary:hover {
476
+ background: #e63535;
477
+ transform: translateY(-1px);
478
+ box-shadow: 0 8px 20px -4px rgba(255, 69, 69, 0.3);
479
+ }
480
+
481
+ .btn-ghost {
482
+ background: var(--elevated);
483
+ color: var(--text-2);
484
+ border: 1px solid var(--border-hi);
485
+ }
486
+
487
+ .btn-ghost:hover {
488
+ background: var(--raised);
489
+ color: var(--text-1);
490
+ }
491
+
492
+ /* =========================================================
493
+ PRODUCTION 500
494
+ ========================================================= */
495
+ .prod-wrap {
496
+ display: flex;
497
+ flex-direction: column;
498
+ align-items: center;
499
+ justify-content: center;
500
+ text-align: center;
501
+ min-height: calc(100vh - 54px);
502
+ gap: 1.75rem;
503
+ padding: 2rem;
504
+ animation: fadeUp 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
505
+ }
506
+
507
+ .prod-code {
508
+ font-family: 'IBM Plex Sans', sans-serif;
509
+ font-size: clamp(5rem, 18vw, 11rem);
510
+ font-weight: 800;
511
+ letter-spacing: -0.06em;
512
+ line-height: 1;
513
+ background: linear-gradient(130deg, var(--error) 0%, #ff9000 100%);
514
+ -webkit-background-clip: text;
515
+ background-clip: text;
516
+ -webkit-text-fill-color: transparent;
517
+ }
518
+
519
+ .prod-heading {
520
+ font-family: 'IBM Plex Sans', sans-serif;
521
+ font-size: 1.6rem;
522
+ font-weight: 700;
523
+ letter-spacing: -0.025em;
524
+ color: var(--text-1);
525
+ }
526
+
527
+ .prod-sub {
528
+ color: var(--text-2);
529
+ font-size: 0.95rem;
530
+ font-weight: 300;
531
+ max-width: 380px;
532
+ line-height: 1.75;
533
+ }
534
+
535
+ /* =========================================================
536
+ FOOTER
537
+ ========================================================= */
538
+ .footer {
539
+ text-align: center;
540
+ margin-top: 4rem;
541
+ padding-top: 2rem;
542
+ border-top: 1px solid var(--border);
543
+ font-size: 0.72rem;
544
+ color: var(--text-3);
545
+ letter-spacing: 0.04em;
546
+ }
547
+
548
+ /* =========================================================
549
+ RESPONSIVE
550
+ ========================================================= */
551
+ @media (max-width: 600px) {
552
+ .main {
553
+ padding: 2rem 1rem 4rem;
554
+ }
555
+
556
+ .topbar {
557
+ padding: 0 1rem;
558
+ }
559
+
560
+ .frame {
561
+ grid-template-columns: 38px 1fr;
562
+ }
563
+
564
+ .frame-loc {
565
+ display: none;
566
+ }
567
+ }
568
+ </style>
569
+ </head>
570
+
571
+ <body>
572
+
573
+ <% /* ---------------------------------------------------------------- Helpers — parsed in EJS server-side
574
+ ---------------------------------------------------------------- */ function parseFrames(stack) { if (!stack)
575
+ return []; var frames=[]; var lines=stack.split('\n'); for (var i=0; i < lines.length; i++) { var
576
+ t=lines[i].trim(); if (!t.startsWith('at ')) continue;
577
+
578
+ // at FnName (/abs/path.js:10:5)
579
+ var m = t.match(/^at (.+?) \((.+?):(\d+):(\d+)\)$/);
580
+ if (m) { frames.push({ fn: m[1], file: m[2], line: m[3], col: m[4] }); continue; }
581
+
582
+ // at /abs/path.js:10:5 (no fn wrapping)
583
+ m = t.match(/^at (\/?.+?):(\d+):(\d+)$/);
584
+ if (m) { frames.push({ fn: ' <anonymous>', file: m[1], line: m[2], col: m[3] }); }
585
+ }
586
+ return frames;
587
+ }
588
+
589
+ function isApp(file) {
590
+ if (!file) return false;
591
+ return !file.includes('node_modules') && (
592
+ file.includes('/luak-express/') ||
593
+ file.includes('/src/') ||
594
+ file.includes('/app/') ||
595
+ file.includes('/routes/') ||
596
+ file.includes('/controllers/') ||
597
+ file.includes('/middleware/')
598
+ );
599
+ }
600
+
601
+ function fileName(f) {
602
+ if (!f) return '';
603
+ return f.split('/').pop();
604
+ }
605
+
606
+ function dirPath(f) {
607
+ if (!f) return '';
608
+ var parts = f.split('/');
609
+ parts.pop();
610
+ var rel = parts.join('/');
611
+ // Trim to last 4 segments for readability
612
+ var segs = parts.slice(-4);
613
+ return segs.length ? '…/' + segs.join('/') + '/' : '';
614
+ }
615
+
616
+ var frames = [];
617
+ var appCount = 0;
618
+ if (locals.debug) {
619
+ frames = parseFrames(locals.errorStack);
620
+ frames.forEach(function(f) { if (isApp(f.file)) appCount++; });
621
+ }
622
+ %>
623
+
624
+ <!-- ===== TOP BAR ===== -->
625
+ <div class="topbar">
626
+ <a href="/" class="logo">
627
+ <div class="logo-mark">Le</div>
628
+ Luak Express
629
+ </a>
630
+ <div class="topbar-right">
631
+ <% if (locals.debug) { %>
632
+ <span class="status-badge">HTTP 500 · Debug Mode</span>
633
+ <% } %>
634
+ </div>
635
+ </div>
636
+
637
+ <% if (locals.debug) { %>
638
+
639
+ <!-- ===== DEBUG VIEW ===== -->
640
+ <div class="main">
641
+
642
+ <!-- Hero -->
643
+ <div class="hero">
644
+ <div class="error-eyebrow">
645
+ <%= locals.errorName || 'Error' %>
646
+ </div>
647
+ <h1 class="error-title">Terjadi Kesalahan<br>pada Aplikasi</h1>
648
+ <div class="error-message-block">
649
+ <div class="error-message-bar"></div>
650
+ <div class="error-message-text">
651
+ <%= locals.errorMessage || 'Tidak ada pesan error.' %>
652
+ </div>
653
+ </div>
654
+ </div>
655
+
656
+ <!-- Stack Trace -->
657
+ <div class="section">
658
+ <div class="section-header">
659
+ <span class="section-label">Stack Trace</span>
660
+ <div class="section-rule"></div>
661
+ <span class="section-meta">
662
+ <%= frames.length %> frames &nbsp;·&nbsp; <span style="color:var(--app)">
663
+ <%= appCount %> app
664
+ </span>
665
+ </span>
666
+ </div>
667
+
668
+ <div class="legend">
669
+ <div class="legend-item">
670
+ <div class="legend-pip" style="background:var(--app)"></div>
671
+ Application frame
672
+ </div>
673
+ <div class="legend-item">
674
+ <div class="legend-pip" style="background:var(--fn)"></div>
675
+ External / node_modules
676
+ </div>
677
+ <div class="legend-item">
678
+ <div class="legend-pip" style="background:var(--line-num)"></div>
679
+ Line :number
680
+ </div>
681
+ </div>
682
+
683
+ <div class="frames">
684
+ <% if (frames.length> 0) { %>
685
+ <% frames.forEach(function(fr, idx) { var app=isApp(fr.file); var fn=fileName(fr.file); var
686
+ dir=dirPath(fr.file); %>
687
+ <div class="frame <%= app ? 'app-frame' : '' %>">
688
+ <div class="frame-num">#<%= idx %>
689
+ </div>
690
+ <div class="frame-body">
691
+ <div class="frame-fn">
692
+ <%= fr.fn %>
693
+ <% if (app) { %><span class="frame-pill">app</span>
694
+ <% } %>
695
+ </div>
696
+ <div class="frame-path">
697
+ <span>
698
+ <%= dir %>
699
+ </span><span class="fname">
700
+ <%= fn %>
701
+ </span>
702
+ </div>
703
+ </div>
704
+ <div class="frame-loc">
705
+ <div class="frame-line">:<%= fr.line %>
706
+ </div>
707
+ <div class="frame-col">col <%= fr.col %>
708
+ </div>
709
+ </div>
710
+ </div>
711
+ <% }); %>
712
+ <% } else if (locals.errorStack) { %>
713
+ <!-- Fallback raw stack -->
714
+ <div class="raw-stack">
715
+ <pre><%= locals.errorStack %></pre>
716
+ </div>
717
+ <% } %>
718
+ </div>
719
+ </div>
720
+
721
+ <!-- Actions -->
722
+ <div class="actions">
723
+ <a href="/" class="btn btn-primary">← Kembali ke Home</a>
724
+ <a href="https://github.com/google/luak-express/issues" target="_blank"
725
+ class="btn btn-ghost">Laporkan Bug ↗</a>
726
+ </div>
727
+
728
+ <div class="footer">
729
+ <%= locals.appName %> Framework &nbsp;·&nbsp; v<%= locals.appVersion %>
730
+ </div>
731
+ </div>
732
+
733
+ <% } else { %>
734
+
735
+ <!-- ===== PRODUCTION VIEW ===== -->
736
+ <div class="prod-wrap">
737
+ <div class="prod-code">500</div>
738
+ <div>
739
+ <div class="prod-heading">Something went wrong</div>
740
+ </div>
741
+ <p class="prod-sub">Maaf, kami mengalami kendala teknis. Tim kami sedang berusaha memperbaikinya
742
+ secepat mungkin.</p>
743
+ <a href="/" class="btn btn-primary">← Kembali ke Home</a>
744
+ </div>
745
+
746
+ <% } %>
747
+
748
+ </body>
749
+
750
+ </html>