@aaronbassett/midnight-local-devnet 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/dashboard.js +2 -0
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/dashboard/html.js +1155 -27
- package/dist/cli/dashboard/html.js.map +1 -1
- package/dist/cli/dashboard/lib/proof-server-api.js +5 -3
- package/dist/cli/dashboard/lib/proof-server-api.js.map +1 -1
- package/dist/cli/dashboard/server.js +154 -19
- package/dist/cli/dashboard/server.js.map +1 -1
- package/dist/cli/dashboard/state-collector.d.ts +24 -1
- package/dist/cli/dashboard/state-collector.js +119 -63
- package/dist/cli/dashboard/state-collector.js.map +1 -1
- package/dist/core/wallet.d.ts +2 -0
- package/dist/core/wallet.js +28 -0
- package/dist/core/wallet.js.map +1 -1
- package/package.json +1 -1
|
@@ -126,7 +126,6 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
126
126
|
border: 1px solid var(--mn-border);
|
|
127
127
|
border-radius: 12px;
|
|
128
128
|
position: relative;
|
|
129
|
-
overflow: hidden;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
.header::before {
|
|
@@ -138,6 +137,8 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
138
137
|
height: 300px;
|
|
139
138
|
background: radial-gradient(circle, rgba(59, 59, 255, 0.12) 0%, transparent 70%);
|
|
140
139
|
pointer-events: none;
|
|
140
|
+
border-radius: inherit;
|
|
141
|
+
clip-path: inset(0 0 0 0 round 12px);
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
.header-left {
|
|
@@ -553,6 +554,374 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
553
554
|
word-break: break-all;
|
|
554
555
|
}
|
|
555
556
|
|
|
557
|
+
/* --- Modal Overlay --- */
|
|
558
|
+
.modal-overlay {
|
|
559
|
+
position: fixed;
|
|
560
|
+
inset: 0;
|
|
561
|
+
background: rgba(9, 9, 15, 0.85);
|
|
562
|
+
display: flex;
|
|
563
|
+
align-items: center;
|
|
564
|
+
justify-content: center;
|
|
565
|
+
z-index: 150;
|
|
566
|
+
backdrop-filter: blur(4px);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.modal-dialog {
|
|
570
|
+
background: var(--mn-surface);
|
|
571
|
+
border: 1px solid var(--mn-border);
|
|
572
|
+
border-radius: 12px;
|
|
573
|
+
padding: 24px;
|
|
574
|
+
width: 480px;
|
|
575
|
+
max-width: 90vw;
|
|
576
|
+
max-height: 80vh;
|
|
577
|
+
overflow-y: auto;
|
|
578
|
+
box-shadow: 0 16px 64px rgba(9, 9, 15, 0.8);
|
|
579
|
+
animation: fadeInUp 0.2s ease-out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.modal-header {
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
justify-content: space-between;
|
|
586
|
+
margin-bottom: 20px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.modal-title {
|
|
590
|
+
font-size: 16px;
|
|
591
|
+
font-weight: 600;
|
|
592
|
+
color: var(--mn-text);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.modal-close {
|
|
596
|
+
background: none;
|
|
597
|
+
border: none;
|
|
598
|
+
color: var(--mn-text-secondary);
|
|
599
|
+
cursor: pointer;
|
|
600
|
+
padding: 4px;
|
|
601
|
+
display: flex;
|
|
602
|
+
align-items: center;
|
|
603
|
+
justify-content: center;
|
|
604
|
+
border-radius: 6px;
|
|
605
|
+
transition: all 0.15s;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.modal-close:hover {
|
|
609
|
+
color: var(--mn-text);
|
|
610
|
+
background: var(--mn-surface-alt);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* --- Import Modal Tabs --- */
|
|
614
|
+
.modal-tabs {
|
|
615
|
+
display: flex;
|
|
616
|
+
gap: 0;
|
|
617
|
+
margin-bottom: 16px;
|
|
618
|
+
border-bottom: 1px solid var(--mn-border);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.modal-tab {
|
|
622
|
+
flex: 1;
|
|
623
|
+
padding: 10px 16px;
|
|
624
|
+
background: none;
|
|
625
|
+
border: none;
|
|
626
|
+
border-bottom: 2px solid transparent;
|
|
627
|
+
color: var(--mn-text-secondary);
|
|
628
|
+
font-family: 'Inter', sans-serif;
|
|
629
|
+
font-size: 13px;
|
|
630
|
+
font-weight: 500;
|
|
631
|
+
cursor: pointer;
|
|
632
|
+
transition: all 0.15s;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.modal-tab:hover {
|
|
636
|
+
color: var(--mn-text);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.modal-tab.active {
|
|
640
|
+
color: var(--mn-accent);
|
|
641
|
+
border-bottom-color: var(--mn-accent);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.modal-field {
|
|
645
|
+
margin-bottom: 14px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.modal-label {
|
|
649
|
+
display: block;
|
|
650
|
+
font-size: 12px;
|
|
651
|
+
font-weight: 500;
|
|
652
|
+
color: var(--mn-text-secondary);
|
|
653
|
+
margin-bottom: 6px;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.modal-input {
|
|
657
|
+
width: 100%;
|
|
658
|
+
padding: 8px 12px;
|
|
659
|
+
border-radius: 6px;
|
|
660
|
+
border: 1px solid var(--mn-border);
|
|
661
|
+
background: var(--mn-surface-alt);
|
|
662
|
+
color: var(--mn-text);
|
|
663
|
+
font-family: 'Inter', sans-serif;
|
|
664
|
+
font-size: 13px;
|
|
665
|
+
outline: none;
|
|
666
|
+
transition: border-color 0.15s;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.modal-input:focus {
|
|
670
|
+
border-color: var(--mn-accent);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.modal-input.mono {
|
|
674
|
+
font-family: 'JetBrains Mono', monospace;
|
|
675
|
+
font-size: 12px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.modal-actions {
|
|
679
|
+
display: flex;
|
|
680
|
+
gap: 8px;
|
|
681
|
+
justify-content: flex-end;
|
|
682
|
+
margin-top: 20px;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.modal-textarea {
|
|
686
|
+
width: 100%;
|
|
687
|
+
padding: 10px 12px;
|
|
688
|
+
background: var(--mn-bg);
|
|
689
|
+
border: 1px solid var(--mn-border);
|
|
690
|
+
border-radius: 8px;
|
|
691
|
+
color: var(--mn-text);
|
|
692
|
+
font-size: 13px;
|
|
693
|
+
resize: vertical;
|
|
694
|
+
min-height: 60px;
|
|
695
|
+
box-sizing: border-box;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.modal-textarea:focus {
|
|
699
|
+
outline: none;
|
|
700
|
+
border-color: var(--mn-accent);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.mnemonic-display {
|
|
704
|
+
padding: 14px;
|
|
705
|
+
background: var(--mn-bg);
|
|
706
|
+
border: 1px solid var(--mn-border);
|
|
707
|
+
border-radius: 8px;
|
|
708
|
+
font-family: 'JetBrains Mono', monospace;
|
|
709
|
+
font-size: 13px;
|
|
710
|
+
line-height: 1.8;
|
|
711
|
+
color: var(--mn-text);
|
|
712
|
+
word-spacing: 6px;
|
|
713
|
+
user-select: all;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.mnemonic-warning {
|
|
717
|
+
display: flex;
|
|
718
|
+
align-items: flex-start;
|
|
719
|
+
gap: 8px;
|
|
720
|
+
padding: 10px 12px;
|
|
721
|
+
background: rgba(234, 179, 8, 0.08);
|
|
722
|
+
border: 1px solid rgba(234, 179, 8, 0.25);
|
|
723
|
+
border-radius: 8px;
|
|
724
|
+
font-size: 12px;
|
|
725
|
+
color: var(--mn-warning);
|
|
726
|
+
margin-bottom: 14px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.file-drop-zone {
|
|
730
|
+
position: relative;
|
|
731
|
+
border: 2px dashed var(--mn-border);
|
|
732
|
+
border-radius: 8px;
|
|
733
|
+
padding: 24px;
|
|
734
|
+
text-align: center;
|
|
735
|
+
cursor: pointer;
|
|
736
|
+
transition: border-color 0.15s, background 0.15s;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.file-drop-zone:hover {
|
|
740
|
+
border-color: var(--mn-accent);
|
|
741
|
+
background: rgba(59, 59, 255, 0.04);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.file-drop-zone input[type="file"] {
|
|
745
|
+
position: absolute;
|
|
746
|
+
inset: 0;
|
|
747
|
+
opacity: 0;
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.file-preview-table {
|
|
752
|
+
width: 100%;
|
|
753
|
+
font-size: 12px;
|
|
754
|
+
border-collapse: collapse;
|
|
755
|
+
margin-top: 10px;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.file-preview-table th,
|
|
759
|
+
.file-preview-table td {
|
|
760
|
+
text-align: left;
|
|
761
|
+
padding: 6px 8px;
|
|
762
|
+
border-bottom: 1px solid var(--mn-border);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.file-preview-table th {
|
|
766
|
+
color: var(--mn-text-muted);
|
|
767
|
+
font-weight: 500;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.file-preview-table td.mono {
|
|
771
|
+
font-family: 'JetBrains Mono', monospace;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* --- Wallet Selector Dropdown --- */
|
|
775
|
+
.wallet-selector {
|
|
776
|
+
display: flex;
|
|
777
|
+
align-items: center;
|
|
778
|
+
gap: 8px;
|
|
779
|
+
flex: 1;
|
|
780
|
+
min-width: 0;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.wallet-select {
|
|
784
|
+
flex: 1;
|
|
785
|
+
min-width: 0;
|
|
786
|
+
padding: 6px 10px;
|
|
787
|
+
border-radius: 6px;
|
|
788
|
+
border: 1px solid var(--mn-border);
|
|
789
|
+
background: var(--mn-surface-alt);
|
|
790
|
+
color: var(--mn-text);
|
|
791
|
+
font-family: 'Inter', sans-serif;
|
|
792
|
+
font-size: 13px;
|
|
793
|
+
cursor: pointer;
|
|
794
|
+
outline: none;
|
|
795
|
+
transition: border-color 0.15s;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.wallet-select:focus {
|
|
799
|
+
border-color: var(--mn-accent);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* --- Inline Edit Input --- */
|
|
803
|
+
.inline-edit-input {
|
|
804
|
+
padding: 4px 8px;
|
|
805
|
+
border-radius: 4px;
|
|
806
|
+
border: 1px solid var(--mn-accent);
|
|
807
|
+
background: var(--mn-surface-alt);
|
|
808
|
+
color: var(--mn-text);
|
|
809
|
+
font-family: 'Inter', sans-serif;
|
|
810
|
+
font-size: 13px;
|
|
811
|
+
outline: none;
|
|
812
|
+
width: 180px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* --- Icon Button --- */
|
|
816
|
+
.icon-btn {
|
|
817
|
+
background: none;
|
|
818
|
+
border: none;
|
|
819
|
+
color: var(--mn-text-muted);
|
|
820
|
+
cursor: pointer;
|
|
821
|
+
padding: 4px;
|
|
822
|
+
display: inline-flex;
|
|
823
|
+
align-items: center;
|
|
824
|
+
justify-content: center;
|
|
825
|
+
border-radius: 4px;
|
|
826
|
+
transition: all 0.15s;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.icon-btn:hover {
|
|
830
|
+
color: var(--mn-text);
|
|
831
|
+
background: var(--mn-surface-alt);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.icon-btn.danger:hover {
|
|
835
|
+
color: var(--mn-error);
|
|
836
|
+
background: rgba(239, 68, 68, 0.1);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/* --- Copy Button --- */
|
|
840
|
+
.copy-btn {
|
|
841
|
+
background: none;
|
|
842
|
+
border: none;
|
|
843
|
+
color: var(--mn-text-muted);
|
|
844
|
+
cursor: pointer;
|
|
845
|
+
padding: 4px 6px;
|
|
846
|
+
display: inline-flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
gap: 4px;
|
|
849
|
+
border-radius: 4px;
|
|
850
|
+
font-family: 'Inter', sans-serif;
|
|
851
|
+
font-size: 11px;
|
|
852
|
+
transition: all 0.15s;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.copy-btn:hover {
|
|
856
|
+
color: var(--mn-text);
|
|
857
|
+
background: var(--mn-surface-alt);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.copy-btn.copied {
|
|
861
|
+
color: var(--mn-success);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/* --- Pulsing Sync Animation --- */
|
|
865
|
+
@keyframes syncPulse {
|
|
866
|
+
0%, 100% { box-shadow: 0 0 0 0 var(--mn-accent-glow); }
|
|
867
|
+
50% { box-shadow: 0 0 12px 4px var(--mn-accent-glow); }
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.sync-indicator {
|
|
871
|
+
display: flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: 8px;
|
|
874
|
+
font-size: 12px;
|
|
875
|
+
color: var(--mn-text-secondary);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.sync-spinner {
|
|
879
|
+
width: 14px;
|
|
880
|
+
height: 14px;
|
|
881
|
+
border: 2px solid var(--mn-border);
|
|
882
|
+
border-top-color: var(--mn-accent);
|
|
883
|
+
border-radius: 50%;
|
|
884
|
+
animation: spin 0.8s linear infinite;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.sync-glow {
|
|
888
|
+
animation: syncPulse 2s ease-in-out infinite;
|
|
889
|
+
border-radius: 12px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* --- Confirm Delete Inline Prompt --- */
|
|
893
|
+
.confirm-delete {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
gap: 8px;
|
|
897
|
+
padding: 6px 10px;
|
|
898
|
+
background: var(--mn-surface-alt);
|
|
899
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
900
|
+
border-radius: 6px;
|
|
901
|
+
font-size: 12px;
|
|
902
|
+
color: var(--mn-text-secondary);
|
|
903
|
+
animation: fadeInUp 0.15s ease-out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.confirm-delete .btn {
|
|
907
|
+
padding: 4px 10px;
|
|
908
|
+
font-size: 11px;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* --- Wallet Card Header Row --- */
|
|
912
|
+
.wallet-header-row {
|
|
913
|
+
display: flex;
|
|
914
|
+
align-items: center;
|
|
915
|
+
gap: 8px;
|
|
916
|
+
margin-bottom: 16px;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.wallet-address-row {
|
|
920
|
+
display: flex;
|
|
921
|
+
align-items: center;
|
|
922
|
+
gap: 8px;
|
|
923
|
+
}
|
|
924
|
+
|
|
556
925
|
/* --- Connection Overlay --- */
|
|
557
926
|
.connection-overlay {
|
|
558
927
|
position: fixed;
|
|
@@ -622,6 +991,98 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
622
991
|
.toast.error { border-left: 3px solid var(--mn-error); }
|
|
623
992
|
.toast.fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
624
993
|
|
|
994
|
+
/* --- Card Footer (server time) --- */
|
|
995
|
+
.card-footer {
|
|
996
|
+
color: var(--mn-text-muted);
|
|
997
|
+
font-size: 11px;
|
|
998
|
+
text-align: right;
|
|
999
|
+
margin-top: 12px;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/* --- Disabled Button --- */
|
|
1003
|
+
.btn:disabled {
|
|
1004
|
+
opacity: 0.5;
|
|
1005
|
+
cursor: not-allowed;
|
|
1006
|
+
pointer-events: none;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.btn-spinner {
|
|
1010
|
+
display: inline-block;
|
|
1011
|
+
width: 12px;
|
|
1012
|
+
height: 12px;
|
|
1013
|
+
border: 2px solid var(--mn-border);
|
|
1014
|
+
border-top-color: currentColor;
|
|
1015
|
+
border-radius: 50%;
|
|
1016
|
+
animation: spin 0.8s linear infinite;
|
|
1017
|
+
flex-shrink: 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/* --- Polling Settings Popover --- */
|
|
1021
|
+
.settings-wrapper {
|
|
1022
|
+
position: relative;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.settings-popover {
|
|
1026
|
+
position: absolute;
|
|
1027
|
+
top: calc(100% + 8px);
|
|
1028
|
+
right: 0;
|
|
1029
|
+
width: 280px;
|
|
1030
|
+
background: var(--mn-surface);
|
|
1031
|
+
border: 1px solid var(--mn-border-bright);
|
|
1032
|
+
border-radius: 10px;
|
|
1033
|
+
padding: 16px;
|
|
1034
|
+
z-index: 50;
|
|
1035
|
+
box-shadow: 0 8px 32px rgba(9, 9, 15, 0.6);
|
|
1036
|
+
animation: fadeInUp 0.15s ease-out;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.settings-title {
|
|
1040
|
+
font-size: 13px;
|
|
1041
|
+
font-weight: 600;
|
|
1042
|
+
color: var(--mn-text);
|
|
1043
|
+
margin-bottom: 12px;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.settings-row {
|
|
1047
|
+
display: flex;
|
|
1048
|
+
align-items: center;
|
|
1049
|
+
justify-content: space-between;
|
|
1050
|
+
padding: 6px 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.settings-row:not(:last-child) {
|
|
1054
|
+
border-bottom: 1px solid var(--mn-border);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.settings-label {
|
|
1058
|
+
font-size: 12px;
|
|
1059
|
+
color: var(--mn-text-secondary);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.settings-input {
|
|
1063
|
+
width: 64px;
|
|
1064
|
+
padding: 4px 8px;
|
|
1065
|
+
border-radius: 4px;
|
|
1066
|
+
border: 1px solid var(--mn-border);
|
|
1067
|
+
background: var(--mn-surface-alt);
|
|
1068
|
+
color: var(--mn-text);
|
|
1069
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1070
|
+
font-size: 12px;
|
|
1071
|
+
text-align: center;
|
|
1072
|
+
outline: none;
|
|
1073
|
+
transition: border-color 0.15s;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.settings-input:focus {
|
|
1077
|
+
border-color: var(--mn-accent);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.settings-unit {
|
|
1081
|
+
font-size: 11px;
|
|
1082
|
+
color: var(--mn-text-muted);
|
|
1083
|
+
margin-left: 4px;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
625
1086
|
/* --- Responsive --- */
|
|
626
1087
|
@media (max-width: 768px) {
|
|
627
1088
|
#app { padding: 12px; }
|
|
@@ -638,7 +1099,7 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
638
1099
|
|
|
639
1100
|
<script type="module">
|
|
640
1101
|
import { h, render } from 'preact';
|
|
641
|
-
import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
|
|
1102
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'preact/hooks';
|
|
642
1103
|
import { html } from 'htm/preact';
|
|
643
1104
|
|
|
644
1105
|
// --- Lucide icon SVGs (inline, stroke-based) ---
|
|
@@ -653,6 +1114,15 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
653
1114
|
activity: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>\`,
|
|
654
1115
|
terminal: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>\`,
|
|
655
1116
|
search: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>\`,
|
|
1117
|
+
copy: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>\`,
|
|
1118
|
+
pencil: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>\`,
|
|
1119
|
+
trash: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>\`,
|
|
1120
|
+
plus: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>\`,
|
|
1121
|
+
x: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>\`,
|
|
1122
|
+
settings: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>\`,
|
|
1123
|
+
refresh: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>\`,
|
|
1124
|
+
upload: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>\`,
|
|
1125
|
+
key: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.3 9.3"/><circle cx="7.5" cy="15.5" r="5.5"/></svg>\`,
|
|
656
1126
|
};
|
|
657
1127
|
|
|
658
1128
|
// --- WebSocket URL (injected at generation time) ---
|
|
@@ -687,6 +1157,19 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
687
1157
|
return 'var(--mn-success)';
|
|
688
1158
|
}
|
|
689
1159
|
|
|
1160
|
+
function formatTime(isoString) {
|
|
1161
|
+
if (!isoString) return '--:--:--';
|
|
1162
|
+
try {
|
|
1163
|
+
const d = new Date(isoString);
|
|
1164
|
+
if (isNaN(d.getTime())) return '--:--:--';
|
|
1165
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
return '--:--:--';
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const POLLING_KEY = 'mn-polling-config';
|
|
1172
|
+
|
|
690
1173
|
// --- Default state ---
|
|
691
1174
|
const defaultState = {
|
|
692
1175
|
node: { chain: null, name: null, version: null, blockHeight: null, avgBlockTime: null, peers: null, syncing: null },
|
|
@@ -701,6 +1184,8 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
701
1184
|
containers: [],
|
|
702
1185
|
logs: [],
|
|
703
1186
|
networkStatus: 'unknown',
|
|
1187
|
+
walletSyncStatus: 'idle',
|
|
1188
|
+
serverTime: '',
|
|
704
1189
|
};
|
|
705
1190
|
|
|
706
1191
|
// --- Toast component ---
|
|
@@ -731,9 +1216,102 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
731
1216
|
\`;
|
|
732
1217
|
}
|
|
733
1218
|
|
|
1219
|
+
// --- PollingSettings ---
|
|
1220
|
+
function PollingSettings({ sendMessage }) {
|
|
1221
|
+
const [open, setOpen] = useState(false);
|
|
1222
|
+
const [config, setConfig] = useState(() => {
|
|
1223
|
+
try {
|
|
1224
|
+
const raw = localStorage.getItem(POLLING_KEY);
|
|
1225
|
+
if (raw) return JSON.parse(raw);
|
|
1226
|
+
} catch (e) { /* ignore */ }
|
|
1227
|
+
return { node: 5, indexer: 5, proofServer: 5 };
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// Send saved polling config on mount
|
|
1231
|
+
useEffect(() => {
|
|
1232
|
+
const saved = config;
|
|
1233
|
+
['node', 'indexer', 'proofServer'].forEach(service => {
|
|
1234
|
+
if (saved[service] && saved[service] !== 5) {
|
|
1235
|
+
sendMessage({ type: 'command', action: 'set-polling', service, interval: saved[service] * 1000 });
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
}, []);
|
|
1239
|
+
|
|
1240
|
+
const handleChange = useCallback((service, value) => {
|
|
1241
|
+
const num = Math.max(1, Math.min(60, parseInt(value, 10) || 5));
|
|
1242
|
+
setConfig(prev => {
|
|
1243
|
+
const updated = { ...prev, [service]: num };
|
|
1244
|
+
localStorage.setItem(POLLING_KEY, JSON.stringify(updated));
|
|
1245
|
+
return updated;
|
|
1246
|
+
});
|
|
1247
|
+
sendMessage({ type: 'command', action: 'set-polling', service, interval: num * 1000 });
|
|
1248
|
+
}, [sendMessage]);
|
|
1249
|
+
|
|
1250
|
+
return html\`
|
|
1251
|
+
<div class="settings-wrapper">
|
|
1252
|
+
<button class="btn" onClick=\${() => setOpen(!open)} title="Polling Settings">
|
|
1253
|
+
\${icons.settings}
|
|
1254
|
+
</button>
|
|
1255
|
+
\${open ? html\`
|
|
1256
|
+
<div class="settings-popover">
|
|
1257
|
+
<div class="settings-title">Polling Intervals</div>
|
|
1258
|
+
<div class="settings-row">
|
|
1259
|
+
<span class="settings-label">Node</span>
|
|
1260
|
+
<div>
|
|
1261
|
+
<input class="settings-input" type="number" min="1" max="60"
|
|
1262
|
+
value=\${config.node}
|
|
1263
|
+
onChange=\${e => handleChange('node', e.target.value)} />
|
|
1264
|
+
<span class="settings-unit">sec</span>
|
|
1265
|
+
</div>
|
|
1266
|
+
</div>
|
|
1267
|
+
<div class="settings-row">
|
|
1268
|
+
<span class="settings-label">Indexer</span>
|
|
1269
|
+
<div>
|
|
1270
|
+
<input class="settings-input" type="number" min="1" max="60"
|
|
1271
|
+
value=\${config.indexer}
|
|
1272
|
+
onChange=\${e => handleChange('indexer', e.target.value)} />
|
|
1273
|
+
<span class="settings-unit">sec</span>
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
<div class="settings-row">
|
|
1277
|
+
<span class="settings-label">Proof Server</span>
|
|
1278
|
+
<div>
|
|
1279
|
+
<input class="settings-input" type="number" min="1" max="60"
|
|
1280
|
+
value=\${config.proofServer}
|
|
1281
|
+
onChange=\${e => handleChange('proofServer', e.target.value)} />
|
|
1282
|
+
<span class="settings-unit">sec</span>
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
\` : null}
|
|
1287
|
+
</div>
|
|
1288
|
+
\`;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
734
1291
|
// --- Header ---
|
|
735
|
-
function Header({ networkStatus, onStart, onStop }) {
|
|
1292
|
+
function Header({ networkStatus, onStart, onStop, sendMessage }) {
|
|
736
1293
|
const statusLabel = networkStatus.charAt(0).toUpperCase() + networkStatus.slice(1);
|
|
1294
|
+
|
|
1295
|
+
const renderButtons = () => {
|
|
1296
|
+
if (networkStatus === 'running') {
|
|
1297
|
+
return html\`<button class="btn btn-danger" onClick=\${onStop}>\${icons.square} Stop</button>\`;
|
|
1298
|
+
}
|
|
1299
|
+
if (networkStatus === 'stopped') {
|
|
1300
|
+
return html\`<button class="btn btn-primary" onClick=\${onStart}>\${icons.play} Start</button>\`;
|
|
1301
|
+
}
|
|
1302
|
+
if (networkStatus === 'starting') {
|
|
1303
|
+
return html\`<button class="btn btn-primary" disabled><span class="btn-spinner"></span> Starting...</button>\`;
|
|
1304
|
+
}
|
|
1305
|
+
if (networkStatus === 'stopping') {
|
|
1306
|
+
return html\`<button class="btn btn-danger" disabled><span class="btn-spinner"></span> Stopping...</button>\`;
|
|
1307
|
+
}
|
|
1308
|
+
// unknown — show both
|
|
1309
|
+
return html\`
|
|
1310
|
+
<button class="btn btn-primary" onClick=\${onStart}>\${icons.play} Start</button>
|
|
1311
|
+
<button class="btn btn-danger" onClick=\${onStop}>\${icons.square} Stop</button>
|
|
1312
|
+
\`;
|
|
1313
|
+
};
|
|
1314
|
+
|
|
737
1315
|
return html\`
|
|
738
1316
|
<div class="header fade-in">
|
|
739
1317
|
<div class="header-left">
|
|
@@ -744,15 +1322,15 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
744
1322
|
</div>
|
|
745
1323
|
</div>
|
|
746
1324
|
<div class="header-actions">
|
|
747
|
-
|
|
748
|
-
|
|
1325
|
+
\${renderButtons()}
|
|
1326
|
+
<\${PollingSettings} sendMessage=\${sendMessage} />
|
|
749
1327
|
</div>
|
|
750
1328
|
</div>
|
|
751
1329
|
\`;
|
|
752
1330
|
}
|
|
753
1331
|
|
|
754
1332
|
// --- NodeCard ---
|
|
755
|
-
function NodeCard({ node, health }) {
|
|
1333
|
+
function NodeCard({ node, health, serverTime }) {
|
|
756
1334
|
const blockTimeStr = node.avgBlockTime != null ? (node.avgBlockTime / 1000).toFixed(1) + 's' : '--';
|
|
757
1335
|
return html\`
|
|
758
1336
|
<div class="card fade-in" style="animation-delay: 0ms">
|
|
@@ -779,12 +1357,13 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
779
1357
|
<span class="stat-row-value">\${node.version || '--'}</span>
|
|
780
1358
|
</div>
|
|
781
1359
|
</div>
|
|
1360
|
+
<div class="card-footer">\${formatTime(serverTime)}</div>
|
|
782
1361
|
</div>
|
|
783
1362
|
\`;
|
|
784
1363
|
}
|
|
785
1364
|
|
|
786
1365
|
// --- IndexerCard ---
|
|
787
|
-
function IndexerCard({ indexer, health }) {
|
|
1366
|
+
function IndexerCard({ indexer, health, serverTime }) {
|
|
788
1367
|
return html\`
|
|
789
1368
|
<div class="card fade-in" style="animation-delay: 150ms">
|
|
790
1369
|
<div class="card-header">
|
|
@@ -802,15 +1381,19 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
802
1381
|
<span class="stat-row-value">\${formatMs(indexer.responseTime)}</span>
|
|
803
1382
|
</div>
|
|
804
1383
|
</div>
|
|
1384
|
+
<div class="card-footer">\${formatTime(serverTime)}</div>
|
|
805
1385
|
</div>
|
|
806
1386
|
\`;
|
|
807
1387
|
}
|
|
808
1388
|
|
|
809
1389
|
// --- ProofServerCard ---
|
|
810
|
-
function ProofServerCard({ proofServer, health }) {
|
|
1390
|
+
function ProofServerCard({ proofServer, health, serverTime }) {
|
|
811
1391
|
const capacity = proofServer.jobCapacity || 1;
|
|
812
1392
|
const processing = proofServer.jobsProcessing || 0;
|
|
813
1393
|
const pct = Math.min(100, (processing / capacity) * 100);
|
|
1394
|
+
const proofVersionsDisplay = proofServer.proofVersions && proofServer.proofVersions.length > 0
|
|
1395
|
+
? proofServer.proofVersions.join(', ')
|
|
1396
|
+
: 'None';
|
|
814
1397
|
return html\`
|
|
815
1398
|
<div class="card fade-in" style="animation-delay: 300ms">
|
|
816
1399
|
<div class="card-header">
|
|
@@ -833,41 +1416,548 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
833
1416
|
</div>
|
|
834
1417
|
<div style="margin-top: 10px">
|
|
835
1418
|
<div class="stat-row">
|
|
836
|
-
<span class="stat-row-label">Version</span>
|
|
1419
|
+
<span class="stat-row-label">Server Version</span>
|
|
837
1420
|
<span class="stat-row-value">\${proofServer.version || '--'}</span>
|
|
838
1421
|
</div>
|
|
1422
|
+
<div class="stat-row">
|
|
1423
|
+
<span class="stat-row-label">Proof Versions</span>
|
|
1424
|
+
<span class="stat-row-value">\${proofVersionsDisplay}</span>
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
<div class="card-footer">\${formatTime(serverTime)}</div>
|
|
1428
|
+
</div>
|
|
1429
|
+
\`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// --- localStorage helpers for wallet list ---
|
|
1433
|
+
const WALLETS_KEY = 'mn-wallets';
|
|
1434
|
+
|
|
1435
|
+
function loadWallets() {
|
|
1436
|
+
try {
|
|
1437
|
+
const raw = localStorage.getItem(WALLETS_KEY);
|
|
1438
|
+
if (raw) return JSON.parse(raw);
|
|
1439
|
+
} catch (e) { /* ignore */ }
|
|
1440
|
+
return [];
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function saveWallets(wallets) {
|
|
1444
|
+
localStorage.setItem(WALLETS_KEY, JSON.stringify(wallets));
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// --- ImportWalletModal ---
|
|
1448
|
+
function ImportWalletModal({ onClose, onImport, sendMessage, deriveResult, deriveAccountsResult }) {
|
|
1449
|
+
const [tab, setTab] = useState('mnemonic');
|
|
1450
|
+
const [mnemonic, setMnemonic] = useState('');
|
|
1451
|
+
const [address, setAddress] = useState('');
|
|
1452
|
+
const [displayName, setDisplayName] = useState('');
|
|
1453
|
+
const [deriving, setDeriving] = useState(false);
|
|
1454
|
+
const [derivedAddress, setDerivedAddress] = useState(null);
|
|
1455
|
+
|
|
1456
|
+
// File import state
|
|
1457
|
+
const [fileAccounts, setFileAccounts] = useState(null);
|
|
1458
|
+
const [fileError, setFileError] = useState(null);
|
|
1459
|
+
const [derivingFile, setDerivingFile] = useState(false);
|
|
1460
|
+
const [fileResults, setFileResults] = useState(null);
|
|
1461
|
+
|
|
1462
|
+
// Watch for derive-result coming in
|
|
1463
|
+
useEffect(() => {
|
|
1464
|
+
if (deriveResult && deriving) {
|
|
1465
|
+
setDerivedAddress(deriveResult);
|
|
1466
|
+
setDeriving(false);
|
|
1467
|
+
}
|
|
1468
|
+
}, [deriveResult, deriving]);
|
|
1469
|
+
|
|
1470
|
+
// Watch for derive-accounts-result
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
if (deriveAccountsResult && derivingFile) {
|
|
1473
|
+
setFileResults(deriveAccountsResult);
|
|
1474
|
+
setDerivingFile(false);
|
|
1475
|
+
}
|
|
1476
|
+
}, [deriveAccountsResult, derivingFile]);
|
|
1477
|
+
|
|
1478
|
+
const handleDerive = useCallback(() => {
|
|
1479
|
+
if (!mnemonic.trim()) return;
|
|
1480
|
+
setDeriving(true);
|
|
1481
|
+
setDerivedAddress(null);
|
|
1482
|
+
sendMessage({ type: 'command', action: 'derive-address', mnemonic: mnemonic.trim() });
|
|
1483
|
+
}, [mnemonic, sendMessage]);
|
|
1484
|
+
|
|
1485
|
+
const handleFileSelect = useCallback((e) => {
|
|
1486
|
+
const file = e.target.files[0];
|
|
1487
|
+
if (!file) return;
|
|
1488
|
+
setFileError(null);
|
|
1489
|
+
setFileResults(null);
|
|
1490
|
+
const reader = new FileReader();
|
|
1491
|
+
reader.onload = (ev) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const data = JSON.parse(ev.target.result);
|
|
1494
|
+
if (!data.accounts || !Array.isArray(data.accounts)) {
|
|
1495
|
+
setFileError('Invalid format: expected { "accounts": [...] }');
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const withMnemonic = data.accounts.filter(a => a.mnemonic);
|
|
1499
|
+
if (withMnemonic.length === 0) {
|
|
1500
|
+
setFileError('No accounts with mnemonic phrases found');
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
setFileAccounts(withMnemonic);
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
setFileError('Failed to parse JSON: ' + err.message);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
reader.readAsText(file);
|
|
1509
|
+
}, []);
|
|
1510
|
+
|
|
1511
|
+
const handleDeriveFile = useCallback(() => {
|
|
1512
|
+
if (!fileAccounts) return;
|
|
1513
|
+
setDerivingFile(true);
|
|
1514
|
+
setFileResults(null);
|
|
1515
|
+
sendMessage({
|
|
1516
|
+
type: 'command',
|
|
1517
|
+
action: 'derive-accounts',
|
|
1518
|
+
accounts: fileAccounts.map(a => ({ name: a.name, mnemonic: a.mnemonic })),
|
|
1519
|
+
});
|
|
1520
|
+
}, [fileAccounts, sendMessage]);
|
|
1521
|
+
|
|
1522
|
+
const handleImportFile = useCallback(() => {
|
|
1523
|
+
if (!fileResults) return;
|
|
1524
|
+
for (const account of fileResults) {
|
|
1525
|
+
onImport({
|
|
1526
|
+
id: crypto.randomUUID(),
|
|
1527
|
+
publicKey: account.address,
|
|
1528
|
+
displayName: account.name || 'Imported Wallet',
|
|
1529
|
+
hasMnemonic: true,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
onClose();
|
|
1533
|
+
}, [fileResults, onImport, onClose]);
|
|
1534
|
+
|
|
1535
|
+
const handleImport = useCallback(() => {
|
|
1536
|
+
if (tab === 'mnemonic') {
|
|
1537
|
+
if (!derivedAddress) return;
|
|
1538
|
+
onImport({
|
|
1539
|
+
id: crypto.randomUUID(),
|
|
1540
|
+
publicKey: derivedAddress,
|
|
1541
|
+
displayName: displayName.trim() || 'Imported Wallet',
|
|
1542
|
+
hasMnemonic: true,
|
|
1543
|
+
});
|
|
1544
|
+
} else {
|
|
1545
|
+
if (!address.trim()) return;
|
|
1546
|
+
onImport({
|
|
1547
|
+
id: crypto.randomUUID(),
|
|
1548
|
+
publicKey: address.trim(),
|
|
1549
|
+
displayName: displayName.trim() || 'Imported Wallet',
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
onClose();
|
|
1553
|
+
}, [tab, derivedAddress, address, displayName, onImport, onClose]);
|
|
1554
|
+
|
|
1555
|
+
return html\`
|
|
1556
|
+
<div class="modal-overlay" onClick=\${(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
|
1557
|
+
<div class="modal-dialog">
|
|
1558
|
+
<div class="modal-header">
|
|
1559
|
+
<span class="modal-title">Import Wallet</span>
|
|
1560
|
+
<button class="modal-close" onClick=\${onClose}>\${icons.x}</button>
|
|
1561
|
+
</div>
|
|
1562
|
+
<div class="modal-tabs">
|
|
1563
|
+
<button class="modal-tab \${tab === 'file' ? 'active' : ''}" onClick=\${() => setTab('file')}>File</button>
|
|
1564
|
+
<button class="modal-tab \${tab === 'mnemonic' ? 'active' : ''}" onClick=\${() => { setTab('mnemonic'); setDerivedAddress(null); }}>Mnemonic</button>
|
|
1565
|
+
<button class="modal-tab \${tab === 'address' ? 'active' : ''}" onClick=\${() => setTab('address')}>Address</button>
|
|
1566
|
+
</div>
|
|
1567
|
+
\${tab === 'file' ? html\`
|
|
1568
|
+
<div class="modal-field">
|
|
1569
|
+
<label class="modal-label">accounts.json File</label>
|
|
1570
|
+
<div class="file-drop-zone">
|
|
1571
|
+
<input type="file" accept=".json,application/json" onChange=\${handleFileSelect} />
|
|
1572
|
+
<div style="color: var(--mn-text-muted); font-size: 13px;">
|
|
1573
|
+
\${icons.upload} Click to select accounts.json
|
|
1574
|
+
</div>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
\${fileError ? html\`
|
|
1578
|
+
<div style="color: var(--mn-error); font-size: 12px; margin-bottom: 10px;">\${fileError}</div>
|
|
1579
|
+
\` : null}
|
|
1580
|
+
\${fileAccounts && !fileResults ? html\`
|
|
1581
|
+
<div style="margin-bottom: 14px;">
|
|
1582
|
+
<div style="font-size: 12px; color: var(--mn-text-muted); margin-bottom: 8px;">
|
|
1583
|
+
Found \${fileAccounts.length} account\${fileAccounts.length !== 1 ? 's' : ''} with mnemonics
|
|
1584
|
+
</div>
|
|
1585
|
+
<table class="file-preview-table">
|
|
1586
|
+
<thead><tr><th>Name</th><th>Mnemonic</th></tr></thead>
|
|
1587
|
+
<tbody>
|
|
1588
|
+
\${fileAccounts.map(a => html\`
|
|
1589
|
+
<tr>
|
|
1590
|
+
<td>\${a.name}</td>
|
|
1591
|
+
<td class="mono" style="color: var(--mn-text-muted);">\${a.mnemonic.split(' ').slice(0, 3).join(' ')}...</td>
|
|
1592
|
+
</tr>
|
|
1593
|
+
\`)}
|
|
1594
|
+
</tbody>
|
|
1595
|
+
</table>
|
|
1596
|
+
<div style="margin-top: 12px;">
|
|
1597
|
+
<button class="btn btn-primary" onClick=\${handleDeriveFile} disabled=\${derivingFile}>
|
|
1598
|
+
\${derivingFile ? html\`<span class="btn-spinner"></span> Deriving...
|
|
1599
|
+
\` : 'Derive Addresses'}
|
|
1600
|
+
</button>
|
|
1601
|
+
</div>
|
|
1602
|
+
</div>
|
|
1603
|
+
\` : null}
|
|
1604
|
+
\${fileResults ? html\`
|
|
1605
|
+
<div style="margin-bottom: 14px;">
|
|
1606
|
+
<div style="font-size: 12px; color: var(--mn-success); margin-bottom: 8px;">
|
|
1607
|
+
Derived \${fileResults.length} address\${fileResults.length !== 1 ? 'es' : ''}
|
|
1608
|
+
</div>
|
|
1609
|
+
<table class="file-preview-table">
|
|
1610
|
+
<thead><tr><th>Name</th><th>Address</th></tr></thead>
|
|
1611
|
+
<tbody>
|
|
1612
|
+
\${fileResults.map(a => html\`
|
|
1613
|
+
<tr>
|
|
1614
|
+
<td>\${a.name}</td>
|
|
1615
|
+
<td class="mono">\${truncateAddress(a.address)}</td>
|
|
1616
|
+
</tr>
|
|
1617
|
+
\`)}
|
|
1618
|
+
</tbody>
|
|
1619
|
+
</table>
|
|
1620
|
+
</div>
|
|
1621
|
+
\` : null}
|
|
1622
|
+
<div class="modal-actions">
|
|
1623
|
+
<button class="btn" onClick=\${onClose}>Cancel</button>
|
|
1624
|
+
<button class="btn btn-primary" onClick=\${handleImportFile}
|
|
1625
|
+
disabled=\${!fileResults}>
|
|
1626
|
+
Import \${fileResults ? fileResults.length : ''} Wallet\${fileResults && fileResults.length !== 1 ? 's' : ''}
|
|
1627
|
+
</button>
|
|
1628
|
+
</div>
|
|
1629
|
+
\` : tab === 'mnemonic' ? html\`
|
|
1630
|
+
<div class="modal-field">
|
|
1631
|
+
<label class="modal-label">Mnemonic Phrase</label>
|
|
1632
|
+
<input class="modal-input mono" type="text" placeholder="Enter mnemonic words..."
|
|
1633
|
+
value=\${mnemonic} onInput=\${e => setMnemonic(e.target.value)} />
|
|
1634
|
+
</div>
|
|
1635
|
+
<div style="margin-bottom: 14px">
|
|
1636
|
+
<button class="btn" onClick=\${handleDerive} disabled=\${deriving || !mnemonic.trim()}>
|
|
1637
|
+
\${deriving ? 'Deriving...' : 'Derive Address'}
|
|
1638
|
+
</button>
|
|
1639
|
+
\${derivedAddress ? html\`
|
|
1640
|
+
<div style="margin-top: 8px; font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--mn-success); word-break: break-all;">
|
|
1641
|
+
Derived: \${truncateAddress(derivedAddress)}
|
|
1642
|
+
</div>
|
|
1643
|
+
\` : null}
|
|
1644
|
+
</div>
|
|
1645
|
+
<div class="modal-field">
|
|
1646
|
+
<label class="modal-label">Display Name (optional)</label>
|
|
1647
|
+
<input class="modal-input" type="text" placeholder="e.g. My Wallet"
|
|
1648
|
+
value=\${displayName} onInput=\${e => setDisplayName(e.target.value)} />
|
|
1649
|
+
</div>
|
|
1650
|
+
<div class="modal-actions">
|
|
1651
|
+
<button class="btn" onClick=\${onClose}>Cancel</button>
|
|
1652
|
+
<button class="btn btn-primary" onClick=\${handleImport}
|
|
1653
|
+
disabled=\${!derivedAddress}>
|
|
1654
|
+
Import
|
|
1655
|
+
</button>
|
|
1656
|
+
</div>
|
|
1657
|
+
\` : html\`
|
|
1658
|
+
<div class="modal-field">
|
|
1659
|
+
<label class="modal-label">Public Key / Address</label>
|
|
1660
|
+
<input class="modal-input mono" type="text" placeholder="Paste public key..."
|
|
1661
|
+
value=\${address} onInput=\${e => setAddress(e.target.value)} />
|
|
1662
|
+
</div>
|
|
1663
|
+
<div class="modal-field">
|
|
1664
|
+
<label class="modal-label">Display Name (optional)</label>
|
|
1665
|
+
<input class="modal-input" type="text" placeholder="e.g. My Wallet"
|
|
1666
|
+
value=\${displayName} onInput=\${e => setDisplayName(e.target.value)} />
|
|
1667
|
+
</div>
|
|
1668
|
+
<div class="modal-actions">
|
|
1669
|
+
<button class="btn" onClick=\${onClose}>Cancel</button>
|
|
1670
|
+
<button class="btn btn-primary" onClick=\${handleImport}
|
|
1671
|
+
disabled=\${!address.trim()}>
|
|
1672
|
+
Import
|
|
1673
|
+
</button>
|
|
1674
|
+
</div>
|
|
1675
|
+
\`}
|
|
1676
|
+
</div>
|
|
1677
|
+
</div>
|
|
1678
|
+
\`;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// --- GenerateWalletModal ---
|
|
1682
|
+
function GenerateWalletModal({ onClose, onImport, sendMessage, generateResult }) {
|
|
1683
|
+
const [generating, setGenerating] = useState(false);
|
|
1684
|
+
const [result, setResult] = useState(null);
|
|
1685
|
+
const [copied, setCopied] = useState(false);
|
|
1686
|
+
|
|
1687
|
+
useEffect(() => {
|
|
1688
|
+
if (generateResult && generating) {
|
|
1689
|
+
setResult(generateResult);
|
|
1690
|
+
setGenerating(false);
|
|
1691
|
+
}
|
|
1692
|
+
}, [generateResult, generating]);
|
|
1693
|
+
|
|
1694
|
+
const handleGenerate = useCallback(() => {
|
|
1695
|
+
setGenerating(true);
|
|
1696
|
+
setResult(null);
|
|
1697
|
+
setCopied(false);
|
|
1698
|
+
sendMessage({ type: 'command', action: 'generate-wallet' });
|
|
1699
|
+
}, [sendMessage]);
|
|
1700
|
+
|
|
1701
|
+
// Generate on mount
|
|
1702
|
+
useEffect(() => { handleGenerate(); }, []);
|
|
1703
|
+
|
|
1704
|
+
const handleCopy = useCallback(() => {
|
|
1705
|
+
if (!result) return;
|
|
1706
|
+
navigator.clipboard.writeText(result.mnemonic).then(() => {
|
|
1707
|
+
setCopied(true);
|
|
1708
|
+
setTimeout(() => setCopied(false), 2000);
|
|
1709
|
+
}).catch(() => {});
|
|
1710
|
+
}, [result]);
|
|
1711
|
+
|
|
1712
|
+
const handleAdd = useCallback(() => {
|
|
1713
|
+
if (!result) return;
|
|
1714
|
+
onImport({
|
|
1715
|
+
id: crypto.randomUUID(),
|
|
1716
|
+
publicKey: result.address,
|
|
1717
|
+
displayName: 'Generated Wallet',
|
|
1718
|
+
hasMnemonic: true,
|
|
1719
|
+
});
|
|
1720
|
+
onClose();
|
|
1721
|
+
}, [result, onImport, onClose]);
|
|
1722
|
+
|
|
1723
|
+
return html\`
|
|
1724
|
+
<div class="modal-overlay" onClick=\${(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
|
1725
|
+
<div class="modal-dialog">
|
|
1726
|
+
<div class="modal-header">
|
|
1727
|
+
<span class="modal-title">Generate Wallet</span>
|
|
1728
|
+
<button class="modal-close" onClick=\${onClose}>\${icons.x}</button>
|
|
1729
|
+
</div>
|
|
1730
|
+
\${generating ? html\`
|
|
1731
|
+
<div style="text-align: center; padding: 24px 0;">
|
|
1732
|
+
<span class="btn-spinner" style="width: 20px; height: 20px;"></span>
|
|
1733
|
+
<div style="margin-top: 10px; color: var(--mn-text-muted); font-size: 13px;">Generating wallet...</div>
|
|
1734
|
+
</div>
|
|
1735
|
+
\` : result ? html\`
|
|
1736
|
+
<div class="mnemonic-warning">
|
|
1737
|
+
<span style="font-size: 16px; flex-shrink: 0;">!</span>
|
|
1738
|
+
<span>Save this mnemonic phrase securely. It will not be shown again and cannot be recovered.</span>
|
|
1739
|
+
</div>
|
|
1740
|
+
<div class="modal-field">
|
|
1741
|
+
<label class="modal-label">Mnemonic Phrase</label>
|
|
1742
|
+
<div class="mnemonic-display">\${result.mnemonic}</div>
|
|
1743
|
+
</div>
|
|
1744
|
+
<div class="modal-field">
|
|
1745
|
+
<label class="modal-label">Derived Address</label>
|
|
1746
|
+
<div style="font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--mn-success); word-break: break-all;">
|
|
1747
|
+
\${result.address}
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
<div class="modal-actions" style="justify-content: space-between;">
|
|
1751
|
+
<div style="display: flex; gap: 8px;">
|
|
1752
|
+
<button class="btn" onClick=\${handleCopy}>
|
|
1753
|
+
\${icons.copy} \${copied ? 'Copied!' : 'Copy Mnemonic'}
|
|
1754
|
+
</button>
|
|
1755
|
+
<button class="btn" onClick=\${handleGenerate}>
|
|
1756
|
+
\${icons.refresh} New
|
|
1757
|
+
</button>
|
|
1758
|
+
</div>
|
|
1759
|
+
<button class="btn btn-primary" onClick=\${handleAdd}>Add to Wallets</button>
|
|
1760
|
+
</div>
|
|
1761
|
+
\` : null}
|
|
839
1762
|
</div>
|
|
840
1763
|
</div>
|
|
841
1764
|
\`;
|
|
842
1765
|
}
|
|
843
1766
|
|
|
844
1767
|
// --- WalletCard ---
|
|
845
|
-
function WalletCard({ wallet }) {
|
|
1768
|
+
function WalletCard({ wallet, walletSyncStatus, sendMessage, onOpenImportModal, onOpenGenerateModal, importHandlerRef }) {
|
|
1769
|
+
// Load wallets from localStorage, ensuring master wallet is always first
|
|
1770
|
+
const [wallets, setWallets] = useState(() => {
|
|
1771
|
+
const stored = loadWallets();
|
|
1772
|
+
const hasMaster = stored.some(w => w.id === 'master');
|
|
1773
|
+
if (!hasMaster) {
|
|
1774
|
+
const masterEntry = { id: 'master', publicKey: wallet.address || '', displayName: 'Master Wallet' };
|
|
1775
|
+
const all = [masterEntry, ...stored];
|
|
1776
|
+
saveWallets(all);
|
|
1777
|
+
return all;
|
|
1778
|
+
}
|
|
1779
|
+
return stored;
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
const [selectedId, setSelectedId] = useState('master');
|
|
1783
|
+
const [editingId, setEditingId] = useState(null);
|
|
1784
|
+
const [editName, setEditName] = useState('');
|
|
1785
|
+
const [deletingId, setDeletingId] = useState(null);
|
|
1786
|
+
const [copied, setCopied] = useState(false);
|
|
1787
|
+
|
|
1788
|
+
// Keep master wallet address in sync with server state
|
|
1789
|
+
useEffect(() => {
|
|
1790
|
+
if (wallet.address) {
|
|
1791
|
+
setWallets(prev => {
|
|
1792
|
+
const updated = prev.map(w => w.id === 'master' ? { ...w, publicKey: wallet.address } : w);
|
|
1793
|
+
saveWallets(updated);
|
|
1794
|
+
return updated;
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
}, [wallet.address]);
|
|
1798
|
+
|
|
1799
|
+
const selectedWallet = useMemo(() => {
|
|
1800
|
+
return wallets.find(w => w.id === selectedId) || wallets[0] || null;
|
|
1801
|
+
}, [wallets, selectedId]);
|
|
1802
|
+
|
|
1803
|
+
const isMaster = selectedWallet && selectedWallet.id === 'master';
|
|
1804
|
+
|
|
1805
|
+
const handleImportWallet = useCallback((newWallet) => {
|
|
1806
|
+
setWallets(prev => {
|
|
1807
|
+
const updated = [...prev, newWallet];
|
|
1808
|
+
saveWallets(updated);
|
|
1809
|
+
return updated;
|
|
1810
|
+
});
|
|
1811
|
+
setSelectedId(newWallet.id);
|
|
1812
|
+
}, []);
|
|
1813
|
+
|
|
1814
|
+
// Expose import handler to parent via ref
|
|
1815
|
+
useEffect(() => {
|
|
1816
|
+
if (importHandlerRef) importHandlerRef.current = handleImportWallet;
|
|
1817
|
+
}, [importHandlerRef, handleImportWallet]);
|
|
1818
|
+
|
|
1819
|
+
const handleStartEdit = useCallback((w) => {
|
|
1820
|
+
setEditingId(w.id);
|
|
1821
|
+
setEditName(w.displayName);
|
|
1822
|
+
}, []);
|
|
1823
|
+
|
|
1824
|
+
const handleSaveEdit = useCallback(() => {
|
|
1825
|
+
if (!editingId) return;
|
|
1826
|
+
setWallets(prev => {
|
|
1827
|
+
const updated = prev.map(w => w.id === editingId ? { ...w, displayName: editName.trim() || w.displayName } : w);
|
|
1828
|
+
saveWallets(updated);
|
|
1829
|
+
return updated;
|
|
1830
|
+
});
|
|
1831
|
+
setEditingId(null);
|
|
1832
|
+
}, [editingId, editName]);
|
|
1833
|
+
|
|
1834
|
+
const handleDelete = useCallback((id) => {
|
|
1835
|
+
setWallets(prev => {
|
|
1836
|
+
const updated = prev.filter(w => w.id !== id);
|
|
1837
|
+
saveWallets(updated);
|
|
1838
|
+
return updated;
|
|
1839
|
+
});
|
|
1840
|
+
setDeletingId(null);
|
|
1841
|
+
if (selectedId === id) setSelectedId('master');
|
|
1842
|
+
}, [selectedId]);
|
|
1843
|
+
|
|
1844
|
+
const handleCopy = useCallback((text) => {
|
|
1845
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1846
|
+
setCopied(true);
|
|
1847
|
+
setTimeout(() => setCopied(false), 1500);
|
|
1848
|
+
}).catch(() => {});
|
|
1849
|
+
}, []);
|
|
1850
|
+
|
|
1851
|
+
const showBalances = isMaster && walletSyncStatus !== 'syncing';
|
|
1852
|
+
const displayAddress = selectedWallet ? selectedWallet.publicKey : null;
|
|
1853
|
+
|
|
846
1854
|
return html\`
|
|
847
|
-
<div class="card full-width fade-in" style="animation-delay: 450ms">
|
|
1855
|
+
<div class="card full-width fade-in \${walletSyncStatus === 'syncing' ? 'sync-glow' : ''}" style="animation-delay: 450ms">
|
|
848
1856
|
<div class="card-header">
|
|
849
1857
|
<div class="card-title">
|
|
850
1858
|
\${icons.wallet}
|
|
851
1859
|
Wallet
|
|
1860
|
+
\${walletSyncStatus === 'syncing' ? html\`
|
|
1861
|
+
<span class="sync-indicator">
|
|
1862
|
+
<span class="sync-spinner"></span>
|
|
1863
|
+
Syncing...
|
|
1864
|
+
</span>
|
|
1865
|
+
\` : null}
|
|
852
1866
|
</div>
|
|
853
1867
|
<span class="card-health-dot \${wallet.connected ? 'healthy' : 'unhealthy'}"></span>
|
|
854
1868
|
</div>
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
<div class="
|
|
858
|
-
|
|
859
|
-
|
|
1869
|
+
|
|
1870
|
+
<div class="wallet-header-row">
|
|
1871
|
+
<div class="wallet-selector">
|
|
1872
|
+
\${editingId === (selectedWallet && selectedWallet.id) ? html\`
|
|
1873
|
+
<input class="inline-edit-input" type="text" value=\${editName}
|
|
1874
|
+
onInput=\${e => setEditName(e.target.value)}
|
|
1875
|
+
onBlur=\${handleSaveEdit}
|
|
1876
|
+
onKeyDown=\${e => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') setEditingId(null); }}
|
|
1877
|
+
autofocus />
|
|
1878
|
+
\` : html\`
|
|
1879
|
+
<select class="wallet-select" value=\${selectedId}
|
|
1880
|
+
onChange=\${e => { setSelectedId(e.target.value); setDeletingId(null); }}>
|
|
1881
|
+
\${wallets.map(w => html\`
|
|
1882
|
+
<option key=\${w.id} value=\${w.id}>\${w.displayName}</option>
|
|
1883
|
+
\`)}
|
|
1884
|
+
</select>
|
|
1885
|
+
\`}
|
|
1886
|
+
<button class="icon-btn" title="Rename wallet" onClick=\${() => selectedWallet && handleStartEdit(selectedWallet)}>
|
|
1887
|
+
\${icons.pencil}
|
|
1888
|
+
</button>
|
|
1889
|
+
\${selectedWallet && selectedWallet.id !== 'master' ? html\`
|
|
1890
|
+
<button class="icon-btn danger" title="Delete wallet" onClick=\${() => setDeletingId(selectedWallet.id)}>
|
|
1891
|
+
\${icons.trash}
|
|
1892
|
+
</button>
|
|
1893
|
+
\` : null}
|
|
1894
|
+
</div>
|
|
1895
|
+
<div style="display: flex; gap: 6px;">
|
|
1896
|
+
<button class="btn" onClick=\${() => sendMessage({ type: 'command', action: 'sync-wallet' })}
|
|
1897
|
+
disabled=\${walletSyncStatus === 'syncing'} title="Sync wallet balances">
|
|
1898
|
+
\${icons.refresh} Sync
|
|
1899
|
+
</button>
|
|
1900
|
+
<button class="btn" onClick=\${onOpenGenerateModal} title="Generate new wallet">
|
|
1901
|
+
\${icons.key} Generate
|
|
1902
|
+
</button>
|
|
1903
|
+
<button class="btn" onClick=\${onOpenImportModal} title="Import wallet">
|
|
1904
|
+
\${icons.plus} Import
|
|
1905
|
+
</button>
|
|
860
1906
|
</div>
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1907
|
+
</div>
|
|
1908
|
+
|
|
1909
|
+
\${deletingId ? html\`
|
|
1910
|
+
<div class="confirm-delete">
|
|
1911
|
+
<span>Delete this wallet?</span>
|
|
1912
|
+
<button class="btn btn-danger" onClick=\${() => handleDelete(deletingId)}>Yes, delete</button>
|
|
1913
|
+
<button class="btn" onClick=\${() => setDeletingId(null)}>Cancel</button>
|
|
864
1914
|
</div>
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1915
|
+
\` : null}
|
|
1916
|
+
|
|
1917
|
+
<div class="wallet-grid">
|
|
1918
|
+
<div class="wallet-address">
|
|
1919
|
+
<div class="wallet-address-row">
|
|
1920
|
+
<span class="mono" title=\${displayAddress || ''} style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">
|
|
1921
|
+
\${displayAddress ? truncateAddress(displayAddress) : 'No wallet connected'}
|
|
1922
|
+
</span>
|
|
1923
|
+
\${displayAddress ? html\`
|
|
1924
|
+
<button class="copy-btn \${copied ? 'copied' : ''}" onClick=\${() => handleCopy(displayAddress)}>
|
|
1925
|
+
\${icons.copy}
|
|
1926
|
+
\${copied ? 'Copied!' : 'Copy'}
|
|
1927
|
+
</button>
|
|
1928
|
+
\` : null}
|
|
1929
|
+
</div>
|
|
868
1930
|
</div>
|
|
1931
|
+
\${showBalances ? html\`
|
|
1932
|
+
<div class="balance-item">
|
|
1933
|
+
<div class="balance-value">\${formatBalance(wallet.unshielded)}</div>
|
|
1934
|
+
<div class="balance-label">NIGHT (Unshielded)</div>
|
|
1935
|
+
</div>
|
|
1936
|
+
<div class="balance-item">
|
|
1937
|
+
<div class="balance-value">\${formatBalance(wallet.shielded)}</div>
|
|
1938
|
+
<div class="balance-label">NIGHT (Shielded)</div>
|
|
1939
|
+
</div>
|
|
1940
|
+
<div class="balance-item">
|
|
1941
|
+
<div class="balance-value">\${formatBalance(wallet.dust)}</div>
|
|
1942
|
+
<div class="balance-label">DUST</div>
|
|
1943
|
+
</div>
|
|
1944
|
+
\` : isMaster && walletSyncStatus === 'syncing' ? html\`
|
|
1945
|
+
<div class="balance-item" style="grid-column: 1 / -1; text-align: center;">
|
|
1946
|
+
<div class="sync-indicator" style="justify-content: center;">
|
|
1947
|
+
<span class="sync-spinner"></span>
|
|
1948
|
+
Syncing balances...
|
|
1949
|
+
</div>
|
|
1950
|
+
</div>
|
|
1951
|
+
\` : !isMaster ? html\`
|
|
1952
|
+
<div class="balance-item" style="grid-column: 1 / -1; text-align: center;">
|
|
1953
|
+
<div style="font-size: 13px; color: var(--mn-text-muted); padding: 8px 0;">
|
|
1954
|
+
\${selectedWallet && selectedWallet.hasMnemonic ? 'Derived address — balances not available' : 'Address only — balances not available'}
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
\` : null}
|
|
869
1958
|
</div>
|
|
870
1959
|
</div>
|
|
1960
|
+
|
|
871
1961
|
\`;
|
|
872
1962
|
}
|
|
873
1963
|
|
|
@@ -1017,6 +2107,19 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
1017
2107
|
}
|
|
1018
2108
|
}, []);
|
|
1019
2109
|
|
|
2110
|
+
const sendMessage = useCallback((msg) => {
|
|
2111
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
2112
|
+
wsRef.current.send(JSON.stringify(msg));
|
|
2113
|
+
}
|
|
2114
|
+
}, []);
|
|
2115
|
+
|
|
2116
|
+
const [deriveResult, setDeriveResult] = useState(null);
|
|
2117
|
+
const [deriveAccountsResult, setDeriveAccountsResult] = useState(null);
|
|
2118
|
+
const [generateResult, setGenerateResult] = useState(null);
|
|
2119
|
+
const [showImportModal, setShowImportModal] = useState(false);
|
|
2120
|
+
const [showGenerateModal, setShowGenerateModal] = useState(false);
|
|
2121
|
+
const importHandlerRef = useRef(null);
|
|
2122
|
+
|
|
1020
2123
|
useEffect(() => {
|
|
1021
2124
|
function connect() {
|
|
1022
2125
|
const ws = new WebSocket(WS_URL);
|
|
@@ -1025,6 +2128,8 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
1025
2128
|
ws.addEventListener('open', () => {
|
|
1026
2129
|
setConnected(true);
|
|
1027
2130
|
backoff.current = 1000;
|
|
2131
|
+
// Auto-sync wallet on first connect
|
|
2132
|
+
ws.send(JSON.stringify({ type: 'command', action: 'sync-wallet' }));
|
|
1028
2133
|
});
|
|
1029
2134
|
|
|
1030
2135
|
ws.addEventListener('message', (event) => {
|
|
@@ -1034,6 +2139,12 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
1034
2139
|
setState(msg.data);
|
|
1035
2140
|
} else if (msg.type === 'result') {
|
|
1036
2141
|
addToast(msg.message || (msg.success ? 'Command succeeded' : 'Command failed'), msg.success ? 'success' : 'error');
|
|
2142
|
+
} else if (msg.type === 'derive-result') {
|
|
2143
|
+
setDeriveResult(msg.address || null);
|
|
2144
|
+
} else if (msg.type === 'derive-accounts-result') {
|
|
2145
|
+
setDeriveAccountsResult(msg.accounts || null);
|
|
2146
|
+
} else if (msg.type === 'generate-result') {
|
|
2147
|
+
setGenerateResult(msg);
|
|
1037
2148
|
}
|
|
1038
2149
|
} catch (e) {
|
|
1039
2150
|
// ignore malformed messages
|
|
@@ -1069,19 +2180,36 @@ export function generateDashboardHtml({ wsUrl }) {
|
|
|
1069
2180
|
return html\`
|
|
1070
2181
|
<\${ConnectionStatus} connected=\${connected} />
|
|
1071
2182
|
<\${ToastContainer} toasts=\${toasts} onRemove=\${removeToast} />
|
|
1072
|
-
<\${Header} networkStatus=\${state.networkStatus} onStart=\${() => sendCommand('start')} onStop=\${() => sendCommand('stop')} />
|
|
2183
|
+
<\${Header} networkStatus=\${state.networkStatus} onStart=\${() => sendCommand('start')} onStop=\${() => sendCommand('stop')} sendMessage=\${sendMessage} />
|
|
1073
2184
|
<div class="cards-grid">
|
|
1074
|
-
<\${NodeCard} node=\${state.node} health=\${state.health.node} />
|
|
1075
|
-
<\${IndexerCard} indexer=\${state.indexer} health=\${state.health.indexer} />
|
|
1076
|
-
<\${ProofServerCard} proofServer=\${state.proofServer} health=\${state.health.proofServer} />
|
|
2185
|
+
<\${NodeCard} node=\${state.node} health=\${state.health.node} serverTime=\${state.serverTime} />
|
|
2186
|
+
<\${IndexerCard} indexer=\${state.indexer} health=\${state.health.indexer} serverTime=\${state.serverTime} />
|
|
2187
|
+
<\${ProofServerCard} proofServer=\${state.proofServer} health=\${state.health.proofServer} serverTime=\${state.serverTime} />
|
|
1077
2188
|
</div>
|
|
1078
2189
|
<div class="cards-grid">
|
|
1079
|
-
<\${WalletCard} wallet=\${state.wallet} />
|
|
2190
|
+
<\${WalletCard} wallet=\${state.wallet} walletSyncStatus=\${state.walletSyncStatus} sendMessage=\${sendMessage} onOpenImportModal=\${() => { setDeriveResult(null); setDeriveAccountsResult(null); setShowImportModal(true); }} onOpenGenerateModal=\${() => { setGenerateResult(null); setShowGenerateModal(true); }} importHandlerRef=\${importHandlerRef} />
|
|
1080
2191
|
</div>
|
|
1081
2192
|
<div class="cards-grid">
|
|
1082
2193
|
<\${ResponseChart} health=\${state.health} />
|
|
1083
2194
|
</div>
|
|
1084
2195
|
<\${LogViewer} logs=\${state.logs} />
|
|
2196
|
+
\${showImportModal ? html\`
|
|
2197
|
+
<\${ImportWalletModal}
|
|
2198
|
+
onClose=\${() => setShowImportModal(false)}
|
|
2199
|
+
onImport=\${(w) => importHandlerRef.current && importHandlerRef.current(w)}
|
|
2200
|
+
sendMessage=\${sendMessage}
|
|
2201
|
+
deriveResult=\${deriveResult}
|
|
2202
|
+
deriveAccountsResult=\${deriveAccountsResult}
|
|
2203
|
+
/>
|
|
2204
|
+
\` : null}
|
|
2205
|
+
\${showGenerateModal ? html\`
|
|
2206
|
+
<\${GenerateWalletModal}
|
|
2207
|
+
onClose=\${() => setShowGenerateModal(false)}
|
|
2208
|
+
onImport=\${(w) => importHandlerRef.current && importHandlerRef.current(w)}
|
|
2209
|
+
sendMessage=\${sendMessage}
|
|
2210
|
+
generateResult=\${generateResult}
|
|
2211
|
+
/>
|
|
2212
|
+
\` : null}
|
|
1085
2213
|
\`;
|
|
1086
2214
|
}
|
|
1087
2215
|
|