Administrator
2026-08-14 57e5a8385de2d5ff2e43c72535b472e42d65ba91
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Station Dashboard — 策略监控中心</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
    <style>
        :root {
            --bg: #eaf2ff; --card: #ffffff; --border: #e3e8f0;
            --text: #1f2937; --dim: #6b7280; --accent: #3b82f6;
            --danger: #dc2626; --green: #16a34a; --warn: #d97706;
            --input-bg: #f9fafb; --sidebar-w: 280px;
            --purple: #7c3aed; --teal: #0d9488;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: var(--bg); color: var(--text); min-height: 100vh;
            display: flex; overflow: hidden;
        }
 
        /* ===== 侧边栏 ===== */
        .sidebar {
            width: var(--sidebar-w); min-width: var(--sidebar-w);
            background: var(--card); border-right: 1px solid var(--border);
            display: flex; flex-direction: column; height: 100vh;
        }
        .sidebar-header {
            padding: 16px 16px 12px; border-bottom: 1px solid var(--border);
            display: flex; justify-content: space-between; align-items: center;
        }
        .sidebar-header h1 {
            font-size: 16px; display: flex; align-items: center; gap: 8px;
        }
        .refresh-badge {
            font-size: 10px; color: var(--dim); border: 1px solid var(--border);
            border-radius: 10px; padding: 2px 8px;
        }
        .instance-list {
            flex: 1; overflow-y: auto; padding: 8px;
        }
        .instance-card {
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 6px; padding: 12px; margin-bottom: 6px;
            cursor: pointer; transition: all .15s;
        }
        .instance-card:hover { border-color: var(--accent); }
        .instance-card.selected { border-color: var(--accent); background: #eef4ff; }
        .instance-card .top-row {
            display: flex; justify-content: space-between; align-items: center;
        }
        .instance-card .alias-line {
            display: flex; align-items: center; gap: 4px; min-width: 0;
        }
        .instance-card .alias-text {
            font-size: 14px; font-weight: 600;
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        .instance-card .alias-text.placeholder { color: var(--dim); font-weight: 400; }
        .instance-card .alias-edit {
            background: none; border: none; cursor: pointer; color: var(--dim);
            font-size: 11px; line-height: 1; padding: 2px 4px; border-radius: 4px; flex-shrink: 0;
        }
        .instance-card .alias-edit:hover { color: var(--accent); background: rgba(59,130,246,0.12); }
        .instance-card .md5 {
            font-size: 11px; color: var(--dim); font-family: "SF Mono", "Consolas", monospace;
            margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        .instance-card .meta {
            display: flex; gap: 10px; margin-top: 6px; font-size: 11px; color: var(--dim);
        }
        .instance-card .pnl { font-weight: 600; }
        .instance-card .pnl.positive { color: var(--green); }
        .instance-card .pnl.negative { color: var(--danger); }
        .status-dot {
            width: 8px; height: 8px; border-radius: 50%; display: inline-block;
            flex-shrink: 0;
        }
        .status-dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); }
        .status-dot.stopped { background: var(--danger); }
        .status-dot.waiting { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
        .status-dot.offline { background: var(--dim); }
 
        .sidebar-footer {
            padding: 10px 16px; border-top: 1px solid var(--border);
            font-size: 11px; color: var(--dim); display: flex; justify-content: space-between;
        }
 
        /* ===== 主区域 ===== */
        .main {
            flex: 1; display: flex; flex-direction: column; height: 100vh;
            overflow: hidden;
        }
        .main-header {
            padding: 14px 24px; border-bottom: 1px solid var(--border);
            background: var(--card);
            display: flex; justify-content: space-between; align-items: center;
        }
        .main-header .instance-title {
            display: flex; align-items: center; gap: 10px;
        }
        .main-header h2 { font-size: 17px; }
        .main-header .header-alias {
            display: inline-block; max-width: 160px; vertical-align: middle;
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
            background: #eef4ff; color: var(--accent);
            border: 1px solid rgba(59,130,246,0.25);
            font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 10px;
            margin-right: 6px;
        }
        .main-header .state-badge {
            font-size: 11px; padding: 2px 10px; border-radius: 10px;
            font-weight: 600;
        }
        .state-badge.ACTIVE { background: #dcfce7; color: #15803d; }
        .state-badge.STOPPED { background: #fee2e2; color: #b91c1c; }
        .state-badge.WAITING_KLINE { background: #fef3c7; color: #b45309; }
        .state-badge.OPENING { background: #dbeafe; color: #1d4ed8; }
 
        .btn-group { display: flex; gap: 6px; }
        .btn {
            padding: 6px 14px; border-radius: 5px; border: none;
            font-size: 12px; cursor: pointer; font-weight: 500;
            transition: opacity .15s;
        }
        .btn:hover { opacity: .85; }
        .btn-success { background: var(--green); color: #fff; }
        .btn-danger { background: var(--danger); color: #fff; }
        .btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
        .btn:disabled { opacity: .4; cursor: not-allowed; }
 
        /* ===== 内容区(Tabs) ===== */
        .content { flex: 1; overflow-y: auto; padding: 16px 24px; }
        .tabs {
            display: flex; gap: 0; margin-bottom: 16px;
            border-bottom: 1px solid var(--border);
        }
        .tab {
            padding: 8px 20px; font-size: 13px; cursor: pointer;
            border-bottom: 2px solid transparent; color: var(--dim);
            transition: all .15s;
        }
        .tab:hover { color: var(--text); }
        .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
 
        .tab-content { display: none; }
        .tab-content.active { display: block; }
 
        /* ===== 统计卡片 ===== */
        .stats-grid {
            display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
            gap: 12px; margin-bottom: 20px;
        }
        .stat-card {
            background: var(--card); border: 1px solid var(--border);
            border-radius: 8px; padding: 14px 16px;
        }
        .stat-card .label { font-size: 11px; color: var(--dim); margin-bottom: 4px; }
        .stat-card .value { font-size: 22px; font-weight: 600; }
        .stat-card .sub { font-size: 11px; color: var(--dim); margin-top: 2px; }
 
        /* ===== 图表容器 ===== */
        .chart-container {
            background: var(--card); border: 1px solid var(--border);
            border-radius: 8px; padding: 16px; margin-bottom: 20px;
        }
        .chart-container h3 {
            font-size: 13px; color: var(--dim); margin-bottom: 12px;
        }
        .pnl-summary {
            display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 10px; margin-bottom: 16px;
        }
        .pnl-summary-item {
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 8px; padding: 10px 14px;
        }
        .pnl-summary-item .s-label { font-size: 11px; color: var(--dim); margin-bottom: 4px; }
        .pnl-summary-item .s-value { font-size: 17px; font-weight: 600; color: var(--text); }
        .pnl-summary-item .s-delta { font-size: 11px; margin-top: 3px; font-weight: 500; color: var(--dim); }
        .pnl-summary-item .s-value.positive { color: var(--green); }
        .pnl-summary-item .s-value.negative { color: var(--danger); }
        .pnl-summary-item .s-delta.positive { color: var(--green); }
        .pnl-summary-item .s-delta.negative { color: var(--danger); }
        .chart-wrap { position: relative; height: 300px; }
        .chart-wrap canvas { width: 100% !important; height: 100% !important; }
 
        /* ===== 事件日志 ===== */
        .events-panel {
            background: var(--card); border: 1px solid var(--border);
            border-radius: 8px; overflow: hidden;
        }
        .events-header {
            padding: 12px 16px; border-bottom: 1px solid var(--border);
            display: flex; justify-content: space-between; align-items: center;
        }
        .events-header h3 { font-size: 13px; color: var(--dim); }
        .events-table {
            width: 100%; font-size: 12px; border-collapse: collapse;
        }
        .events-table th {
            text-align: left; padding: 8px 16px; font-weight: 500;
            color: var(--dim); border-bottom: 1px solid var(--border);
            background: var(--bg);
        }
        .events-table td {
            padding: 7px 16px; border-bottom: 1px solid rgba(0,0,0,0.06);
            vertical-align: middle;
        }
        .events-table tr:hover { background: rgba(59,130,246,0.05); }
        .event-tag {
            font-size: 10px; padding: 1px 8px; border-radius: 8px;
            font-weight: 600; white-space: nowrap;
        }
        .event-tag.STRATEGY_START { background: #dcfce7; color: #15803d; }
        .event-tag.STRATEGY_STOP { background: #fee2e2; color: #b91c1c; }
        .event-tag.ROUND_COMPLETE { background: #dbeafe; color: #1d4ed8; }
        .event-tag.STOP_LOSS_TRIGGERED { background: #fef3c7; color: #b45309; }
        .event-tag.ENTRY_FILLED { background: #ede9fe; color: #6d28d9; }
        .event-tag.PNL_SNAPSHOT { background: #ccfbf1; color: #0f766e; }
        .event-tag.CMD_ACK { background: #e5e7eb; color: #4b5563; }
        .payload-preview {
            max-width: 260px; overflow: hidden; text-overflow: ellipsis;
            white-space: nowrap; color: var(--dim); font-family: "SF Mono", "Consolas", monospace;
        }
        .relative-time { color: var(--dim); white-space: nowrap; font-size: 11px; }
 
        /* ===== 配置面板 ===== */
        .config-status-bar {
            display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px;
        }
        .config-status-item {
            flex: 1; min-width: 120px;
            background: var(--card); border: 1px solid var(--border);
            border-radius: 8px; padding: 10px 14px;
        }
        .config-status-item .label { font-size: 11px; color: var(--dim); margin-bottom: 3px; }
        .config-status-item .value { font-size: 16px; font-weight: 600; }
        .config-status-item .value.positive { color: var(--green); }
        .config-status-item .value.negative { color: var(--danger); }
        .config-status-item .value.dim { color: var(--dim); }
 
        .config-section { margin-bottom: 18px; }
        .config-section-title {
            font-size: 13px; font-weight: 600; color: var(--text);
            margin-bottom: 10px; padding-bottom: 7px;
            border-bottom: 1px solid var(--border);
            display: flex; align-items: center; gap: 7px;
        }
        .config-section-title .count {
            font-size: 10px; font-weight: 400; color: var(--dim);
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 9px; padding: 0 7px; line-height: 16px;
        }
        .form-grid {
            display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px;
        }
        .form-group { display: flex; flex-direction: column; gap: 5px; }
        .form-group label { font-size: 11px; color: var(--dim); font-weight: 500; }
        .form-group input, .form-group select {
            background: var(--input-bg); border: 1px solid var(--border);
            border-radius: 6px; color: var(--text); padding: 8px 10px;
            font-size: 13px; outline: none; width: 100%;
            transition: border-color .15s;
        }
        .form-group input:focus, .form-group select:focus { border-color: var(--accent); }
        .form-group input::placeholder { color: #9ca3af; }
        .form-group .field-tip { font-size: 10px; color: #9ca3af; line-height: 1.3; }
        .form-group.modified label::after {
            content: ' • 已修改'; color: var(--warn); font-weight: 600;
        }
        .form-group.modified input, .form-group.modified select { border-color: var(--warn); }
        .config-actions {
            display: flex; justify-content: flex-end; gap: 8px;
            margin-bottom: 18px; padding-bottom: 14px;
            border-bottom: 1px solid var(--border);
        }
 
        /* ===== 空状态 ===== */
        .empty-state {
            text-align: center; padding: 60px 20px; color: var(--dim);
        }
        .empty-state .icon { font-size: 48px; margin-bottom: 12px; }
        .empty-state p { font-size: 14px; }
 
        /* ===== 确认弹窗 ===== */
        .modal-overlay {
            position: fixed; inset: 0; background: rgba(15,23,42,0.4);
            display: flex; align-items: center; justify-content: center; z-index: 1000;
            display: none;
        }
        .modal-overlay.show { display: flex; }
        .modal {
            background: var(--card); border: 1px solid var(--border);
            border-radius: 10px; padding: 24px; width: 380px;
        }
        .modal h3 { font-size: 16px; margin-bottom: 12px; }
        .modal p { font-size: 13px; color: var(--dim); margin-bottom: 20px; }
        .modal .modal-btns { display: flex; justify-content: flex-end; gap: 8px; }
        .modal input[type="text"] {
            width: 100%; padding: 8px 10px; box-sizing: border-box;
            border: 1px solid var(--border); border-radius: 6px;
            font-size: 13px; outline: none; color: var(--text); background: var(--input-bg);
            margin-bottom: 16px;
        }
        .modal input[type="text"]:focus { border-color: var(--accent); }
 
        /* Toast — 页面正中间,3s 后彻底隐藏 */
        .toast {
            position: fixed; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            padding: 12px 22px; border-radius: 8px; font-size: 14px;
            z-index: 2000; display: none;
            max-width: 80vw; text-align: center;
        }
        .toast.show { display: block; animation: toastIn .25s ease; }
        .toast.success { background: #dcfce7; border:1px solid #86efac; color:#15803d; }
        .toast.error { background: #fee2e2; border:1px solid #fca5a5; color:#b91c1c; }
        @keyframes toastIn {
            from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
            to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
        }
 
        /* Scrollbar */
        ::-webkit-scrollbar { width: 6px; }
        ::-webkit-scrollbar-track { background: transparent; }
        ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
 
        @media (max-width: 900px) {
            body { flex-direction: column; }
            .sidebar { width: 100%; min-width: 100%; height: auto; max-height: 40vh; }
            .main { height: auto; }
            .stats-grid { grid-template-columns: 1fr 1fr; }
            .form-grid { grid-template-columns: 1fr; }
        }
    </style>
</head>
<body>
 
<!-- ===== 侧边栏:实例列表 ===== -->
<div class="sidebar">
    <div class="sidebar-header">
        <h1>📡 Station</h1>
        <button class="btn btn-outline" onclick="refreshInstanceList()" style="font-size:11px;padding:3px 10px">🔄 刷新</button>
    </div>
    <div class="instance-list" id="instanceList">
        <div class="empty-state">
            <div class="icon">📭</div>
            <p>暂无在线实例</p>
            <p style="font-size:11px;margin-top:4px">等待 JAR 心跳注册...</p>
        </div>
    </div>
    <div class="sidebar-footer">
        <span id="connStatus">🟢 已连接</span>
        <span id="instanceCount">0 实例</span>
    </div>
</div>
 
<!-- ===== 主区域 ===== -->
<div class="main">
    <!-- 顶栏 -->
    <div class="main-header" id="mainHeader">
        <div class="instance-title">
            <h2 style="color:var(--dim)">← 选择一个实例</h2>
        </div>
        <div class="btn-group">
            <button class="btn btn-success" id="btnStart" disabled onclick="showConfirm('start')">▶ 启动</button>
            <button class="btn btn-danger" id="btnStop" disabled onclick="showConfirm('stop')">⏹ 停止</button>
        </div>
    </div>
 
    <!-- 内容区 -->
    <div class="content" id="mainContent">
        <div class="empty-state">
            <div class="icon">👈</div>
            <p>从左侧列表选择一个实例查看详情</p>
        </div>
    </div>
</div>
 
<!-- 详情模板 — 原生 <template>:内容惰性不参与文档流,克隆后无重复 ID -->
<template id="detailTemplate">
    <!-- Tabs -->
    <div class="tabs">
        <div class="tab active" data-tab="overview">概览</div>
        <div class="tab" data-tab="events">事件日志</div>
        <div class="tab" data-tab="config">策略参数</div>
    </div>
 
    <!-- 概览 Tab -->
    <div class="tab-content active" data-tab="overview">
        <div class="stats-grid" id="statsGrid"></div>
        <div class="chart-container">
            <h3>📈 盈亏趋势(最近 PNL_SNAPSHOT 事件)</h3>
            <div class="pnl-summary" id="pnlSummary"></div>
            <div class="chart-wrap"><canvas id="pnlChart"></canvas></div>
        </div>
    </div>
 
    <!-- 事件日志 Tab -->
    <div class="tab-content" data-tab="events">
        <div class="events-panel">
            <div class="events-header">
                <h3>📋 最近 100 条事件</h3>
                <span style="font-size:11px;color:var(--dim)" id="eventCount">—</span>
            </div>
            <div style="max-height:500px;overflow-y:auto">
                <table class="events-table">
                    <thead>
                        <tr>
                            <th style="width:120px">时间</th>
                            <th style="width:140px">类型</th>
                            <th>Payload</th>
                        </tr>
                    </thead>
                    <tbody id="eventsBody"></tbody>
                </table>
            </div>
        </div>
    </div>
 
    <!-- 策略参数 Tab(可编辑) -->
    <div class="tab-content" data-tab="config">
        <!-- 只读状态条 -->
        <div class="config-status-bar" id="configStatusBar"></div>
 
        <div class="chart-container">
            <div class="config-actions">
                <span style="flex:1;font-size:12px;color:var(--dim);align-self:center">💡 修改后需手动点击顶部「▶ 启动」使新配置生效</span>
                <button class="btn btn-outline" id="btnResetConfig" onclick="resetConfig()">↺ 重置</button>
                <button class="btn btn-success" id="btnSaveConfig" onclick="saveConfig()">💾 保存</button>
            </div>
            <div id="configForm"></div>
        </div>
    </div>
</template>
 
<!-- 确认弹窗 -->
<div class="modal-overlay" id="modalOverlay">
    <div class="modal">
        <h3 id="modalTitle">确认操作</h3>
        <p id="modalMsg"></p>
        <div class="modal-btns">
            <button class="btn btn-outline" onclick="closeModal()">取消</button>
            <button class="btn" id="modalConfirmBtn" onclick="confirmAction()">确认</button>
        </div>
    </div>
</div>
 
<!-- 别名编辑弹窗 -->
<div class="modal-overlay" id="aliasModalOverlay">
    <div class="modal">
        <h3>✎ 编辑别名</h3>
        <p>为实例设置便于识别的显示名(留空保存可清除别名)</p>
        <input id="aliasInput" type="text" maxlength="64" placeholder="例如:ETH_USDT"
               onkeydown="if(event.key==='Enter'){saveAlias();} else if(event.key==='Escape'){closeAliasModal();}">
        <div class="modal-btns">
            <button class="btn btn-outline" onclick="closeAliasModal()">取消</button>
            <button class="btn btn-success" onclick="saveAlias()">保存</button>
        </div>
    </div>
</div>
 
<div id="toast"></div>
 
<script>
const API = '/api/gate/station';
let selectedMd5 = null;
let instances = {};
let eventsCache = {};
let pnlChart = null;
let pendingAction = null;
let pnlData = [];
 
// ==================== 工具 ====================
function $(id) { return document.getElementById(id); }
 
/** HTML 转义 — 所有来自服务端/JAR 的动态字符串插值前必须使用,防止属性逃逸注入 */
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => (
    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
));
function toast(msg, type) {
    const t = $('toast');
    t.textContent = msg;
    t.className = 'toast ' + type + ' show';
    // 清除上一次未消失的定时器,避免重复点击时闪烁
    clearTimeout(t._toastTimer);
    t._toastTimer = setTimeout(() => {
        t.className = 'toast'; // 移除 show → display:none,彻底隐藏不留空白框
    }, 3000);
}
function fmtNum(v) {
    if (v == null || v === '') return '—';
    const n = parseFloat(v);
    if (isNaN(n)) return v;
    return n.toFixed(n < 1 ? 6 : 2);
}
function fmtPnl(v) {
    if (v == null || v === '') return { text: '—', cls: '' };
    const n = parseFloat(v);
    if (isNaN(n)) return { text: v, cls: '' };
    return {
        text: (n >= 0 ? '+' : '') + n.toFixed(4),
        cls: n > 0 ? 'positive' : n < 0 ? 'negative' : ''
    };
}
function relativeTime(ts) {
    if (!ts) return '—';
    const diff = Date.now() - ts;
    if (diff < 5000) return '刚刚';
    if (diff < 60000) return Math.floor(diff / 1000) + 's 前';
    if (diff < 3600000) return Math.floor(diff / 60000) + 'm 前';
    if (diff < 86400000) return Math.floor(diff / 3600000) + 'h 前';
    return new Date(ts).toLocaleDateString();
}
function formatTime(ts) {
    if (!ts) return '—';
    const d = new Date(ts);
    return d.toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0');
}
 
// ==================== API 请求 ====================
const API_TIMEOUT_MS = 10000;
 
async function api(url, opts) {
    // 超时保护:10s 未响应则中断请求,避免页面无限等待
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), API_TIMEOUT_MS);
    try {
        const res = await fetch(url, Object.assign({}, opts, { signal: ctrl.signal }));
        // 未登录/无权限:引导回登录页
        if (res.status === 401 || res.status === 403) {
            window.location.href = '/login';
            throw new Error('未登录或会话已过期');
        }
        // 非 2xx 或非 JSON 响应(如安全框架拦截返回的 HTML):给出明确错误
        const ct = res.headers.get('content-type') || '';
        if (!res.ok || ct.indexOf('application/json') === -1) {
            throw new Error('服务器异常 (' + res.status + ')');
        }
        const data = await res.json();
        if (data.code !== 0 && data.code !== 200) throw new Error(data.msg || '请求失败');
        return data.data !== undefined ? data.data : data.msg;
    } catch (e) {
        if (e.name === 'AbortError') {
            $('connStatus').innerHTML = '🔴 离线';
            throw new Error('请求超时,请检查网络');
        }
        if (e.message.includes('Failed to fetch') || e.message.includes('NetworkError')) {
            $('connStatus').innerHTML = '🔴 离线';
        }
        throw e;
    } finally {
        clearTimeout(timer);
    }
}
 
// ==================== 实例列表 ====================
async function refreshInstanceList() {
    try {
        const list = await api(API + '/list');
        $('connStatus').innerHTML = '🟢 已连接';
        const newInstances = {};
        for (const inst of (list || [])) {
            newInstances[inst.apiKeyMd5] = inst;
        }
        instances = newInstances;
        renderInstanceList();
        // 实例数据已更新,同步刷新选中实例的概览统计、参数页状态条与操作按钮
        if (selectedMd5 && instances[selectedMd5]) {
            renderStats(instances[selectedMd5]);
            renderConfigStatusBar();
            updateActionButtons(instances[selectedMd5]);
        } else if (selectedMd5) {
            // 选中实例已离线:禁用操作按钮,详情区保留最后快照
            updateActionButtons(null);
        }
    } catch (e) {
        // 静默失败,保留旧数据
    }
}
 
// 手动刷新事件日志(对应「事件日志」Tab 的刷新按钮)
function refreshEvents() {
    if (!selectedMd5) return;
    loadEvents(selectedMd5);
}
 
function renderInstanceList() {
    const container = $('instanceList');
    const keys = Object.keys(instances);
 
    if (keys.length === 0) {
        container.innerHTML = `
            <div class="empty-state">
                <div class="icon">📭</div>
                <p>暂无在线实例</p>
                <p style="font-size:11px;margin-top:4px">等待 JAR 心跳注册...</p>
            </div>`;
    } else {
        container.innerHTML = keys.map(k => {
            const i = instances[k];
            const isActive = i.state === 'ACTIVE' || i.state === 'OPENING';
            const dotClass = i.state === 'STOPPED' ? 'stopped'
                : isActive ? 'active'
                : i.state === 'WAITING_KLINE' ? 'waiting' : 'offline';
            const pnl = fmtPnl(i.cumulativePnl);
            const selected = k === selectedMd5 ? ' selected' : '';
            // 第一行显示别名;未设置时回退显示合约名(占位样式)
            const hasAlias = !!i.aliasName;
            const aliasText = hasAlias ? i.aliasName : (i.contract || '未命名实例');
            return `
            <div class="instance-card${selected}" onclick="selectInstance('${esc(k)}')" data-md5="${esc(k)}">
                <div class="top-row">
                    <span class="alias-line">
                        <span class="alias-text${hasAlias ? '' : ' placeholder'}" title="${esc(aliasText)}">${esc(aliasText)}</span>
                        <button class="alias-edit" title="编辑别名" onclick="event.stopPropagation(); showAliasModal('${esc(k)}')">✎</button>
                    </span>
                    <span class="status-dot ${dotClass}" title="${esc(i.state || 'UNKNOWN')}"></span>
                </div>
                <div class="md5">${esc(k.substring(0, 16))}...</div>
                <div class="meta">
                    <span>轮次 ${i.currentRound ?? 0}</span>
                    <span class="pnl ${pnl.cls}">${esc(pnl.text)}</span>
                    <span>${relativeTime(i.lastSeen)}</span>
                </div>
            </div>`;
        }).join('');
    }
    $('instanceCount').textContent = keys.length + ' 实例';
}
 
function selectInstance(md5) {
    selectedMd5 = md5;
    eventsCache[md5] = null; // 强制刷新
    pnlData = [];
    renderInstanceList();
    loadInstanceDetail(md5, true);
}
 
async function loadInstanceDetail(md5, full) {
    const inst = instances[md5];
    if (!inst) return;
 
    // 显示详情模板,隐藏空状态
    const content = $('mainContent');
    const tmpl = $('detailTemplate');
    if (!content.querySelector('.tabs')) {
        // 先清空空状态占位,再克隆 <template> 内容(惰性片段,克隆后无重复 ID)
        content.innerHTML = '';
        content.appendChild(tmpl.content.cloneNode(true));
        // 绑定 tab 切换
        content.querySelectorAll('.tab').forEach(t => {
            t.addEventListener('click', () => switchTab(t.dataset.tab));
        });
    }
 
    // 更新 header(含别名,未设置别名时不显示)
    renderHeader(inst);
 
    // 更新概览统计
    renderStats(inst);
 
    // 加载事件(内部会渲染事件表格 + PNL 图表)
    await loadEvents(md5);
 
    // 事件加载完成后重算统计卡:账户总权益依赖最新 PNL_SNAPSHOT
    if (selectedMd5 === md5 && instances[md5]) {
        renderStats(instances[md5]);
    }
 
    // 策略参数表单:仅全量加载(切换实例)时渲染,避免轮询清空用户正在编辑的内容
    if (full) {
        loadConfig(md5);
    }
}
 
function renderHeader(inst) {
    const header = $('mainHeader');
    const md5 = inst.apiKeyMd5;
    // 别名徽标:仅当设置了别名时显示
    const aliasHtml = inst.aliasName
        ? `<span class="header-alias" title="${esc(inst.aliasName)}">${esc(inst.aliasName)}</span>`
        : '';
    header.querySelector('.instance-title').innerHTML = `
        <h2>${aliasHtml}${esc(inst.contract || '—')} <span style="font-size:12px;color:var(--dim);font-weight:400">${esc(md5.substring(0, 12))}...</span></h2>
        <span class="state-badge ${sanitizeState(inst.state)}">${esc(inst.state || 'UNKNOWN')}</span>
        <button class="btn btn-outline" onclick="refreshEvents()" style="font-size:11px;padding:3px 10px">🔄 刷新</button>
    `;
    updateActionButtons(inst);
}
 
function sanitizeState(s) {
    if (!s) return '';
    const valid = ['ACTIVE', 'STOPPED', 'WAITING_KLINE', 'OPENING'];
    return valid.includes(s) ? s : '';
}
 
function switchTab(name) {
    // 查询限定在 mainContent 内,避免误伤模板或其他区域
    const main = $('mainContent');
    main.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
    main.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
    main.querySelector(`.tab[data-tab="${name}"]`).classList.add('active');
    main.querySelector(`.tab-content[data-tab="${name}"]`).classList.add('active');
    // 概览 Tab 变为可见时校正图表尺寸(图表可能在隐藏容器中创建过)
    if (name === 'overview' && pnlChart) pnlChart.resize();
}
 
// ==================== 统计卡片 ====================
function renderStats(inst) {
    const pnl = fmtPnl(inst.cumulativePnl);
    const principal = inst.principal ? parseFloat(inst.principal) : 0;
    const cumpnl = inst.cumulativePnl ? parseFloat(inst.cumulativePnl) : 0;
    // 账户总权益:优先用最新 PNL_SNAPSHOT 的真实值(账户余额 + 浮动盈亏),无快照数据时回退估算
    let totalEquity = principal + cumpnl;
    let equityFromSnapshot = false;
    if (pnlData.length > 0) {
        const latest = pnlData[pnlData.length - 1];
        if (isFinite(latest.totalEquity) && latest.totalEquity > 0 && isFinite(latest.unrealizedPnl)) {
            totalEquity = latest.totalEquity + latest.unrealizedPnl;
            equityFromSnapshot = true;
        }
    }
    const roi = principal > 0 ? ((cumpnl / principal) * 100) : 0;
 
    $('statsGrid').innerHTML = `
        <div class="stat-card">
            <div class="label">累计已实现盈亏</div>
            <div class="value ${pnl.cls}">${esc(pnl.text)}</div>
            <div class="sub">USDT</div>
        </div>
        <div class="stat-card">
            <div class="label">初始本金</div>
            <div class="value">${esc(fmtNum(inst.principal))}</div>
            <div class="sub">USDT</div>
        </div>
        <div class="stat-card">
            <div class="label">账户总权益</div>
            <div class="value">${fmtNum(totalEquity)}</div>
            <div class="sub">${equityFromSnapshot ? '含浮动盈亏(最新快照)' : '本金 + 已实现盈亏(暂无快照)'}</div>
        </div>
        <div class="stat-card">
            <div class="label">收益率 (ROI)</div>
            <div class="value ${roi >= 0 ? 'positive' : 'negative'}" style="color:${roi >= 0 ? 'var(--green)' : 'var(--danger)'}">${roi >= 0 ? '+' : ''}${roi.toFixed(4)}%</div>
            <div class="sub">累计</div>
        </div>
        <div class="stat-card">
            <div class="label">当前轮次</div>
            <div class="value">${inst.currentRound ?? 0}</div>
            <div class="sub">杠杆 ${esc(inst.leverage || '—')}x</div>
        </div>
        <div class="stat-card">
            <div class="label">最后心跳</div>
            <div class="value" style="font-size:16px">${relativeTime(inst.lastSeen)}</div>
            <div class="sub">${esc(inst.hostPort || '—')}</div>
        </div>
    `;
}
 
// ==================== 事件日志 ====================
async function loadEvents(md5) {
    try {
        const events = await api(API + '/events?apiKeyMd5=' + encodeURIComponent(md5));
        eventsCache[md5] = events || [];
        renderEvents(events || []);
    } catch (e) {
        eventsCache[md5] = [];
        renderEvents([]);
    }
}
 
function renderEvents(events) {
    const tbody = $('eventsBody');
    $('eventCount').textContent = events.length + ' 条';
 
    if (events.length === 0) {
        tbody.innerHTML = `<tr><td colspan="3" style="text-align:center;padding:30px;color:var(--dim)">暂无事件记录</td></tr>`;
        pnlData = [];
        renderPnlChart();
        return;
    }
 
    // 收集 PNL_SNAPSHOT 数据用于图表
    pnlData = events
        .filter(e => e.eventType === 'PNL_SNAPSHOT')
        .map(e => {
            let payload = {};
            try { payload = JSON.parse(e.payloadJson || '{}'); } catch (_) {}
            return {
                time: e.eventTime || e.createTime,
                cumulativePnl: parseFloat(payload.cumulativePnl || 0),
                unrealizedPnl: parseFloat(payload.unrealizedPnl || 0),
                totalEquity: parseFloat(payload.totalEquity || 0)
            };
        })
        .slice(0, 50).reverse();  // 后端返回 DESC(最新在前),取最新 50 条并转时间正序
 
    tbody.innerHTML = events.slice(0, 100).map(e => {
        let payloadStr = e.payloadJson || '';
        // 截断显示
        let shortPayload = payloadStr;
        try {
            const obj = JSON.parse(payloadStr);
            const keys = Object.keys(obj);
            shortPayload = keys.map(k => `${k}: ${obj[k]}`).join(', ');
        } catch (_) {}
        if (shortPayload.length > 80) shortPayload = shortPayload.substring(0, 80) + '...';
 
        const ts = e.eventTime || (e.createTime ? new Date(e.createTime).getTime() : null);
        return `
        <tr>
            <td class="relative-time" title="${esc(new Date(ts).toLocaleString())}">${formatTime(ts)}</td>
            <td><span class="event-tag ${esc(e.eventType || '')}">${esc(e.eventType || '—')}</span></td>
            <td class="payload-preview" title="${esc(payloadStr)}">${esc(shortPayload || '—')}</td>
        </tr>`;
    }).join('');
 
    renderPnlChart();
}
 
// ==================== PNL 图表 ====================
function renderPnlChart() {
    const canvas = document.getElementById('pnlChart');
    if (!canvas) return;
 
    // 顶部摘要指标(当前值 + 较上个快照涨跌)
    renderPnlSummary();
 
    if (pnlChart) pnlChart.destroy();
 
    if (pnlData.length === 0) {
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#6b7280';
        ctx.font = '14px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('暂无 PNL 数据(等待 PNL_SNAPSHOT 事件上报)', canvas.width / 2, canvas.height / 2);
        return;
    }
 
    const labels = pnlData.map(d => formatTime(d.time));
    const cumulative = pnlData.map(d => d.cumulativePnl);
    const unrealized = pnlData.map(d => d.unrealizedPnl);
    const total = pnlData.map(d => d.totalEquity);
 
    // 面积渐变填充:顶部半透明 → 底部透明
    function gradient(rgb, topAlpha) {
        return (context) => {
            const { ctx, chartArea } = context.chart;
            if (!chartArea) return `rgba(${rgb},${topAlpha})`;
            const g = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
            g.addColorStop(0, `rgba(${rgb},${topAlpha})`);
            g.addColorStop(1, `rgba(${rgb},0)`);
            return g;
        };
    }
 
    pnlChart = new Chart(canvas, {
        type: 'line',
        data: {
            labels,
            datasets: [
                {
                    label: '已实现盈亏',
                    data: cumulative,
                    borderColor: '#16a34a',
                    backgroundColor: gradient('22,163,74', 0.22),
                    fill: true,
                    tension: 0.3,
                    pointRadius: 2,
                    pointBackgroundColor: '#16a34a',
                    pointBorderColor: '#ffffff',
                    pointBorderWidth: 1,
                    borderWidth: 1.8
                },
                {
                    label: '未实现盈亏',
                    data: unrealized,
                    borderColor: '#d97706',
                    backgroundColor: gradient('217,119,6', 0.16),
                    fill: true,
                    tension: 0.3,
                    pointRadius: 0,
                    borderWidth: 1.5,
                    borderDash: [4, 3]
                },
                {
                    label: '总权益',
                    data: total,
                    borderColor: '#3b82f6',
                    backgroundColor: gradient('59,130,246', 0.10),
                    fill: true,
                    tension: 0.3,
                    pointRadius: 0,
                    borderWidth: 2,
                    yAxisID: 'y1'
                }
            ]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            interaction: { intersect: false, mode: 'index' },
            plugins: {
                legend: {
                    labels: { color: '#6b7280', font: { size: 11 }, usePointStyle: true, padding: 15, boxWidth: 8 }
                },
                tooltip: {
                    backgroundColor: '#ffffff',
                    borderColor: '#e3e8f0',
                    borderWidth: 1,
                    titleColor: '#1f2937',
                    bodyColor: '#1f2937',
                    titleFont: { size: 12 },
                    bodyFont: { size: 12 },
                    padding: 10,
                    callbacks: {
                        label: (ctx) => {
                            const isEquity = ctx.dataset.label === '总权益';
                            const v = ctx.parsed.y;
                            const txt = isEquity
                                ? v.toFixed(2)
                                : (v >= 0 ? '+' : '') + v.toFixed(4);
                            return ctx.dataset.label + ': ' + txt;
                        }
                    }
                }
            },
            scales: {
                x: {
                    ticks: { color: '#6b7280', font: { size: 9 }, maxTicksLimit: 10, maxRotation: 0 },
                    grid: { color: 'rgba(0,0,0,0.06)' }
                },
                y: {
                    type: 'linear',
                    display: true,
                    position: 'left',
                    ticks: { color: '#6b7280', font: { size: 10 }, callback: v => v.toFixed(4) },
                    grid: { color: 'rgba(0,0,0,0.06)' }
                },
                y1: {
                    type: 'linear',
                    display: true,
                    position: 'right',
                    ticks: { color: '#3b82f6', font: { size: 10 }, callback: v => v.toFixed(2) },
                    grid: { drawOnChartArea: false }
                }
            }
        }
    });
}
 
// 盈亏趋势顶部摘要:当前值 + 较上个快照涨跌
function renderPnlSummary() {
    const box = document.getElementById('pnlSummary');
    if (!box) return;
    if (pnlData.length === 0) {
        box.innerHTML = '';
        return;
    }
    const cur = pnlData[pnlData.length - 1];
    const prev = pnlData.length > 1 ? pnlData[pnlData.length - 2] : null;
 
    const items = [
        { label: '已实现盈亏', key: 'cumulativePnl', signed: true, digits: 4, fmt: v => (v >= 0 ? '+' : '') + v.toFixed(4) },
        { label: '未实现盈亏', key: 'unrealizedPnl', signed: true, digits: 4, fmt: v => (v >= 0 ? '+' : '') + v.toFixed(4) },
        { label: '总权益', key: 'totalEquity', signed: false, digits: 2, fmt: v => v.toFixed(2) }
    ];
 
    box.innerHTML = items.map(it => {
        const v = cur[it.key];
        const cls = (it.signed && v !== 0) ? (v > 0 ? 'positive' : 'negative') : '';
        let deltaHtml = '<div class="s-delta">—</div>';
        if (prev != null) {
            const d = v - prev[it.key];
            if (d !== 0) {
                const dCls = d > 0 ? 'positive' : 'negative';
                deltaHtml = `<div class="s-delta ${dCls}">${d > 0 ? '▲' : '▼'} ${Math.abs(d).toFixed(it.digits)}</div>`;
            } else {
                deltaHtml = '<div class="s-delta">持平</div>';
            }
        }
        return `<div class="pnl-summary-item">
            <div class="s-label">${it.label}</div>
            <div class="s-value ${cls}">${it.fmt(v)}</div>
            ${deltaHtml}
        </div>`;
    }).join('');
}
 
// ==================== 策略参数 ====================
// 从事件流提取最近一次 STRATEGY_START 的完整参数快照(toParamsMap 含全部 18 个字段)
function extractConfigFromEvents(events) {
    for (let i = events.length - 1; i >= 0; i--) {
        if (events[i].eventType === 'STRATEGY_START') {
            try {
                return JSON.parse(events[i].payloadJson || '{}');
            } catch (_) {
                return null;
            }
        }
    }
    return null;
}
 
// 默认参数(与后端 GateConfigDTO.defaultsFor() 保持一致)
const DEFAULT_CONFIG = {
    gridRate: 0.005,
    expectedProfit: 0.15,
    maxLoss: 1.5,
    baseQuantity: '2',
    quantity: '2',
    maxPositionSize: 4,
    stopLossCount: 0,
    takeProfitGridSpan: 2,
    rounds: 0,
    stopLossCountMode: 'dual',
    addPositionInterval: 3,
    addPositionQuantity: 1,
    maxPositionPerSide: 0,
    addPositionStartThreshold: 1,
    placeExcessTakeProfit: false,
    priceDriveEnabled: true,
};
 
async function loadConfig(md5) {
    // 优先:DB strategy_status(保存的配置已持久化,STRATEGY_START 也会写入这里,始终是最新值)
    try {
        const status = await api(API + '/strategy-status?apiKeyMd5=' + encodeURIComponent(md5));
        if (status) {
            // DB 字段 totalRounds 映射为前端 key rounds
            if (status.totalRounds != null && status.rounds == null) status.rounds = status.totalRounds;
            renderConfig(status);
            return;
        }
    } catch (e) {
        // 请求失败 → 回退事件流 / 默认值
    }
    // 回退:最近 STRATEGY_START 事件 payload(字段最全,含 rounds/expectedProfit/maxLoss 等)
    const config = extractConfigFromEvents(eventsCache[md5] || []);
    if (config) {
        renderConfig(config);
        return;
    }
    // 兜底:默认值填充,方便用户预填后启动
    renderConfig(DEFAULT_CONFIG);
}
 
// 可编辑的策略参数字段(jtype 标记 Java 类型用于正确序列化,group 用于分组渲染)
const EDITABLE_FIELDS = [
    { key: 'gridRate', label: '网格间距比例', type: 'number', step: '0.0001', placeholder: '0.005', jtype: 'decimal', group: 'grid', tip: '短基价 × 该比例 = 绝对步长' },
    { key: 'baseQuantity', label: '基底开仓张数', type: 'number', placeholder: '2', jtype: 'string', group: 'grid' },
    { key: 'quantity', label: '每次下单张数', type: 'number', placeholder: '2', jtype: 'string', group: 'grid' },
    { key: 'maxPositionSize', label: '最大持仓张数', type: 'number', placeholder: '4', jtype: 'int', group: 'grid' },
    { key: 'takeProfitGridSpan', label: '止盈网格跨度', type: 'number', placeholder: '2', jtype: 'int', group: 'grid' },
 
    { key: 'expectedProfit', label: '预期收益 (USDT)', type: 'number', step: '0.01', placeholder: '0.15', jtype: 'decimal', group: 'risk' },
    { key: 'maxLoss', label: '最大亏损 (USDT)', type: 'number', step: '0.01', placeholder: '1.5', jtype: 'decimal', group: 'risk' },
    { key: 'stopLossCount', label: '止损阶梯次数', type: 'number', placeholder: '0', jtype: 'int', group: 'risk', tip: '0 = 禁用阶梯止损' },
 
    { key: 'stopLossCountMode', label: '止损统计方式', type: 'select', options: [{ v: 'dual', l: '双向统一统计' }, { v: 'single', l: '单向分别统计' }], group: 'addon' },
    { key: 'addPositionInterval', label: '加仓间隔 (次)', type: 'number', placeholder: '3', jtype: 'int', group: 'addon', tip: '每隔 N 次止损触发一次加仓' },
    { key: 'addPositionQuantity', label: '加仓数量 (张)', type: 'number', placeholder: '1', jtype: 'int', group: 'addon' },
    { key: 'maxPositionPerSide', label: '单边最大仓位 (0=不限)', type: 'number', placeholder: '0', jtype: 'int', group: 'addon' },
    { key: 'addPositionStartThreshold', label: '加仓启动阈值 (次)', type: 'number', placeholder: '1', jtype: 'int', group: 'addon', tip: '前 N 次止损不触发加仓' },
 
    { key: 'rounds', label: '运行轮数 (0=不限)', type: 'number', placeholder: '0', jtype: 'int', group: 'run', tip: '盈利重启达此轮数后停止' },
    { key: 'placeExcessTakeProfit', label: '超额止盈', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'run' },
    { key: 'priceDriveEnabled', label: '价格驱动', type: 'select', options: [{ v: 'true', l: '开启' }, { v: 'false', l: '关闭' }], group: 'run' },
];
 
// 分组定义(渲染顺序)
const CONFIG_GROUPS = [
    { id: 'grid', name: '📐 网格参数' },
    { id: 'risk', name: '🛡️ 风控参数' },
    { id: 'addon', name: '📈 加仓 & 止损' },
    { id: 'run', name: '⚡ 运行控制' },
];
 
// 字段是否处于"已修改"状态(用于高亮)
const modifiedFields = new Set();
 
function fieldHtml(f) {
    const inputId = 'cfg_' + f.key;
    let inner;
    if (f.type === 'select') {
        inner = `<select id="${inputId}" onchange="markModified('${f.key}')">${f.options.map(o => `<option value="${o.v}">${o.l}</option>`).join('')}</select>`;
    } else {
        inner = `<input id="${inputId}" type="${f.type}" placeholder="${f.placeholder || ''}" ${f.step ? 'step="' + f.step + '"' : ''} oninput="markModified('${f.key}')">`;
    }
    const tip = f.tip ? `<div class="field-tip">${f.tip}</div>` : '';
    return `<div class="form-group" id="group_${f.key}"><label>${f.label}</label>${inner}${tip}</div>`;
}
 
function renderConfig(status) {
    const form = $('configForm');
    if (!status) {
        form.innerHTML = `<div style="padding:20px;color:var(--dim);text-align:center">暂无策略参数</div>`;
        return;
    }
    // 1. 只读状态条
    renderConfigStatusBar();
 
    // 2. 分组渲染表单
    let html = '';
    for (const g of CONFIG_GROUPS) {
        const fields = EDITABLE_FIELDS.filter(f => f.group === g.id);
        if (fields.length === 0) continue;
        html += `<div class="config-section">
            <div class="config-section-title">${g.name}<span class="count">${fields.length} 项</span></div>
            <div class="form-grid">${fields.map(fieldHtml).join('')}</div>
        </div>`;
    }
    form.innerHTML = html;
 
    // 3. 填充当前值
    for (const f of EDITABLE_FIELDS) {
        const el = document.getElementById('cfg_' + f.key);
        if (!el) continue;
        let val = status[f.key];
        if (val === true) val = 'true';
        else if (val === false) val = 'false';
        else if (val == null || val === '') val = '';
        else val = String(val);
        el.value = val;
    }
    modifiedFields.clear();
}
 
function renderConfigStatusBar() {
    const bar = $('configStatusBar');
    const inst = instances[selectedMd5];
    if (!inst) {
        bar.innerHTML = '';
        return;
    }
    const pnl = fmtPnl(inst.cumulativePnl);
    const items = [
        { label: '合约', value: esc(inst.contract || '—'), cls: '' },
        { label: '状态', value: esc(inst.state || '—'), cls: 'dim' },
        { label: '杠杆', value: esc((inst.leverage || '—') + 'x'), cls: '' },
        { label: '当前轮次', value: inst.currentRound ?? 0, cls: '' },
        { label: '累计盈亏', value: esc(pnl.text), cls: pnl.cls },
        { label: '初始本金', value: esc(fmtNum(inst.principal)), cls: '' },
    ];
    bar.innerHTML = items.map(i =>
        `<div class="config-status-item"><div class="label">${i.label}</div><div class="value ${i.cls}">${i.value}</div></div>`
    ).join('');
}
 
function markModified(key) {
    modifiedFields.add(key);
    const g = document.getElementById('group_' + key);
    if (g) g.classList.add('modified');
}
 
function resetConfig() {
    if (!selectedMd5) return;
    // 重新从当前数据源加载,覆盖用户未保存的编辑
    loadConfig(selectedMd5);
    toast('已重置为当前生效参数', 'success');
}
 
async function saveConfig() {
    if (!selectedMd5) return;
    const params = {};
    for (const f of EDITABLE_FIELDS) {
        const el = document.getElementById('cfg_' + f.key);
        if (!el) continue;
        const raw = el.value;
        // 跳过空值:缺失字段在 DTO 中为 null,buildFromDTO 会用 nvl() 填充默认值
        if (raw === '' || raw == null) continue;
 
        if (f.jtype === 'string') {
            params[f.key] = raw;
        } else if (f.jtype === 'int') {
            params[f.key] = parseInt(raw, 10);
        } else if (f.jtype === 'decimal') {
            params[f.key] = parseFloat(raw);
        } else {
            // select 等保持不变
            params[f.key] = raw;
        }
    }
    try {
        const msg = await api(API + '/config?apiKeyMd5=' + encodeURIComponent(selectedMd5), {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params)
        });
        toast(msg || '参数已保存,点击「启动」使新配置生效', 'success');
        // 保存成功后清除修改高亮
        modifiedFields.clear();
        document.querySelectorAll('.form-group.modified').forEach(g => g.classList.remove('modified'));
    } catch (e) {
        toast(e.message, 'error');
    }
}
 
// ==================== 启停操作 ====================
 
// 按实例状态联动启用/禁用 启动/停止 按钮,防止误触(JAR 的 START 会平仓重建策略)
function updateActionButtons(inst) {
    const startBtn = $('btnStart');
    const stopBtn = $('btnStop');
    if (!inst) {
        startBtn.disabled = true;
        stopBtn.disabled = true;
        return;
    }
    const running = inst.state === 'ACTIVE' || inst.state === 'OPENING' || inst.state === 'WAITING_KLINE';
    startBtn.disabled = running;   // 运行中不可重复启动
    stopBtn.disabled = !running;   // 未运行不可停止
}
 
function showConfirm(action) {
    if (!selectedMd5) return;
    pendingAction = action;
    const verb = action === 'start' ? '启动' : '停止';
    $('modalTitle').textContent = `确认${verb}`;
    $('modalMsg').textContent = `确定要${verb}实例 ${selectedMd5.substring(0, 12)}... 的策略吗?`;
    $('modalConfirmBtn').textContent = verb;
    $('modalConfirmBtn').className = 'btn ' + (action === 'start' ? 'btn-success' : 'btn-danger');
    $('modalOverlay').classList.add('show');
}
 
function closeModal() {
    $('modalOverlay').classList.remove('show');
    pendingAction = null;
}
 
// ==================== 别名编辑 ====================
let aliasEditingMd5 = null;
 
function showAliasModal(md5) {
    aliasEditingMd5 = md5;
    const inst = instances[md5];
    $('aliasInput').value = inst && inst.aliasName ? inst.aliasName : '';
    $('aliasModalOverlay').classList.add('show');
    $('aliasInput').focus();
}
 
function closeAliasModal() {
    $('aliasModalOverlay').classList.remove('show');
    aliasEditingMd5 = null;
}
 
async function saveAlias() {
    if (!aliasEditingMd5) return;
    const md5 = aliasEditingMd5;
    const alias = $('aliasInput').value.trim();
    try {
        await api(API + '/alias?apiKeyMd5=' + encodeURIComponent(md5)
                       + '&alias=' + encodeURIComponent(alias), { method: 'POST' });
        toast(alias ? '别名已保存' : '别名已清除', 'success');
        closeAliasModal();
        await refreshInstanceList();
        // 编辑的是当前选中实例时,同步刷新详情头部别名
        if (selectedMd5 === md5 && instances[md5]) {
            renderHeader(instances[md5]);
        }
    } catch (e) {
        toast(e.message, 'error');
    }
}
 
async function confirmAction() {
    if (!pendingAction || !selectedMd5) return;
    const action = pendingAction;
    closeModal();
 
    try {
        const msg = await api(API + '/' + action + '?apiKeyMd5=' + encodeURIComponent(selectedMd5), { method: 'POST' });
        toast(msg || '指令已发送', 'success');
        // 延迟刷新等待 ACK
        setTimeout(refreshInstanceList, 2000);
    } catch (e) {
        toast(e.message, 'error');
    }
}
 
// ==================== 轮询 ====================
async function poll() {
    await refreshInstanceList();
    // 如果有选中实例,刷新详情(full=false,不重渲染参数表单,避免清空用户编辑)
    if (selectedMd5 && instances[selectedMd5]) {
        await loadInstanceDetail(selectedMd5, false);
    }
}
 
// ==================== 初始化 ====================
function startPolling() {
    poll(); // 页面加载时立即加载一次实例列表(不再自动轮询,改为手动刷新)
}
 
startPolling();
</script>
</body>
</html>