rnilkyway commited on
Commit
c8c52c2
Β·
1 Parent(s): 4773cc9

refactor: extract HTML templates to separate files

Browse files

- Move landing.html and dashboard.html to app/templates/
- Add TemplateLoader class for cached template loading
- Reduce index.py from 2598 to 1381 lines
- Improve code maintainability and separation of concerns

app/index.py CHANGED
@@ -24,6 +24,7 @@ from config import (
24
  MASTER_KEY,
25
  AI_GATEWAY_URL, UPSTREAM_14448_URL,
26
  )
 
27
 
28
  # Free VPN detection: proxycheck.io (1000 req/day, no key needed)
29
  PROXYCHECK_URL = "https://proxycheck.io/v2/{ip}?vpn=1&asn=1"
@@ -785,970 +786,8 @@ async def send_upstream_request(body: dict, model: str):
785
  # ── Landing Page ────────────────────────────────────────────────────────────
786
 
787
  def landing_html() -> str:
788
- return """<!DOCTYPE html>
789
- <html lang="en">
790
- <head>
791
- <title>APIarium - Unified AI Gateway</title>
792
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
793
- <meta name="description" content="APIarium - High-performance OpenAI-compatible AI API gateway with multi-provider routing">
794
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
795
- <style>
796
- :root {
797
- --bg-primary: #000000;
798
- --bg-secondary: #0a0a0a;
799
- --bg-tertiary: #141414;
800
- --border: #2a2a2a;
801
- --text-primary: #ffffff;
802
- --text-secondary: #a0a0a0;
803
- --text-muted: #666666;
804
- --accent: #ffffff;
805
- --glass-bg: rgba(10, 10, 10, 0.9);
806
- --glass-border: rgba(255, 255, 255, 0.08);
807
- }
808
-
809
- * { margin: 0; padding: 0; box-sizing: border-box; }
810
-
811
- body {
812
- background: var(--bg-primary);
813
- color: var(--text-primary);
814
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
815
- line-height: 1.6;
816
- min-height: 100vh;
817
- }
818
-
819
- .container {
820
- max-width: 1200px;
821
- margin: 0 auto;
822
- padding: 0 24px;
823
- }
824
-
825
- header {
826
- border-bottom: 1px solid var(--border);
827
- padding: 20px 0;
828
- background: var(--glass-bg);
829
- backdrop-filter: blur(12px);
830
- position: sticky;
831
- top: 0;
832
- z-index: 100;
833
- }
834
-
835
- .header-content {
836
- display: flex;
837
- justify-content: space-between;
838
- align-items: center;
839
- }
840
-
841
- .logo {
842
- font-size: 24px;
843
- font-weight: 700;
844
- letter-spacing: -0.5px;
845
- }
846
-
847
- .status-badge {
848
- display: flex;
849
- align-items: center;
850
- gap: 8px;
851
- padding: 8px 16px;
852
- background: var(--bg-secondary);
853
- border: 1px solid var(--border);
854
- border-radius: 8px;
855
- font-size: 13px;
856
- font-weight: 500;
857
- }
858
-
859
- .status-dot {
860
- width: 8px;
861
- height: 8px;
862
- background: #fff;
863
- border-radius: 50%;
864
- animation: pulse 2s infinite;
865
- }
866
-
867
- @keyframes pulse {
868
- 0%, 100% { opacity: 1; }
869
- 50% { opacity: 0.5; }
870
- }
871
-
872
- .hero {
873
- padding: 80px 0;
874
- text-align: center;
875
- border-bottom: 1px solid var(--border);
876
- }
877
-
878
- .hero h1 {
879
- font-size: 56px;
880
- font-weight: 800;
881
- letter-spacing: -1.5px;
882
- margin-bottom: 16px;
883
- }
884
-
885
- .hero p {
886
- font-size: 18px;
887
- color: var(--text-secondary);
888
- max-width: 600px;
889
- margin: 0 auto 40px;
890
- }
891
-
892
- .stats-grid {
893
- display: grid;
894
- grid-template-columns: repeat(3, 1fr);
895
- gap: 24px;
896
- max-width: 800px;
897
- margin: 0 auto;
898
- }
899
-
900
- .stat-card {
901
- padding: 32px;
902
- background: var(--bg-secondary);
903
- border: 1px solid var(--border);
904
- border-radius: 12px;
905
- }
906
-
907
- .stat-value {
908
- font-size: 36px;
909
- font-weight: 700;
910
- margin-bottom: 8px;
911
- }
912
-
913
- .stat-label {
914
- font-size: 14px;
915
- color: var(--text-secondary);
916
- text-transform: uppercase;
917
- letter-spacing: 0.5px;
918
- }
919
-
920
- .section {
921
- padding: 80px 0;
922
- border-bottom: 1px solid var(--border);
923
- }
924
-
925
- .section h2 {
926
- font-size: 36px;
927
- font-weight: 700;
928
- margin-bottom: 16px;
929
- letter-spacing: -0.5px;
930
- }
931
-
932
- .section-subtitle {
933
- font-size: 16px;
934
- color: var(--text-secondary);
935
- margin-bottom: 48px;
936
- }
937
-
938
- .features-grid {
939
- display: grid;
940
- grid-template-columns: repeat(2, 1fr);
941
- gap: 24px;
942
- }
943
-
944
- .feature-card {
945
- padding: 32px;
946
- background: var(--bg-secondary);
947
- border: 1px solid var(--border);
948
- border-radius: 12px;
949
- transition: border-color 0.2s;
950
- }
951
-
952
- .feature-card:hover {
953
- border-color: #fff;
954
- }
955
-
956
- .feature-icon {
957
- font-size: 32px;
958
- margin-bottom: 16px;
959
- }
960
-
961
- .feature-title {
962
- font-size: 20px;
963
- font-weight: 600;
964
- margin-bottom: 12px;
965
- }
966
-
967
- .feature-desc {
968
- font-size: 14px;
969
- color: var(--text-secondary);
970
- line-height: 1.7;
971
- }
972
-
973
- .models-section {
974
- padding: 80px 0;
975
- border-bottom: 1px solid var(--border);
976
- }
977
-
978
- .models-header {
979
- display: flex;
980
- justify-content: space-between;
981
- align-items: center;
982
- margin-bottom: 32px;
983
- }
984
-
985
- .models-count {
986
- font-size: 14px;
987
- color: var(--text-secondary);
988
- padding: 8px 16px;
989
- background: var(--bg-secondary);
990
- border: 1px solid var(--border);
991
- border-radius: 8px;
992
- }
993
-
994
- .models-grid {
995
- display: grid;
996
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
997
- gap: 16px;
998
- }
999
-
1000
- .model-card {
1001
- padding: 20px;
1002
- background: var(--bg-secondary);
1003
- border: 1px solid var(--border);
1004
- border-radius: 10px;
1005
- font-family: 'JetBrains Mono', monospace;
1006
- font-size: 13px;
1007
- transition: all 0.2s;
1008
- cursor: pointer;
1009
- }
1010
-
1011
- .model-card:hover {
1012
- border-color: #fff;
1013
- background: var(--bg-tertiary);
1014
- }
1015
-
1016
- .model-name {
1017
- font-weight: 600;
1018
- margin-bottom: 8px;
1019
- word-break: break-all;
1020
- }
1021
-
1022
- .model-id {
1023
- font-size: 11px;
1024
- color: var(--text-muted);
1025
- word-break: break-all;
1026
- }
1027
-
1028
- .code-block {
1029
- background: var(--bg-secondary);
1030
- border: 1px solid var(--border);
1031
- border-radius: 12px;
1032
- padding: 24px;
1033
- font-family: 'JetBrains Mono', monospace;
1034
- font-size: 13px;
1035
- overflow-x: auto;
1036
- margin-top: 32px;
1037
- }
1038
-
1039
- .code-block pre {
1040
- margin: 0;
1041
- line-height: 1.8;
1042
- }
1043
-
1044
- .api-key-section {
1045
- padding: 80px 0;
1046
- }
1047
-
1048
- .api-key-box {
1049
- max-width: 600px;
1050
- margin: 0 auto;
1051
- padding: 32px;
1052
- background: var(--bg-secondary);
1053
- border: 1px solid var(--border);
1054
- border-radius: 12px;
1055
- }
1056
-
1057
- .api-key-input {
1058
- width: 100%;
1059
- padding: 14px 16px;
1060
- background: var(--bg-primary);
1061
- border: 1px solid var(--border);
1062
- border-radius: 8px;
1063
- color: var(--text-primary);
1064
- font-family: 'JetBrains Mono', monospace;
1065
- font-size: 13px;
1066
- margin-top: 16px;
1067
- }
1068
-
1069
- .btn {
1070
- padding: 12px 24px;
1071
- background: var(--text-primary);
1072
- color: var(--bg-primary);
1073
- border: none;
1074
- border-radius: 8px;
1075
- font-size: 14px;
1076
- font-weight: 600;
1077
- cursor: pointer;
1078
- margin-top: 16px;
1079
- transition: opacity 0.2s;
1080
- }
1081
-
1082
- .btn:hover {
1083
- opacity: 0.9;
1084
- }
1085
-
1086
- footer {
1087
- padding: 40px 0;
1088
- text-align: center;
1089
- color: var(--text-muted);
1090
- font-size: 13px;
1091
- }
1092
-
1093
- @media (max-width: 768px) {
1094
- .hero h1 { font-size: 36px; }
1095
- .stats-grid { grid-template-columns: 1fr; }
1096
- .features-grid { grid-template-columns: 1fr; }
1097
- .models-grid { grid-template-columns: 1fr; }
1098
- }mary);
1099
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
1100
- min-height: 100vh;
1101
- overflow-x: hidden;
1102
- }
1103
-
1104
- body {
1105
- background: #000;
1106
- }
1107
-
1108
- .container {
1109
- max-width: 1200px;
1110
- margin: 0 auto;
1111
- padding: 60px 24px;
1112
- }
1113
-
1114
- /* Hero Section */
1115
- .hero {
1116
- text-align: center;
1117
- margin-bottom: 48px;
1118
- padding: 80px 0 60px;
1119
- border-bottom: 1px solid var(--border);
1120
- }
1121
-
1122
- .hero-badge {
1123
- display: inline-flex;
1124
- align-items: center;
1125
- gap: 8px;
1126
- padding: 8px 16px;
1127
- background: var(--bg-secondary);
1128
- border: 1px solid var(--border);
1129
- border-radius: 24px;
1130
- font-size: 13px;
1131
- font-weight: 500;
1132
- color: var(--text-secondary);
1133
- margin-bottom: 24px;
1134
- }
1135
-
1136
- .hero-badge::before {
1137
- content: '';
1138
- width: 8px;
1139
- height: 8px;
1140
- background: #fff;
1141
- border-radius: 50%;
1142
- animation: pulse 2s infinite;
1143
- }
1144
-
1145
- @keyframes pulse {
1146
- 0%, 100% { opacity: 1; }
1147
- 50% { opacity: 0.5; }
1148
- }
1149
-
1150
- .hero h1 {
1151
- font-size: 56px;
1152
- font-weight: 800;
1153
- letter-spacing: -1.5px;
1154
- margin-bottom: 16px;
1155
- color: var(--text-primary);
1156
- }
1157
-
1158
- @keyframes fadeInUp {
1159
- from { opacity: 0; transform: translateY(30px); }
1160
- to { opacity: 1; transform: translateY(0); }
1161
- }
1162
-
1163
- .hero-logo {
1164
- font-size: 5rem;
1165
- margin-bottom: 16px;
1166
- animation: float 3s ease-in-out infinite;
1167
- filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.1));
1168
- }
1169
-
1170
- @keyframes float {
1171
- 0%, 100% { transform: translateY(0); }
1172
- 50% { transform: translateY(-10px); }
1173
- }
1174
-
1175
- .hero h1 {
1176
- font-size: 4rem;
1177
- font-weight: 900;
1178
- letter-spacing: -0.03em;
1179
- color: #fff;
1180
- margin-bottom: 12px;
1181
- }
1182
-
1183
- .hero-subtitle {
1184
- font-size: 1.15rem;
1185
- color: var(--text-secondary);
1186
- font-weight: 400;
1187
- margin-bottom: 24px;
1188
- }
1189
-
1190
- .feature-tags {
1191
- display: flex;
1192
- justify-content: center;
1193
- gap: 12px;
1194
- flex-wrap: wrap;
1195
- }
1196
-
1197
- .feature-tag {
1198
- display: inline-flex;
1199
- align-items: center;
1200
- gap: 6px;
1201
- padding: 8px 16px;
1202
- background: var(--glass-bg);
1203
- border: 1px solid var(--glass-border);
1204
- border-radius: 100px;
1205
- font-size: 0.85rem;
1206
- font-weight: 500;
1207
- color: var(--text-primary);
1208
- backdrop-filter: blur(10px);
1209
- transition: all 0.3s ease;
1210
- }
1211
-
1212
- .feature-tag:hover {
1213
- border-color: #fff;
1214
- transform: translateY(-2px);
1215
- box-shadow: 0 4px 20px rgba(255, 255, 255, 0.08);
1216
- }
1217
-
1218
- .feature-tag .icon {
1219
- font-size: 1.1rem;
1220
- }
1221
-
1222
- /* Glass Card */
1223
- .glass-card {
1224
- background: var(--glass-bg);
1225
- border: 1px solid var(--glass-border);
1226
- border-radius: 20px;
1227
- padding: 32px;
1228
- margin-bottom: 24px;
1229
- backdrop-filter: blur(20px);
1230
- transition: all 0.3s ease;
1231
- animation: fadeInUp 0.8s ease-out backwards;
1232
- }
1233
 
1234
- .glass-card:nth-child(2) { animation-delay: 0.1s; }
1235
- .glass-card:nth-child(3) { animation-delay: 0.2s; }
1236
- .glass-card:nth-child(4) { animation-delay: 0.3s; }
1237
-
1238
- .glass-card:hover {
1239
- border-color: rgba(255, 255, 255, 0.2);
1240
- transform: translateY(-4px);
1241
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1242
- }
1243
-
1244
- .card-header {
1245
- display: flex;
1246
- align-items: center;
1247
- gap: 12px;
1248
- margin-bottom: 20px;
1249
- }
1250
-
1251
- .card-icon {
1252
- width: 40px;
1253
- height: 40px;
1254
- display: flex;
1255
- align-items: center;
1256
- justify-content: center;
1257
- background: linear-gradient(135deg, #fff, #ccc);
1258
- border-radius: 12px;
1259
- font-size: 1.2rem;
1260
- }
1261
-
1262
- .card-title {
1263
- font-size: 1.25rem;
1264
- font-weight: 700;
1265
- color: var(--text-primary);
1266
- }
1267
-
1268
- .card-badge {
1269
- margin-left: auto;
1270
- padding: 4px 12px;
1271
- background: rgba(255, 255, 255, 0.05);
1272
- border: 1px solid rgba(255, 255, 255, 0.15);
1273
- border-radius: 100px;
1274
- font-size: 0.75rem;
1275
- font-weight: 600;
1276
- color: #fff;
1277
- }
1278
-
1279
- /* Key Card */
1280
- .key-card {
1281
- background: linear-gradient(135deg, rgba(17, 24, 39, 0.9), rgba(15, 23, 42, 0.9));
1282
- border: 1px solid rgba(255, 255, 255, 0.15);
1283
- position: relative;
1284
- overflow: hidden;
1285
- }
1286
-
1287
- .key-card::before {
1288
- content: '';
1289
- position: absolute;
1290
- top: 0;
1291
- left: 0;
1292
- right: 0;
1293
- height: 3px;
1294
- background: linear-gradient(90deg, #fff, #ccc, #999);
1295
- }
1296
-
1297
- .key-display {
1298
- background: var(--bg-primary);
1299
- border: 1px solid var(--border);
1300
- border-radius: 12px;
1301
- padding: 20px;
1302
- text-align: center;
1303
- cursor: pointer;
1304
- transition: all 0.3s ease;
1305
- position: relative;
1306
- }
1307
-
1308
- .key-display:hover {
1309
- border-color: #fff;
1310
- box-shadow: 0 0 30px rgba(255, 255, 255, 0.05);
1311
- }
1312
-
1313
- .key-label {
1314
- font-size: 0.75rem;
1315
- color: var(--text-muted);
1316
- text-transform: uppercase;
1317
- letter-spacing: 2px;
1318
- margin-bottom: 8px;
1319
- }
1320
-
1321
- .key-value {
1322
- font-family: 'JetBrains Mono', monospace;
1323
- font-size: 1.4rem;
1324
- font-weight: 700;
1325
- color: #fff;
1326
- word-break: break-all;
1327
- }
1328
-
1329
- .key-ip {
1330
- font-family: 'JetBrains Mono', monospace;
1331
- font-size: 0.9rem;
1332
- color: #ccc;
1333
- margin-bottom: 16px;
1334
- }
1335
-
1336
- .key-status {
1337
- display: inline-flex;
1338
- align-items: center;
1339
- gap: 6px;
1340
- padding: 6px 16px;
1341
- border-radius: 100px;
1342
- font-size: 0.85rem;
1343
- font-weight: 600;
1344
- margin-top: 12px;
1345
- }
1346
-
1347
- .status-active {
1348
- background: rgba(255, 255, 255, 0.05);
1349
- color: #fff;
1350
- }
1351
-
1352
- .status-blocked {
1353
- background: rgba(239, 68, 68, 0.1);
1354
- color: #ef4444;
1355
- }
1356
-
1357
- .status-verify {
1358
- background: rgba(0, 191, 255, 0.1);
1359
- color: #ccc;
1360
- }
1361
-
1362
- /* Endpoints */
1363
- .endpoint-list {
1364
- list-style: none;
1365
- }
1366
-
1367
- .endpoint-item {
1368
- display: flex;
1369
- align-items: center;
1370
- gap: 16px;
1371
- padding: 16px 0;
1372
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1373
- transition: all 0.2s ease;
1374
- }
1375
-
1376
- .endpoint-item:last-child {
1377
- border-bottom: none;
1378
- }
1379
-
1380
- .endpoint-item:hover {
1381
- padding-left: 8px;
1382
- }
1383
-
1384
- .method-badge {
1385
- font-family: 'JetBrains Mono', monospace;
1386
- font-size: 0.7rem;
1387
- font-weight: 700;
1388
- padding: 4px 10px;
1389
- border-radius: 6px;
1390
- min-width: 50px;
1391
- text-align: center;
1392
- text-transform: uppercase;
1393
- }
1394
-
1395
- .method-get {
1396
- background: rgba(255, 255, 255, 0.05);
1397
- color: #fff;
1398
- }
1399
-
1400
- .method-post {
1401
- background: rgba(0, 191, 255, 0.1);
1402
- color: #ccc;
1403
- }
1404
-
1405
- .endpoint-path {
1406
- font-family: 'JetBrains Mono', monospace;
1407
- font-size: 0.95rem;
1408
- color: var(--text-primary);
1409
- flex: 1;
1410
- }
1411
-
1412
- .endpoint-desc {
1413
- font-size: 0.85rem;
1414
- color: var(--text-muted);
1415
- text-align: right;
1416
- }
1417
-
1418
- /* Stats Grid */
1419
- .stats-grid {
1420
- display: grid;
1421
- grid-template-columns: repeat(3, 1fr);
1422
- gap: 16px;
1423
- }
1424
-
1425
- .stat-item {
1426
- background: var(--bg-primary);
1427
- border: 1px solid var(--border);
1428
- border-radius: 16px;
1429
- padding: 24px;
1430
- text-align: center;
1431
- transition: all 0.3s ease;
1432
- }
1433
-
1434
- .stat-item:hover {
1435
- border-color: #fff;
1436
- transform: translateY(-4px);
1437
- }
1438
-
1439
- .stat-value {
1440
- font-size: 2.5rem;
1441
- font-weight: 900;
1442
- background: linear-gradient(135deg, #fff, #ccc);
1443
- -webkit-background-clip: text;
1444
- -webkit-text-fill-color: transparent;
1445
- line-height: 1;
1446
- margin-bottom: 8px;
1447
- }
1448
-
1449
- .stat-label {
1450
- font-size: 0.8rem;
1451
- color: var(--text-muted);
1452
- text-transform: uppercase;
1453
- letter-spacing: 1px;
1454
- }
1455
-
1456
- /* Footer */
1457
- .footer {
1458
- text-align: center;
1459
- padding: 40px 0 20px;
1460
- color: var(--text-muted);
1461
- font-size: 0.85rem;
1462
- }
1463
-
1464
- .footer a {
1465
- color: #fff;
1466
- text-decoration: none;
1467
- }
1468
-
1469
- /* Toast */
1470
- .toast {
1471
- position: fixed;
1472
- bottom: 30px;
1473
- left: 50%;
1474
- transform: translateX(-50%) translateY(100px);
1475
- background: #fff;
1476
- color: var(--bg-primary);
1477
- padding: 14px 28px;
1478
- border-radius: 12px;
1479
- font-weight: 700;
1480
- font-size: 0.9rem;
1481
- opacity: 0;
1482
- transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1483
- z-index: 999;
1484
- box-shadow: 0 10px 40px rgba(255, 255, 255, 0.1);
1485
- }
1486
-
1487
- .toast.show {
1488
- opacity: 1;
1489
- transform: translateX(-50%) translateY(0);
1490
- }
1491
-
1492
- /* Turnstile */
1493
- .turnstile-wrapper {
1494
- margin: 20px 0;
1495
- display: flex;
1496
- justify-content: center;
1497
- }
1498
-
1499
- /* Responsive */
1500
- @media (max-width: 640px) {
1501
- .hero h1 { font-size: 2.5rem; }
1502
- .stats-grid { grid-template-columns: 1fr; }
1503
- .endpoint-desc { display: none; }
1504
- .container { padding: 40px 16px; }
1505
- }
1506
- </style>
1507
- </head>
1508
- <body>
1509
- <div class="bg-gradient"></div>
1510
-
1511
- <div class="container">
1512
- <!-- Hero -->
1513
- <div class="hero">
1514
- <div class="hero-logo">🐝</div>
1515
- <h1>APIarium</h1>
1516
- <p class="hero-subtitle">OpenAI-compatible API router with intelligent load balancing</p>
1517
- <div class="feature-tags">
1518
- <span class="feature-tag"><span class="icon">⚑</span> Auto-continue</span>
1519
- <span class="feature-tag"><span class="icon">πŸ”€</span> Multi-upstream</span>
1520
- <span class="feature-tag"><span class="icon">πŸ”‘</span> Auto-keys</span>
1521
- <span class="feature-tag"><span class="icon">πŸ›‘οΈ</span> Bot protection</span>
1522
- </div>
1523
- </div>
1524
-
1525
- <!-- Key Card -->
1526
- <div class="glass-card key-card">
1527
- <div class="card-header">
1528
- <div class="card-icon">πŸ”‘</div>
1529
- <div class="card-title">Your API Key</div>
1530
- <div class="card-badge" id="keyBadge">Loading...</div>
1531
- </div>
1532
-
1533
- <div style="text-align:center; margin-bottom:20px;">
1534
- <div class="key-label">IP Address</div>
1535
- <div class="key-ip" id="ykIp">Loading...</div>
1536
- </div>
1537
-
1538
- <div id="turnstile-container" style="display:none;">
1539
- <div class="turnstile-wrapper">
1540
- <div class="cf-turnstile" data-sitekey="0x4AAAAAADTUsX1T7VmpXhDi" data-callback="onTurnstileSuccess"></div>
1541
- </div>
1542
- </div>
1543
-
1544
- <div class="key-display" id="keyDisplay" onclick="copyKey()">
1545
- <div class="key-label">API Key</div>
1546
- <div class="key-value" id="ykKey">Loading...</div>
1547
- </div>
1548
-
1549
- <div style="text-align:center;">
1550
- <div id="ykStatus"></div>
1551
- <div style="font-size:0.8rem; color:var(--text-muted); margin-top:12px;" id="ykHint"></div>
1552
- </div>
1553
- </div>
1554
-
1555
- <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
1556
-
1557
- <!-- Endpoints -->
1558
- <div class="glass-card">
1559
- <div class="card-header">
1560
- <div class="card-icon">πŸ“‘</div>
1561
- <div class="card-title">Endpoints</div>
1562
- </div>
1563
- <ul class="endpoint-list">
1564
- <li class="endpoint-item">
1565
- <span class="method-badge method-get">GET</span>
1566
- <span class="endpoint-path">/v1/models</span>
1567
- <span class="endpoint-desc">List available models</span>
1568
- </li>
1569
- <li class="endpoint-item">
1570
- <span class="method-badge method-post">POST</span>
1571
- <span class="endpoint-path">/v1/chat/completions</span>
1572
- <span class="endpoint-desc">Chat completions</span>
1573
- </li>
1574
- <li class="endpoint-item">
1575
- <span class="method-badge method-get">GET</span>
1576
- <span class="endpoint-path">/health</span>
1577
- <span class="endpoint-desc">Health check</span>
1578
- </li>
1579
- </ul>
1580
- </div>
1581
-
1582
- <!-- Stats -->
1583
- <div class="glass-card">
1584
- <div class="card-header">
1585
- <div class="card-icon">πŸ“Š</div>
1586
- <div class="card-title">Statistics</div>
1587
- </div>
1588
- <div class="stats-grid">
1589
- <div class="stat-item">
1590
- <div class="stat-value" id="modelCount">-</div>
1591
- <div class="stat-label">Models</div>
1592
- </div>
1593
- <div class="stat-item">
1594
- <div class="stat-value">4</div>
1595
- <div class="stat-label">RPM Limit</div>
1596
- </div>
1597
- <div class="stat-item">
1598
- <div class="stat-value">v0.5</div>
1599
- <div class="stat-label">Version</div>
1600
- </div>
1601
- </div>
1602
- </div>
1603
-
1604
- <!-- Models -->
1605
- <div class="glass-card">
1606
- <div class="card-header">
1607
- <div class="card-icon">πŸ€–</div>
1608
- <div class="card-title">Available Models</div>
1609
- <div class="card-badge" id="modelsBadge">Loading...</div>
1610
- </div>
1611
- <div class="models-grid" id="modelsGrid">
1612
- <div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">Loading models...</div>
1613
- </div>
1614
- </div>
1615
-
1616
- <!-- Security -->
1617
- <div class="glass-card">
1618
- <div class="card-header">
1619
- <div class="card-icon">πŸ›‘οΈ</div>
1620
- <div class="card-title">Security</div>
1621
- </div>
1622
- <div style="display:grid;gap:12px;margin-top:16px;">
1623
- <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
1624
- <span style="font-size:1.2rem;">πŸ”’</span>
1625
- <div>
1626
- <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">VPN/Proxy Detection</div>
1627
- <div style="font-size:0.8rem;color:var(--text-secondary);">Automatic blocking via proxycheck.io</div>
1628
- </div>
1629
- </div>
1630
- <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
1631
- <span style="font-size:1.2rem;">πŸ€–</span>
1632
- <div>
1633
- <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">Cloudflare Turnstile</div>
1634
- <div style="font-size:0.8rem;color:var(--text-secondary);">Bot protection for key generation</div>
1635
- </div>
1636
- </div>
1637
- <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
1638
- <span style="font-size:1.2rem;">⚑</span>
1639
- <div>
1640
- <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">Rate Limiting</div>
1641
- <div style="font-size:0.8rem;color:var(--text-secondary);">4 RPM per IP, abuse protection</div>
1642
- </div>
1643
- </div>
1644
- <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
1645
- <span style="font-size:1.2rem;">πŸ”</span>
1646
- <div>
1647
- <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">AES-256 Encrypted Config</div>
1648
- <div style="font-size:0.8rem;color:var(--text-secondary);">All API keys encrypted at rest</div>
1649
- </div>
1650
- </div>
1651
- </div>
1652
- </div>
1653
-
1654
- <!-- Footer -->
1655
- <div class="footer">
1656
- 🐝 APIarium &mdash; Built for speed
1657
- </div>
1658
- </div>
1659
-
1660
- <!-- Toast -->
1661
- <div class="toast" id="toast">βœ“ Copied to clipboard!</div>
1662
-
1663
- <script>
1664
- const myKey = { key: null, ip: null };
1665
-
1666
- function onTurnstileSuccess(token) {
1667
- fetchKey(token);
1668
- }
1669
-
1670
- async function fetchKey(cfToken) {
1671
- try {
1672
- const url = cfToken ? '/v1/keys/me?cf_token=' + encodeURIComponent(cfToken) : '/v1/keys/me';
1673
- const resp = await fetch(url);
1674
- const data = await resp.json();
1675
-
1676
- if (resp.ok) {
1677
- myKey.ip = data.ip;
1678
- myKey.key = data.key;
1679
- document.getElementById('ykIp').textContent = data.ip;
1680
- document.getElementById('ykKey').textContent = data.key;
1681
- document.getElementById('turnstile-container').style.display = 'none';
1682
- document.getElementById('keyBadge').textContent = data.status === 'new' ? 'New' : 'Active';
1683
- document.getElementById('keyBadge').style.background = 'rgba(255, 255, 255, 0.05)';
1684
- document.getElementById('keyBadge').style.borderColor = 'rgba(255, 255, 255, 0.1)';
1685
-
1686
- if (data.status === 'new') {
1687
- document.getElementById('ykStatus').innerHTML = '<div class="key-status status-active">πŸŽ‰ New key generated!</div>';
1688
- } else {
1689
- document.getElementById('ykStatus').innerHTML = '<div class="key-status status-active">βœ… Active</div>';
1690
- }
1691
- document.getElementById('ykHint').textContent = 'Click key to copy Β· ' + data.rpm + ' RPM limit';
1692
- } else {
1693
- const err = data.error || {};
1694
- if (err.code === 'vpn_blocked') {
1695
- document.getElementById('ykIp').textContent = 'BLOCKED';
1696
- document.getElementById('ykKey').textContent = 'β€”';
1697
- document.getElementById('turnstile-container').style.display = 'none';
1698
- document.getElementById('keyBadge').textContent = 'Blocked';
1699
- document.getElementById('keyBadge').style.background = 'rgba(239, 68, 68, 0.1)';
1700
- document.getElementById('keyBadge').style.borderColor = 'rgba(255, 80, 80, 0.5)';
1701
- document.getElementById('ykStatus').innerHTML = '<div class="key-status status-blocked">🚫 VPN/Proxy detected</div>';
1702
- document.getElementById('ykHint').textContent = 'Turn off VPN and refresh';
1703
- } else {
1704
- document.getElementById('ykIp').textContent = 'Verification required';
1705
- document.getElementById('ykKey').textContent = 'β€”';
1706
- document.getElementById('turnstile-container').style.display = 'block';
1707
- document.getElementById('keyBadge').textContent = 'Verify';
1708
- document.getElementById('keyBadge').style.background = 'rgba(255, 255, 255, 0.05)';
1709
- document.getElementById('keyBadge').style.borderColor = 'rgba(255, 255, 255, 0.15)';
1710
- document.getElementById('ykStatus').innerHTML = '<div class="key-status status-verify">πŸ”’ Verify to get your key</div>';
1711
- }
1712
- }
1713
- } catch (e) {
1714
- document.getElementById('ykIp').textContent = 'Error';
1715
- document.getElementById('ykKey').textContent = 'Error loading';
1716
- }
1717
- }
1718
-
1719
- function copyKey() {
1720
- if (!myKey.key) return;
1721
- navigator.clipboard.writeText(myKey.key).then(() => {
1722
- const toast = document.getElementById('toast');
1723
- toast.classList.add('show');
1724
- setTimeout(() => toast.classList.remove('show'), 2500);
1725
- });
1726
- }
1727
-
1728
- fetch('/v1/models').then(r => r.json()).then(d => {
1729
- const models = d.data || [];
1730
- document.getElementById('modelCount').textContent = models.length;
1731
- document.getElementById('modelsBadge').textContent = models.length + ' models';
1732
- const grid = document.getElementById('modelsGrid');
1733
- if (models.length === 0) {
1734
- grid.innerHTML = '<div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">No models available</div>';
1735
- return;
1736
- }
1737
- grid.innerHTML = models.map(m => `
1738
- <div class="model-card" onclick="navigator.clipboard.writeText('${m.id}');showToast()">
1739
- <div class="model-name">${m.id}</div>
1740
- <div class="model-id">${m.owned_by || 'APIarium'}</div>
1741
- </div>
1742
- `).join('');
1743
- }).catch(() => {
1744
- document.getElementById('modelsBadge').textContent = 'Error';
1745
- document.getElementById('modelsGrid').innerHTML = '<div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">Failed to load models</div>';
1746
- });
1747
-
1748
- fetchKey();
1749
- </script>
1750
- </body>
1751
- </html>"""
1752
 
1753
 
1754
  # ── Routes ──────────────────────────────────────────────────────────────────
@@ -1863,259 +902,7 @@ async def dashboard_usage(request: Request, password: str = None):
1863
  <td>{model_breakdown}</td>
1864
  </tr>"""
1865
 
1866
- html = """
1867
- <!DOCTYPE html>
1868
- <html>
1869
- <head>
1870
- <title>🐝 APIarium Dashboard</title>
1871
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1872
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
1873
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1874
- <style>
1875
- :root {
1876
- --bg-primary: #0a0a0f;
1877
- --bg-secondary: #111827;
1878
- --bg-tertiary: #1f2937;
1879
- --border: #374151;
1880
- --text-primary: #f9fafb;
1881
- --text-secondary: #9ca3af;
1882
- --text-muted: #6b7280;
1883
- --accent-green: #fff;
1884
- --accent-blue: #ccc;
1885
- --accent-purple: #999;
1886
- --glass-bg: rgba(17, 24, 39, 0.8);
1887
- --glass-border: rgba(255, 255, 255, 0.1);
1888
- }
1889
-
1890
- * { margin: 0; padding: 0; box-sizing: border-box; }
1891
-
1892
- body {
1893
- background: var(--bg-primary);
1894
- color: var(--text-primary);
1895
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
1896
- min-height: 100vh;
1897
- padding: 40px 24px;
1898
- }
1899
-
1900
- .bg-gradient {
1901
- display: none;
1902
- }
1903
-
1904
- .container { max-width: 1200px; margin: 0 auto; position: relative; z-index: 1; }
1905
-
1906
- .header {
1907
- display: flex; align-items: center; justify-content: space-between;
1908
- margin-bottom: 40px; animation: fadeInUp 0.6s ease-out;
1909
- }
1910
-
1911
- @keyframes fadeInUp {
1912
- from { opacity: 0; transform: translateY(20px); }
1913
- to { opacity: 1; transform: translateY(0); }
1914
- }
1915
-
1916
- .header h1 {
1917
- font-size: 2rem; font-weight: 800;
1918
- background: linear-gradient(135deg, #fff, #ccc);
1919
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
1920
- display: flex; align-items: center; gap: 12px;
1921
- }
1922
-
1923
- .header h1 span { font-size: 2.5rem; }
1924
-
1925
- .back-btn {
1926
- padding: 10px 20px; background: var(--glass-bg); border: 1px solid var(--glass-border);
1927
- border-radius: 10px; color: var(--text-primary); text-decoration: none;
1928
- font-size: 0.9rem; font-weight: 500; transition: all 0.2s;
1929
- }
1930
- .back-btn:hover { border-color: #fff; transform: translateY(-2px); }
1931
-
1932
- /* Stats */
1933
- .stats-grid {
1934
- display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
1935
- margin-bottom: 32px; animation: fadeInUp 0.6s ease-out 0.1s backwards;
1936
- }
1937
-
1938
- .stat-card {
1939
- background: var(--glass-bg); border: 1px solid var(--glass-border);
1940
- border-radius: 16px; padding: 24px; text-align: center;
1941
- backdrop-filter: blur(20px); transition: all 0.3s ease;
1942
- }
1943
- .stat-card:hover { border-color: #fff; transform: translateY(-4px); }
1944
-
1945
- .stat-val {
1946
- font-size: 2.5rem; font-weight: 900;
1947
- background: linear-gradient(135deg, #fff, #ccc);
1948
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
1949
- line-height: 1; margin-bottom: 8px;
1950
- }
1951
- .stat-label { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }
1952
-
1953
- /* Charts */
1954
- .chart-grid {
1955
- display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
1956
- margin-bottom: 32px; animation: fadeInUp 0.6s ease-out 0.2s backwards;
1957
- }
1958
-
1959
- .glass-card {
1960
- background: var(--glass-bg); border: 1px solid var(--glass-border);
1961
- border-radius: 20px; padding: 28px; backdrop-filter: blur(20px);
1962
- transition: all 0.3s ease;
1963
- }
1964
- .glass-card:hover { border-color: rgba(255, 255, 255, 0.2); }
1965
-
1966
- .card-header {
1967
- display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
1968
- }
1969
- .card-icon {
1970
- width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
1971
- background: linear-gradient(135deg, #fff, #ccc);
1972
- border-radius: 10px; font-size: 1.1rem;
1973
- }
1974
- .card-title { font-size: 1.1rem; font-weight: 700; }
1975
-
1976
- .chart-container { position: relative; height: 280px; }
1977
-
1978
- /* Table */
1979
- .table-card {
1980
- animation: fadeInUp 0.6s ease-out 0.3s backwards;
1981
- }
1982
-
1983
- .table-wrapper { overflow-x: auto; max-height: 500px; overflow-y: auto; }
1984
-
1985
- table { width: 100%; border-collapse: collapse; }
1986
- th, td { padding: 14px 18px; text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
1987
- th {
1988
- color: var(--text-muted); font-size: 0.75rem; font-weight: 600;
1989
- text-transform: uppercase; letter-spacing: 1px;
1990
- background: var(--bg-primary); position: sticky; top: 0; z-index: 1;
1991
- }
1992
- tr { transition: background 0.2s; }
1993
- tr:hover { background: rgba(255, 255, 255, 0.03); }
1994
-
1995
- @media (max-width: 768px) {
1996
- .stats-grid { grid-template-columns: repeat(2, 1fr); }
1997
- .chart-grid { grid-template-columns: 1fr; }
1998
- .header { flex-direction: column; gap: 16px; }
1999
- }
2000
- </style>
2001
- </head>
2002
- <body>
2003
- <div class="bg-gradient"></div>
2004
- <div class="container">
2005
- <div class="header">
2006
- <h1>🐝 APIarium Dashboard</h1>
2007
- <a href="/" class="back-btn">← Home</a>
2008
- </div>
2009
-
2010
- <div class="stats-grid">
2011
- <div class="stat-card">
2012
- <div class="stat-val">TOTAL_REQUESTS</div>
2013
- <div class="stat-label">Total Requests</div>
2014
- </div>
2015
- <div class="stat-card">
2016
- <div class="stat-val">TOTAL_IPS</div>
2017
- <div class="stat-label">Unique IPs</div>
2018
- </div>
2019
- <div class="stat-card">
2020
- <div class="stat-val">TOTAL_MODELS</div>
2021
- <div class="stat-label">Models Used</div>
2022
- </div>
2023
- <div class="stat-card">
2024
- <div class="stat-val">TOTAL_KEYS</div>
2025
- <div class="stat-label">API Keys</div>
2026
- </div>
2027
- <div class="stat-card" style="border-color:rgba(251,191,36,0.3);">
2028
- <div class="stat-val" style="background:linear-gradient(135deg,#fbbf24,#f59e0b);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">GOD_IPS</div>
2029
- <div class="stat-label">πŸ‘‘ God Access IPs</div>
2030
- </div>
2031
- </div>
2032
-
2033
- <div class="chart-grid">
2034
- <div class="glass-card">
2035
- <div class="card-header">
2036
- <div class="card-icon">πŸ₯§</div>
2037
- <div class="card-title">Model Usage</div>
2038
- </div>
2039
- <div class="chart-container">
2040
- <canvas id="modelPieChart"></canvas>
2041
- </div>
2042
- </div>
2043
- <div class="glass-card">
2044
- <div class="card-header">
2045
- <div class="card-icon">πŸ“Š</div>
2046
- <div class="card-title">IP Usage</div>
2047
- </div>
2048
- <div class="chart-container">
2049
- <canvas id="ipBarChart"></canvas>
2050
- </div>
2051
- </div>
2052
- </div>
2053
-
2054
- <div class="glass-card table-card">
2055
- <div class="card-header">
2056
- <div class="card-icon">πŸ“‹</div>
2057
- <div class="card-title">IP Details</div>
2058
- </div>
2059
- <div class="table-wrapper">
2060
- <table>
2061
- <thead><tr><th>IP Address</th><th>API Keys</th><th>Total Requests</th><th>Model Breakdown</th></tr></thead>
2062
- <tbody>IP_ROWS</tbody>
2063
- </table>
2064
- </div>
2065
- </div>
2066
- </div>
2067
-
2068
- <script>
2069
- const colors = ['#fff', '#ccc', '#999', '#aaa', '#888', '#ffd93d', '#38bdf8', '#a3e635', '#f472b6', '#67e8f9'];
2070
-
2071
- new Chart(document.getElementById('modelPieChart'), {
2072
- type: 'doughnut',
2073
- data: {
2074
- labels: MODEL_LABELS,
2075
- datasets: [{
2076
- data: MODEL_TOTALS,
2077
- backgroundColor: colors.slice(0, MODEL_LABELS.length),
2078
- borderWidth: 0,
2079
- hoverBorderWidth: 4,
2080
- hoverBorderColor: '#0a0a0f',
2081
- }]
2082
- },
2083
- options: {
2084
- responsive: true,
2085
- maintainAspectRatio: false,
2086
- cutout: '65%',
2087
- plugins: {
2088
- legend: { position: 'bottom', labels: { color: '#9ca3af', padding: 16, font: { size: 11, family: 'Inter' } } }
2089
- }
2090
- }
2091
- });
2092
-
2093
- new Chart(document.getElementById('ipBarChart'), {
2094
- type: 'bar',
2095
- data: {
2096
- labels: IP_LABELS,
2097
- datasets: [{
2098
- label: 'Requests',
2099
- data: IP_TOTALS,
2100
- backgroundColor: colors.slice(0, IP_LABELS.length),
2101
- borderRadius: 8,
2102
- borderSkipped: false,
2103
- }]
2104
- },
2105
- options: {
2106
- responsive: true,
2107
- maintainAspectRatio: false,
2108
- plugins: { legend: { display: false } },
2109
- scales: {
2110
- y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#6b7280' } },
2111
- x: { grid: { display: false }, ticks: { color: '#c9d1d9', font: { size: 10 } } }
2112
- }
2113
- }
2114
- });
2115
- </script>
2116
- </body>
2117
- </html>
2118
- """
2119
 
2120
  total_requests = len(chat_logs)
2121
  unique_ips = len(ip_stats)
 
24
  MASTER_KEY,
25
  AI_GATEWAY_URL, UPSTREAM_14448_URL,
26
  )
27
+ from templates import templates
28
 
29
  # Free VPN detection: proxycheck.io (1000 req/day, no key needed)
30
  PROXYCHECK_URL = "https://proxycheck.io/v2/{ip}?vpn=1&asn=1"
 
786
  # ── Landing Page ────────────────────────────────────────────────────────────
787
 
788
  def landing_html() -> str:
789
+ return templates.load('landing.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
 
792
 
793
  # ── Routes ──────────────────────────────────────────────────────────────────
 
902
  <td>{model_breakdown}</td>
903
  </tr>"""
904
 
905
+ html = templates.load('dashboard.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
 
907
  total_requests = len(chat_logs)
908
  unique_ips = len(ip_stats)
app/templates.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from functools import lru_cache
4
+
5
+
6
+ class TemplateLoader:
7
+ """HTML template loader with caching."""
8
+
9
+ def __init__(self, template_dir: str = "templates"):
10
+ self.template_dir = Path(__file__).parent / template_dir
11
+ if not self.template_dir.exists():
12
+ raise ValueError(f"Template directory not found: {self.template_dir}")
13
+
14
+ @lru_cache(maxsize=32)
15
+ def load(self, name: str) -> str:
16
+ """Load and cache HTML template."""
17
+ template_path = self.template_dir / name
18
+ if not template_path.exists():
19
+ raise FileNotFoundError(f"Template not found: {template_path}")
20
+
21
+ with open(template_path, 'r', encoding='utf-8') as f:
22
+ return f.read()
23
+
24
+ def render(self, name: str, **kwargs) -> str:
25
+ """Load template and replace placeholders with values."""
26
+ html = self.load(name)
27
+ for key, value in kwargs.items():
28
+ placeholder = f"{{{{{key}}}}}"
29
+ html = html.replace(placeholder, str(value))
30
+ return html
31
+
32
+
33
+ # Global instance
34
+ templates = TemplateLoader()
app/templates/dashboard.html ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>🐝 APIarium Dashboard</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <style>
9
+ :root {
10
+ --bg-primary: #0a0a0f;
11
+ --bg-secondary: #111827;
12
+ --bg-tertiary: #1f2937;
13
+ --border: #374151;
14
+ --text-primary: #f9fafb;
15
+ --text-secondary: #9ca3af;
16
+ --text-muted: #6b7280;
17
+ --accent-green: #fff;
18
+ --accent-blue: #ccc;
19
+ --accent-purple: #999;
20
+ --glass-bg: rgba(17, 24, 39, 0.8);
21
+ --glass-border: rgba(255, 255, 255, 0.1);
22
+ }
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+ body {
25
+ background: var(--bg-primary);
26
+ color: var(--text-primary);
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
28
+ min-height: 100vh;
29
+ padding: 40px 24px;
30
+ }
31
+ .bg-gradient {
32
+ display: none;
33
+ }
34
+ .container { max-width: 1200px; margin: 0 auto; position: relative; z-index: 1; }
35
+ .header {
36
+ display: flex; align-items: center; justify-content: space-between;
37
+ margin-bottom: 40px; animation: fadeInUp 0.6s ease-out;
38
+ }
39
+ @keyframes fadeInUp {
40
+ from { opacity: 0; transform: translateY(20px); }
41
+ to { opacity: 1; transform: translateY(0); }
42
+ }
43
+ .header h1 {
44
+ font-size: 2rem; font-weight: 800;
45
+ background: linear-gradient(135deg, #fff, #ccc);
46
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
47
+ display: flex; align-items: center; gap: 12px;
48
+ }
49
+ .header h1 span { font-size: 2.5rem; }
50
+ .back-btn {
51
+ padding: 10px 20px; background: var(--glass-bg); border: 1px solid var(--glass-border);
52
+ border-radius: 10px; color: var(--text-primary); text-decoration: none;
53
+ font-size: 0.9rem; font-weight: 500; transition: all 0.2s;
54
+ }
55
+ .back-btn:hover { border-color: #fff; transform: translateY(-2px); }
56
+ /* Stats */
57
+ .stats-grid {
58
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
59
+ margin-bottom: 32px; animation: fadeInUp 0.6s ease-out 0.1s backwards;
60
+ }
61
+ .stat-card {
62
+ background: var(--glass-bg); border: 1px solid var(--glass-border);
63
+ border-radius: 16px; padding: 24px; text-align: center;
64
+ backdrop-filter: blur(20px); transition: all 0.3s ease;
65
+ }
66
+ .stat-card:hover { border-color: #fff; transform: translateY(-4px); }
67
+ .stat-val {
68
+ font-size: 2.5rem; font-weight: 900;
69
+ background: linear-gradient(135deg, #fff, #ccc);
70
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
71
+ line-height: 1; margin-bottom: 8px;
72
+ }
73
+ .stat-label { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }
74
+ /* Charts */
75
+ .chart-grid {
76
+ display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
77
+ margin-bottom: 32px; animation: fadeInUp 0.6s ease-out 0.2s backwards;
78
+ }
79
+ .glass-card {
80
+ background: var(--glass-bg); border: 1px solid var(--glass-border);
81
+ border-radius: 20px; padding: 28px; backdrop-filter: blur(20px);
82
+ transition: all 0.3s ease;
83
+ }
84
+ .glass-card:hover { border-color: rgba(255, 255, 255, 0.2); }
85
+ .card-header {
86
+ display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
87
+ }
88
+ .card-icon {
89
+ width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
90
+ background: linear-gradient(135deg, #fff, #ccc);
91
+ border-radius: 10px; font-size: 1.1rem;
92
+ }
93
+ .card-title { font-size: 1.1rem; font-weight: 700; }
94
+ .chart-container { position: relative; height: 280px; }
95
+ /* Table */
96
+ .table-card {
97
+ animation: fadeInUp 0.6s ease-out 0.3s backwards;
98
+ }
99
+ .table-wrapper { overflow-x: auto; max-height: 500px; overflow-y: auto; }
100
+ table { width: 100%; border-collapse: collapse; }
101
+ th, td { padding: 14px 18px; text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
102
+ th {
103
+ color: var(--text-muted); font-size: 0.75rem; font-weight: 600;
104
+ text-transform: uppercase; letter-spacing: 1px;
105
+ background: var(--bg-primary); position: sticky; top: 0; z-index: 1;
106
+ }
107
+ tr { transition: background 0.2s; }
108
+ tr:hover { background: rgba(255, 255, 255, 0.03); }
109
+ @media (max-width: 768px) {
110
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
111
+ .chart-grid { grid-template-columns: 1fr; }
112
+ .header { flex-direction: column; gap: 16px; }
113
+ }
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <div class="bg-gradient"></div>
118
+ <div class="container">
119
+ <div class="header">
120
+ <h1>🐝 APIarium Dashboard</h1>
121
+ <a href="/" class="back-btn">← Home</a>
122
+ </div>
123
+ <div class="stats-grid">
124
+ <div class="stat-card">
125
+ <div class="stat-val">TOTAL_REQUESTS</div>
126
+ <div class="stat-label">Total Requests</div>
127
+ </div>
128
+ <div class="stat-card">
129
+ <div class="stat-val">TOTAL_IPS</div>
130
+ <div class="stat-label">Unique IPs</div>
131
+ </div>
132
+ <div class="stat-card">
133
+ <div class="stat-val">TOTAL_MODELS</div>
134
+ <div class="stat-label">Models Used</div>
135
+ </div>
136
+ <div class="stat-card">
137
+ <div class="stat-val">TOTAL_KEYS</div>
138
+ <div class="stat-label">API Keys</div>
139
+ </div>
140
+ <div class="stat-card" style="border-color:rgba(251,191,36,0.3);">
141
+ <div class="stat-val" style="background:linear-gradient(135deg,#fbbf24,#f59e0b);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">GOD_IPS</div>
142
+ <div class="stat-label">πŸ‘‘ God Access IPs</div>
143
+ </div>
144
+ </div>
145
+ <div class="chart-grid">
146
+ <div class="glass-card">
147
+ <div class="card-header">
148
+ <div class="card-icon">πŸ₯§</div>
149
+ <div class="card-title">Model Usage</div>
150
+ </div>
151
+ <div class="chart-container">
152
+ <canvas id="modelPieChart"></canvas>
153
+ </div>
154
+ </div>
155
+ <div class="glass-card">
156
+ <div class="card-header">
157
+ <div class="card-icon">πŸ“Š</div>
158
+ <div class="card-title">IP Usage</div>
159
+ </div>
160
+ <div class="chart-container">
161
+ <canvas id="ipBarChart"></canvas>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ <div class="glass-card table-card">
166
+ <div class="card-header">
167
+ <div class="card-icon">πŸ“‹</div>
168
+ <div class="card-title">IP Details</div>
169
+ </div>
170
+ <div class="table-wrapper">
171
+ <table>
172
+ <thead><tr><th>IP Address</th><th>API Keys</th><th>Total Requests</th><th>Model Breakdown</th></tr></thead>
173
+ <tbody>IP_ROWS</tbody>
174
+ </table>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <script>
179
+ const colors = ['#fff', '#ccc', '#999', '#aaa', '#888', '#ffd93d', '#38bdf8', '#a3e635', '#f472b6', '#67e8f9'];
180
+ new Chart(document.getElementById('modelPieChart'), {
181
+ type: 'doughnut',
182
+ data: {
183
+ labels: MODEL_LABELS,
184
+ datasets: [{
185
+ data: MODEL_TOTALS,
186
+ backgroundColor: colors.slice(0, MODEL_LABELS.length),
187
+ borderWidth: 0,
188
+ hoverBorderWidth: 4,
189
+ hoverBorderColor: '#0a0a0f',
190
+ }]
191
+ },
192
+ options: {
193
+ responsive: true,
194
+ maintainAspectRatio: false,
195
+ cutout: '65%',
196
+ plugins: {
197
+ legend: { position: 'bottom', labels: { color: '#9ca3af', padding: 16, font: { size: 11, family: 'Inter' } } }
198
+ }
199
+ }
200
+ });
201
+ new Chart(document.getElementById('ipBarChart'), {
202
+ type: 'bar',
203
+ data: {
204
+ labels: IP_LABELS,
205
+ datasets: [{
206
+ label: 'Requests',
207
+ data: IP_TOTALS,
208
+ backgroundColor: colors.slice(0, IP_LABELS.length),
209
+ borderRadius: 8,
210
+ borderSkipped: false,
211
+ }]
212
+ },
213
+ options: {
214
+ responsive: true,
215
+ maintainAspectRatio: false,
216
+ plugins: { legend: { display: false } },
217
+ scales: {
218
+ y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#6b7280' } },
219
+ x: { grid: { display: false }, ticks: { color: '#c9d1d9', font: { size: 10 } } }
220
+ }
221
+ }
222
+ });
223
+ </script>
224
+ </body>
225
+ </html>
app/templates/landing.html ADDED
@@ -0,0 +1,847 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>APIarium - Unified AI Gateway</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="APIarium - High-performance OpenAI-compatible AI API gateway with multi-provider routing">
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg-primary: #000000;
11
+ --bg-secondary: #0a0a0a;
12
+ --bg-tertiary: #141414;
13
+ --border: #2a2a2a;
14
+ --text-primary: #ffffff;
15
+ --text-secondary: #a0a0a0;
16
+ --text-muted: #666666;
17
+ --accent: #ffffff;
18
+ --glass-bg: rgba(10, 10, 10, 0.9);
19
+ --glass-border: rgba(255, 255, 255, 0.08);
20
+ }
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+ body {
23
+ background: var(--bg-primary);
24
+ color: var(--text-primary);
25
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
26
+ line-height: 1.6;
27
+ min-height: 100vh;
28
+ }
29
+ .container {
30
+ max-width: 1200px;
31
+ margin: 0 auto;
32
+ padding: 0 24px;
33
+ }
34
+ header {
35
+ border-bottom: 1px solid var(--border);
36
+ padding: 20px 0;
37
+ background: var(--glass-bg);
38
+ backdrop-filter: blur(12px);
39
+ position: sticky;
40
+ top: 0;
41
+ z-index: 100;
42
+ }
43
+ .header-content {
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ }
48
+ .logo {
49
+ font-size: 24px;
50
+ font-weight: 700;
51
+ letter-spacing: -0.5px;
52
+ }
53
+ .status-badge {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ padding: 8px 16px;
58
+ background: var(--bg-secondary);
59
+ border: 1px solid var(--border);
60
+ border-radius: 8px;
61
+ font-size: 13px;
62
+ font-weight: 500;
63
+ }
64
+ .status-dot {
65
+ width: 8px;
66
+ height: 8px;
67
+ background: #fff;
68
+ border-radius: 50%;
69
+ animation: pulse 2s infinite;
70
+ }
71
+ @keyframes pulse {
72
+ 0%, 100% { opacity: 1; }
73
+ 50% { opacity: 0.5; }
74
+ }
75
+ .hero {
76
+ padding: 80px 0;
77
+ text-align: center;
78
+ border-bottom: 1px solid var(--border);
79
+ }
80
+ .hero h1 {
81
+ font-size: 56px;
82
+ font-weight: 800;
83
+ letter-spacing: -1.5px;
84
+ margin-bottom: 16px;
85
+ }
86
+ .hero p {
87
+ font-size: 18px;
88
+ color: var(--text-secondary);
89
+ max-width: 600px;
90
+ margin: 0 auto 40px;
91
+ }
92
+ .stats-grid {
93
+ display: grid;
94
+ grid-template-columns: repeat(3, 1fr);
95
+ gap: 24px;
96
+ max-width: 800px;
97
+ margin: 0 auto;
98
+ }
99
+ .stat-card {
100
+ padding: 32px;
101
+ background: var(--bg-secondary);
102
+ border: 1px solid var(--border);
103
+ border-radius: 12px;
104
+ }
105
+ .stat-value {
106
+ font-size: 36px;
107
+ font-weight: 700;
108
+ margin-bottom: 8px;
109
+ }
110
+ .stat-label {
111
+ font-size: 14px;
112
+ color: var(--text-secondary);
113
+ text-transform: uppercase;
114
+ letter-spacing: 0.5px;
115
+ }
116
+ .section {
117
+ padding: 80px 0;
118
+ border-bottom: 1px solid var(--border);
119
+ }
120
+ .section h2 {
121
+ font-size: 36px;
122
+ font-weight: 700;
123
+ margin-bottom: 16px;
124
+ letter-spacing: -0.5px;
125
+ }
126
+ .section-subtitle {
127
+ font-size: 16px;
128
+ color: var(--text-secondary);
129
+ margin-bottom: 48px;
130
+ }
131
+ .features-grid {
132
+ display: grid;
133
+ grid-template-columns: repeat(2, 1fr);
134
+ gap: 24px;
135
+ }
136
+ .feature-card {
137
+ padding: 32px;
138
+ background: var(--bg-secondary);
139
+ border: 1px solid var(--border);
140
+ border-radius: 12px;
141
+ transition: border-color 0.2s;
142
+ }
143
+ .feature-card:hover {
144
+ border-color: #fff;
145
+ }
146
+ .feature-icon {
147
+ font-size: 32px;
148
+ margin-bottom: 16px;
149
+ }
150
+ .feature-title {
151
+ font-size: 20px;
152
+ font-weight: 600;
153
+ margin-bottom: 12px;
154
+ }
155
+ .feature-desc {
156
+ font-size: 14px;
157
+ color: var(--text-secondary);
158
+ line-height: 1.7;
159
+ }
160
+ .models-section {
161
+ padding: 80px 0;
162
+ border-bottom: 1px solid var(--border);
163
+ }
164
+ .models-header {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ margin-bottom: 32px;
169
+ }
170
+ .models-count {
171
+ font-size: 14px;
172
+ color: var(--text-secondary);
173
+ padding: 8px 16px;
174
+ background: var(--bg-secondary);
175
+ border: 1px solid var(--border);
176
+ border-radius: 8px;
177
+ }
178
+ .models-grid {
179
+ display: grid;
180
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
181
+ gap: 16px;
182
+ }
183
+ .model-card {
184
+ padding: 20px;
185
+ background: var(--bg-secondary);
186
+ border: 1px solid var(--border);
187
+ border-radius: 10px;
188
+ font-family: 'JetBrains Mono', monospace;
189
+ font-size: 13px;
190
+ transition: all 0.2s;
191
+ cursor: pointer;
192
+ }
193
+ .model-card:hover {
194
+ border-color: #fff;
195
+ background: var(--bg-tertiary);
196
+ }
197
+ .model-name {
198
+ font-weight: 600;
199
+ margin-bottom: 8px;
200
+ word-break: break-all;
201
+ }
202
+ .model-id {
203
+ font-size: 11px;
204
+ color: var(--text-muted);
205
+ word-break: break-all;
206
+ }
207
+ .code-block {
208
+ background: var(--bg-secondary);
209
+ border: 1px solid var(--border);
210
+ border-radius: 12px;
211
+ padding: 24px;
212
+ font-family: 'JetBrains Mono', monospace;
213
+ font-size: 13px;
214
+ overflow-x: auto;
215
+ margin-top: 32px;
216
+ }
217
+ .code-block pre {
218
+ margin: 0;
219
+ line-height: 1.8;
220
+ }
221
+ .api-key-section {
222
+ padding: 80px 0;
223
+ }
224
+ .api-key-box {
225
+ max-width: 600px;
226
+ margin: 0 auto;
227
+ padding: 32px;
228
+ background: var(--bg-secondary);
229
+ border: 1px solid var(--border);
230
+ border-radius: 12px;
231
+ }
232
+ .api-key-input {
233
+ width: 100%;
234
+ padding: 14px 16px;
235
+ background: var(--bg-primary);
236
+ border: 1px solid var(--border);
237
+ border-radius: 8px;
238
+ color: var(--text-primary);
239
+ font-family: 'JetBrains Mono', monospace;
240
+ font-size: 13px;
241
+ margin-top: 16px;
242
+ }
243
+ .btn {
244
+ padding: 12px 24px;
245
+ background: var(--text-primary);
246
+ color: var(--bg-primary);
247
+ border: none;
248
+ border-radius: 8px;
249
+ font-size: 14px;
250
+ font-weight: 600;
251
+ cursor: pointer;
252
+ margin-top: 16px;
253
+ transition: opacity 0.2s;
254
+ }
255
+ .btn:hover {
256
+ opacity: 0.9;
257
+ }
258
+ footer {
259
+ padding: 40px 0;
260
+ text-align: center;
261
+ color: var(--text-muted);
262
+ font-size: 13px;
263
+ }
264
+ @media (max-width: 768px) {
265
+ .hero h1 { font-size: 36px; }
266
+ .stats-grid { grid-template-columns: 1fr; }
267
+ .features-grid { grid-template-columns: 1fr; }
268
+ .models-grid { grid-template-columns: 1fr; }
269
+ }mary);
270
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
271
+ min-height: 100vh;
272
+ overflow-x: hidden;
273
+ }
274
+ body {
275
+ background: #000;
276
+ }
277
+ .container {
278
+ max-width: 1200px;
279
+ margin: 0 auto;
280
+ padding: 60px 24px;
281
+ }
282
+ /* Hero Section */
283
+ .hero {
284
+ text-align: center;
285
+ margin-bottom: 48px;
286
+ padding: 80px 0 60px;
287
+ border-bottom: 1px solid var(--border);
288
+ }
289
+ .hero-badge {
290
+ display: inline-flex;
291
+ align-items: center;
292
+ gap: 8px;
293
+ padding: 8px 16px;
294
+ background: var(--bg-secondary);
295
+ border: 1px solid var(--border);
296
+ border-radius: 24px;
297
+ font-size: 13px;
298
+ font-weight: 500;
299
+ color: var(--text-secondary);
300
+ margin-bottom: 24px;
301
+ }
302
+ .hero-badge::before {
303
+ content: '';
304
+ width: 8px;
305
+ height: 8px;
306
+ background: #fff;
307
+ border-radius: 50%;
308
+ animation: pulse 2s infinite;
309
+ }
310
+ @keyframes pulse {
311
+ 0%, 100% { opacity: 1; }
312
+ 50% { opacity: 0.5; }
313
+ }
314
+ .hero h1 {
315
+ font-size: 56px;
316
+ font-weight: 800;
317
+ letter-spacing: -1.5px;
318
+ margin-bottom: 16px;
319
+ color: var(--text-primary);
320
+ }
321
+ @keyframes fadeInUp {
322
+ from { opacity: 0; transform: translateY(30px); }
323
+ to { opacity: 1; transform: translateY(0); }
324
+ }
325
+ .hero-logo {
326
+ font-size: 5rem;
327
+ margin-bottom: 16px;
328
+ animation: float 3s ease-in-out infinite;
329
+ filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.1));
330
+ }
331
+ @keyframes float {
332
+ 0%, 100% { transform: translateY(0); }
333
+ 50% { transform: translateY(-10px); }
334
+ }
335
+ .hero h1 {
336
+ font-size: 4rem;
337
+ font-weight: 900;
338
+ letter-spacing: -0.03em;
339
+ color: #fff;
340
+ margin-bottom: 12px;
341
+ }
342
+ .hero-subtitle {
343
+ font-size: 1.15rem;
344
+ color: var(--text-secondary);
345
+ font-weight: 400;
346
+ margin-bottom: 24px;
347
+ }
348
+ .feature-tags {
349
+ display: flex;
350
+ justify-content: center;
351
+ gap: 12px;
352
+ flex-wrap: wrap;
353
+ }
354
+ .feature-tag {
355
+ display: inline-flex;
356
+ align-items: center;
357
+ gap: 6px;
358
+ padding: 8px 16px;
359
+ background: var(--glass-bg);
360
+ border: 1px solid var(--glass-border);
361
+ border-radius: 100px;
362
+ font-size: 0.85rem;
363
+ font-weight: 500;
364
+ color: var(--text-primary);
365
+ backdrop-filter: blur(10px);
366
+ transition: all 0.3s ease;
367
+ }
368
+ .feature-tag:hover {
369
+ border-color: #fff;
370
+ transform: translateY(-2px);
371
+ box-shadow: 0 4px 20px rgba(255, 255, 255, 0.08);
372
+ }
373
+ .feature-tag .icon {
374
+ font-size: 1.1rem;
375
+ }
376
+ /* Glass Card */
377
+ .glass-card {
378
+ background: var(--glass-bg);
379
+ border: 1px solid var(--glass-border);
380
+ border-radius: 20px;
381
+ padding: 32px;
382
+ margin-bottom: 24px;
383
+ backdrop-filter: blur(20px);
384
+ transition: all 0.3s ease;
385
+ animation: fadeInUp 0.8s ease-out backwards;
386
+ }
387
+ .glass-card:nth-child(2) { animation-delay: 0.1s; }
388
+ .glass-card:nth-child(3) { animation-delay: 0.2s; }
389
+ .glass-card:nth-child(4) { animation-delay: 0.3s; }
390
+ .glass-card:hover {
391
+ border-color: rgba(255, 255, 255, 0.2);
392
+ transform: translateY(-4px);
393
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
394
+ }
395
+ .card-header {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 12px;
399
+ margin-bottom: 20px;
400
+ }
401
+ .card-icon {
402
+ width: 40px;
403
+ height: 40px;
404
+ display: flex;
405
+ align-items: center;
406
+ justify-content: center;
407
+ background: linear-gradient(135deg, #fff, #ccc);
408
+ border-radius: 12px;
409
+ font-size: 1.2rem;
410
+ }
411
+ .card-title {
412
+ font-size: 1.25rem;
413
+ font-weight: 700;
414
+ color: var(--text-primary);
415
+ }
416
+ .card-badge {
417
+ margin-left: auto;
418
+ padding: 4px 12px;
419
+ background: rgba(255, 255, 255, 0.05);
420
+ border: 1px solid rgba(255, 255, 255, 0.15);
421
+ border-radius: 100px;
422
+ font-size: 0.75rem;
423
+ font-weight: 600;
424
+ color: #fff;
425
+ }
426
+ /* Key Card */
427
+ .key-card {
428
+ background: linear-gradient(135deg, rgba(17, 24, 39, 0.9), rgba(15, 23, 42, 0.9));
429
+ border: 1px solid rgba(255, 255, 255, 0.15);
430
+ position: relative;
431
+ overflow: hidden;
432
+ }
433
+ .key-card::before {
434
+ content: '';
435
+ position: absolute;
436
+ top: 0;
437
+ left: 0;
438
+ right: 0;
439
+ height: 3px;
440
+ background: linear-gradient(90deg, #fff, #ccc, #999);
441
+ }
442
+ .key-display {
443
+ background: var(--bg-primary);
444
+ border: 1px solid var(--border);
445
+ border-radius: 12px;
446
+ padding: 20px;
447
+ text-align: center;
448
+ cursor: pointer;
449
+ transition: all 0.3s ease;
450
+ position: relative;
451
+ }
452
+ .key-display:hover {
453
+ border-color: #fff;
454
+ box-shadow: 0 0 30px rgba(255, 255, 255, 0.05);
455
+ }
456
+ .key-label {
457
+ font-size: 0.75rem;
458
+ color: var(--text-muted);
459
+ text-transform: uppercase;
460
+ letter-spacing: 2px;
461
+ margin-bottom: 8px;
462
+ }
463
+ .key-value {
464
+ font-family: 'JetBrains Mono', monospace;
465
+ font-size: 1.4rem;
466
+ font-weight: 700;
467
+ color: #fff;
468
+ word-break: break-all;
469
+ }
470
+ .key-ip {
471
+ font-family: 'JetBrains Mono', monospace;
472
+ font-size: 0.9rem;
473
+ color: #ccc;
474
+ margin-bottom: 16px;
475
+ }
476
+ .key-status {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: 6px;
480
+ padding: 6px 16px;
481
+ border-radius: 100px;
482
+ font-size: 0.85rem;
483
+ font-weight: 600;
484
+ margin-top: 12px;
485
+ }
486
+ .status-active {
487
+ background: rgba(255, 255, 255, 0.05);
488
+ color: #fff;
489
+ }
490
+ .status-blocked {
491
+ background: rgba(239, 68, 68, 0.1);
492
+ color: #ef4444;
493
+ }
494
+ .status-verify {
495
+ background: rgba(0, 191, 255, 0.1);
496
+ color: #ccc;
497
+ }
498
+ /* Endpoints */
499
+ .endpoint-list {
500
+ list-style: none;
501
+ }
502
+ .endpoint-item {
503
+ display: flex;
504
+ align-items: center;
505
+ gap: 16px;
506
+ padding: 16px 0;
507
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
508
+ transition: all 0.2s ease;
509
+ }
510
+ .endpoint-item:last-child {
511
+ border-bottom: none;
512
+ }
513
+ .endpoint-item:hover {
514
+ padding-left: 8px;
515
+ }
516
+ .method-badge {
517
+ font-family: 'JetBrains Mono', monospace;
518
+ font-size: 0.7rem;
519
+ font-weight: 700;
520
+ padding: 4px 10px;
521
+ border-radius: 6px;
522
+ min-width: 50px;
523
+ text-align: center;
524
+ text-transform: uppercase;
525
+ }
526
+ .method-get {
527
+ background: rgba(255, 255, 255, 0.05);
528
+ color: #fff;
529
+ }
530
+ .method-post {
531
+ background: rgba(0, 191, 255, 0.1);
532
+ color: #ccc;
533
+ }
534
+ .endpoint-path {
535
+ font-family: 'JetBrains Mono', monospace;
536
+ font-size: 0.95rem;
537
+ color: var(--text-primary);
538
+ flex: 1;
539
+ }
540
+ .endpoint-desc {
541
+ font-size: 0.85rem;
542
+ color: var(--text-muted);
543
+ text-align: right;
544
+ }
545
+ /* Stats Grid */
546
+ .stats-grid {
547
+ display: grid;
548
+ grid-template-columns: repeat(3, 1fr);
549
+ gap: 16px;
550
+ }
551
+ .stat-item {
552
+ background: var(--bg-primary);
553
+ border: 1px solid var(--border);
554
+ border-radius: 16px;
555
+ padding: 24px;
556
+ text-align: center;
557
+ transition: all 0.3s ease;
558
+ }
559
+ .stat-item:hover {
560
+ border-color: #fff;
561
+ transform: translateY(-4px);
562
+ }
563
+ .stat-value {
564
+ font-size: 2.5rem;
565
+ font-weight: 900;
566
+ background: linear-gradient(135deg, #fff, #ccc);
567
+ -webkit-background-clip: text;
568
+ -webkit-text-fill-color: transparent;
569
+ line-height: 1;
570
+ margin-bottom: 8px;
571
+ }
572
+ .stat-label {
573
+ font-size: 0.8rem;
574
+ color: var(--text-muted);
575
+ text-transform: uppercase;
576
+ letter-spacing: 1px;
577
+ }
578
+ /* Footer */
579
+ .footer {
580
+ text-align: center;
581
+ padding: 40px 0 20px;
582
+ color: var(--text-muted);
583
+ font-size: 0.85rem;
584
+ }
585
+ .footer a {
586
+ color: #fff;
587
+ text-decoration: none;
588
+ }
589
+ /* Toast */
590
+ .toast {
591
+ position: fixed;
592
+ bottom: 30px;
593
+ left: 50%;
594
+ transform: translateX(-50%) translateY(100px);
595
+ background: #fff;
596
+ color: var(--bg-primary);
597
+ padding: 14px 28px;
598
+ border-radius: 12px;
599
+ font-weight: 700;
600
+ font-size: 0.9rem;
601
+ opacity: 0;
602
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
603
+ z-index: 999;
604
+ box-shadow: 0 10px 40px rgba(255, 255, 255, 0.1);
605
+ }
606
+ .toast.show {
607
+ opacity: 1;
608
+ transform: translateX(-50%) translateY(0);
609
+ }
610
+ /* Turnstile */
611
+ .turnstile-wrapper {
612
+ margin: 20px 0;
613
+ display: flex;
614
+ justify-content: center;
615
+ }
616
+ /* Responsive */
617
+ @media (max-width: 640px) {
618
+ .hero h1 { font-size: 2.5rem; }
619
+ .stats-grid { grid-template-columns: 1fr; }
620
+ .endpoint-desc { display: none; }
621
+ .container { padding: 40px 16px; }
622
+ }
623
+ </style>
624
+ </head>
625
+ <body>
626
+ <div class="bg-gradient"></div>
627
+ <div class="container">
628
+ <!-- Hero -->
629
+ <div class="hero">
630
+ <div class="hero-logo">🐝</div>
631
+ <h1>APIarium</h1>
632
+ <p class="hero-subtitle">OpenAI-compatible API router with intelligent load balancing</p>
633
+ <div class="feature-tags">
634
+ <span class="feature-tag"><span class="icon">⚑</span> Auto-continue</span>
635
+ <span class="feature-tag"><span class="icon">πŸ”€</span> Multi-upstream</span>
636
+ <span class="feature-tag"><span class="icon">πŸ”‘</span> Auto-keys</span>
637
+ <span class="feature-tag"><span class="icon">πŸ›‘οΈ</span> Bot protection</span>
638
+ </div>
639
+ </div>
640
+ <!-- Key Card -->
641
+ <div class="glass-card key-card">
642
+ <div class="card-header">
643
+ <div class="card-icon">πŸ”‘</div>
644
+ <div class="card-title">Your API Key</div>
645
+ <div class="card-badge" id="keyBadge">Loading...</div>
646
+ </div>
647
+ <div style="text-align:center; margin-bottom:20px;">
648
+ <div class="key-label">IP Address</div>
649
+ <div class="key-ip" id="ykIp">Loading...</div>
650
+ </div>
651
+ <div id="turnstile-container" style="display:none;">
652
+ <div class="turnstile-wrapper">
653
+ <div class="cf-turnstile" data-sitekey="0x4AAAAAADTUsX1T7VmpXhDi" data-callback="onTurnstileSuccess"></div>
654
+ </div>
655
+ </div>
656
+ <div class="key-display" id="keyDisplay" onclick="copyKey()">
657
+ <div class="key-label">API Key</div>
658
+ <div class="key-value" id="ykKey">Loading...</div>
659
+ </div>
660
+ <div style="text-align:center;">
661
+ <div id="ykStatus"></div>
662
+ <div style="font-size:0.8rem; color:var(--text-muted); margin-top:12px;" id="ykHint"></div>
663
+ </div>
664
+ </div>
665
+ <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
666
+ <!-- Endpoints -->
667
+ <div class="glass-card">
668
+ <div class="card-header">
669
+ <div class="card-icon">πŸ“‘</div>
670
+ <div class="card-title">Endpoints</div>
671
+ </div>
672
+ <ul class="endpoint-list">
673
+ <li class="endpoint-item">
674
+ <span class="method-badge method-get">GET</span>
675
+ <span class="endpoint-path">/v1/models</span>
676
+ <span class="endpoint-desc">List available models</span>
677
+ </li>
678
+ <li class="endpoint-item">
679
+ <span class="method-badge method-post">POST</span>
680
+ <span class="endpoint-path">/v1/chat/completions</span>
681
+ <span class="endpoint-desc">Chat completions</span>
682
+ </li>
683
+ <li class="endpoint-item">
684
+ <span class="method-badge method-get">GET</span>
685
+ <span class="endpoint-path">/health</span>
686
+ <span class="endpoint-desc">Health check</span>
687
+ </li>
688
+ </ul>
689
+ </div>
690
+ <!-- Stats -->
691
+ <div class="glass-card">
692
+ <div class="card-header">
693
+ <div class="card-icon">πŸ“Š</div>
694
+ <div class="card-title">Statistics</div>
695
+ </div>
696
+ <div class="stats-grid">
697
+ <div class="stat-item">
698
+ <div class="stat-value" id="modelCount">-</div>
699
+ <div class="stat-label">Models</div>
700
+ </div>
701
+ <div class="stat-item">
702
+ <div class="stat-value">4</div>
703
+ <div class="stat-label">RPM Limit</div>
704
+ </div>
705
+ <div class="stat-item">
706
+ <div class="stat-value">v0.5</div>
707
+ <div class="stat-label">Version</div>
708
+ </div>
709
+ </div>
710
+ </div>
711
+ <!-- Models -->
712
+ <div class="glass-card">
713
+ <div class="card-header">
714
+ <div class="card-icon">πŸ€–</div>
715
+ <div class="card-title">Available Models</div>
716
+ <div class="card-badge" id="modelsBadge">Loading...</div>
717
+ </div>
718
+ <div class="models-grid" id="modelsGrid">
719
+ <div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">Loading models...</div>
720
+ </div>
721
+ </div>
722
+ <!-- Security -->
723
+ <div class="glass-card">
724
+ <div class="card-header">
725
+ <div class="card-icon">πŸ›‘οΈ</div>
726
+ <div class="card-title">Security</div>
727
+ </div>
728
+ <div style="display:grid;gap:12px;margin-top:16px;">
729
+ <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
730
+ <span style="font-size:1.2rem;">πŸ”’</span>
731
+ <div>
732
+ <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">VPN/Proxy Detection</div>
733
+ <div style="font-size:0.8rem;color:var(--text-secondary);">Automatic blocking via proxycheck.io</div>
734
+ </div>
735
+ </div>
736
+ <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
737
+ <span style="font-size:1.2rem;">πŸ€–</span>
738
+ <div>
739
+ <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">Cloudflare Turnstile</div>
740
+ <div style="font-size:0.8rem;color:var(--text-secondary);">Bot protection for key generation</div>
741
+ </div>
742
+ </div>
743
+ <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
744
+ <span style="font-size:1.2rem;">⚑</span>
745
+ <div>
746
+ <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">Rate Limiting</div>
747
+ <div style="font-size:0.8rem;color:var(--text-secondary);">4 RPM per IP, abuse protection</div>
748
+ </div>
749
+ </div>
750
+ <div style="display:flex;align-items:center;gap:12px;padding:12px;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px;">
751
+ <span style="font-size:1.2rem;">πŸ”</span>
752
+ <div>
753
+ <div style="font-weight:600;font-size:0.9rem;color:var(--text-primary);">AES-256 Encrypted Config</div>
754
+ <div style="font-size:0.8rem;color:var(--text-secondary);">All API keys encrypted at rest</div>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ </div>
759
+ <!-- Footer -->
760
+ <div class="footer">
761
+ 🐝 APIarium &mdash; Built for speed
762
+ </div>
763
+ </div>
764
+ <!-- Toast -->
765
+ <div class="toast" id="toast">βœ“ Copied to clipboard!</div>
766
+ <script>
767
+ const myKey = { key: null, ip: null };
768
+ function onTurnstileSuccess(token) {
769
+ fetchKey(token);
770
+ }
771
+ async function fetchKey(cfToken) {
772
+ try {
773
+ const url = cfToken ? '/v1/keys/me?cf_token=' + encodeURIComponent(cfToken) : '/v1/keys/me';
774
+ const resp = await fetch(url);
775
+ const data = await resp.json();
776
+ if (resp.ok) {
777
+ myKey.ip = data.ip;
778
+ myKey.key = data.key;
779
+ document.getElementById('ykIp').textContent = data.ip;
780
+ document.getElementById('ykKey').textContent = data.key;
781
+ document.getElementById('turnstile-container').style.display = 'none';
782
+ document.getElementById('keyBadge').textContent = data.status === 'new' ? 'New' : 'Active';
783
+ document.getElementById('keyBadge').style.background = 'rgba(255, 255, 255, 0.05)';
784
+ document.getElementById('keyBadge').style.borderColor = 'rgba(255, 255, 255, 0.1)';
785
+ if (data.status === 'new') {
786
+ document.getElementById('ykStatus').innerHTML = '<div class="key-status status-active">πŸŽ‰ New key generated!</div>';
787
+ } else {
788
+ document.getElementById('ykStatus').innerHTML = '<div class="key-status status-active">βœ… Active</div>';
789
+ }
790
+ document.getElementById('ykHint').textContent = 'Click key to copy Β· ' + data.rpm + ' RPM limit';
791
+ } else {
792
+ const err = data.error || {};
793
+ if (err.code === 'vpn_blocked') {
794
+ document.getElementById('ykIp').textContent = 'BLOCKED';
795
+ document.getElementById('ykKey').textContent = 'β€”';
796
+ document.getElementById('turnstile-container').style.display = 'none';
797
+ document.getElementById('keyBadge').textContent = 'Blocked';
798
+ document.getElementById('keyBadge').style.background = 'rgba(239, 68, 68, 0.1)';
799
+ document.getElementById('keyBadge').style.borderColor = 'rgba(255, 80, 80, 0.5)';
800
+ document.getElementById('ykStatus').innerHTML = '<div class="key-status status-blocked">🚫 VPN/Proxy detected</div>';
801
+ document.getElementById('ykHint').textContent = 'Turn off VPN and refresh';
802
+ } else {
803
+ document.getElementById('ykIp').textContent = 'Verification required';
804
+ document.getElementById('ykKey').textContent = 'β€”';
805
+ document.getElementById('turnstile-container').style.display = 'block';
806
+ document.getElementById('keyBadge').textContent = 'Verify';
807
+ document.getElementById('keyBadge').style.background = 'rgba(255, 255, 255, 0.05)';
808
+ document.getElementById('keyBadge').style.borderColor = 'rgba(255, 255, 255, 0.15)';
809
+ document.getElementById('ykStatus').innerHTML = '<div class="key-status status-verify">πŸ”’ Verify to get your key</div>';
810
+ }
811
+ }
812
+ } catch (e) {
813
+ document.getElementById('ykIp').textContent = 'Error';
814
+ document.getElementById('ykKey').textContent = 'Error loading';
815
+ }
816
+ }
817
+ function copyKey() {
818
+ if (!myKey.key) return;
819
+ navigator.clipboard.writeText(myKey.key).then(() => {
820
+ const toast = document.getElementById('toast');
821
+ toast.classList.add('show');
822
+ setTimeout(() => toast.classList.remove('show'), 2500);
823
+ });
824
+ }
825
+ fetch('/v1/models').then(r => r.json()).then(d => {
826
+ const models = d.data || [];
827
+ document.getElementById('modelCount').textContent = models.length;
828
+ document.getElementById('modelsBadge').textContent = models.length + ' models';
829
+ const grid = document.getElementById('modelsGrid');
830
+ if (models.length === 0) {
831
+ grid.innerHTML = '<div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">No models available</div>';
832
+ return;
833
+ }
834
+ grid.innerHTML = models.map(m => `
835
+ <div class="model-card" onclick="navigator.clipboard.writeText('${m.id}');showToast()">
836
+ <div class="model-name">${m.id}</div>
837
+ <div class="model-id">${m.owned_by || 'APIarium'}</div>
838
+ </div>
839
+ `).join('');
840
+ }).catch(() => {
841
+ document.getElementById('modelsBadge').textContent = 'Error';
842
+ document.getElementById('modelsGrid').innerHTML = '<div style="grid-column:1/-1;color:var(--text-muted);text-align:center;padding:24px;font-size:0.9rem;">Failed to load models</div>';
843
+ });
844
+ fetchKey();
845
+ </script>
846
+ </body>
847
+ </html>