ispbills-icli 8.7.2 → 8.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tui/components.js +150 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.7.2",
3
+ "version": "8.7.3",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
@@ -614,6 +614,20 @@ function statusCounts(list) {
614
614
  return { on, off, other: list.length - on - off };
615
615
  }
616
616
 
617
+ /** Format a timestamp as a relative "Xm ago" / "Xh ago" string. */
618
+ function fmtAgo(ts) {
619
+ if (!ts) return '';
620
+ const diff = Date.now() - new Date(ts).getTime();
621
+ if (isNaN(diff) || diff < 0) return '';
622
+ const s = Math.floor(diff / 1000);
623
+ if (s < 90) return `${s}s ago`;
624
+ const m = Math.floor(s / 60);
625
+ if (m < 90) return `${m}m ago`;
626
+ const h = Math.floor(m / 60);
627
+ if (h < 48) return `${h}h ago`;
628
+ return `${Math.floor(h / 24)}d ago`;
629
+ }
630
+
617
631
  // ── DevicesPanel ─────────────────────────────────────────────────────────────
618
632
 
619
633
  export function DevicesPanel({ data, loading, error, cols }) {
@@ -658,56 +672,95 @@ export function DevicesPanel({ data, loading, error, cols }) {
658
672
  <//>
659
673
  <//>`;
660
674
 
661
- const nameW = Math.max(20, Math.floor(cols * 0.35));
662
- const ipW = 16;
663
- const tagW = 10;
664
- const maxRowsPerGroup = Math.max(5, Math.floor((cols > 100 ? 12 : 8)));
675
+ // Compute column widths based on terminal width
676
+ const nameW = Math.max(20, Math.floor(cols * 0.32));
677
+ const ipW = 17;
678
+ const statusW = 9; // ' ONLINE ' / ' OFFLINE ' / ' UNKNOWN '
679
+ const maxRowsPerGroup = Math.max(5, cols > 100 ? 14 : 10);
665
680
 
666
681
  const ts = data.fetchedAt
667
682
  ? new Date(data.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
668
683
  : '';
669
684
 
685
+ // Summary line: total counts across all device types
686
+ const totals = groups.reduce((acc, g) => {
687
+ const { on, off, other } = statusCounts(g.items);
688
+ acc.on += on; acc.off += off; acc.other += other; acc.total += g.items.length;
689
+ return acc;
690
+ }, { on: 0, off: 0, other: 0, total: 0 });
691
+
670
692
  return html`
671
693
  <${Box} flexDirection="column">
672
694
  ${header}
695
+
696
+ <!-- Fleet summary bar -->
697
+ <${Box} paddingX=${2} marginBottom=${1}>
698
+ <${Text} color=${C.muted}>Total <//>
699
+ <${Text} bold>${totals.total}<//>
700
+ <${Text} color=${C.muted}> devices <//>
701
+ <${Text} color=${C.success} bold>● ${totals.on} online<//>
702
+ <${Text} color=${C.muted}> <//>
703
+ <${Text} color=${C.error} bold>○ ${totals.off} offline<//>
704
+ ${totals.other > 0 ? html`<${Text} color=${C.warning} bold> ◌ ${totals.other} unknown<//>` : null}
705
+ <//>
706
+
673
707
  <${Box} flexDirection="column" paddingX=${2} paddingY=${0}>
674
708
  ${groups.map((g) => {
675
709
  const { on, off, other } = statusCounts(g.items);
710
+ // Sort: online first, unknown second, offline last
676
711
  const sorted = [...g.items].sort((a, b) => {
677
712
  const order = { online: 0, unknown: 1, offline: 2 };
678
713
  return (order[devStatus(a)] ?? 1) - (order[devStatus(b)] ?? 1);
679
714
  });
680
715
  const shown = sorted.slice(0, maxRowsPerGroup);
681
716
  const hidden = sorted.length - shown.length;
717
+
682
718
  return html`
683
719
  <${Box} key=${g.label} flexDirection="column" marginTop=${1}>
720
+ <!-- Group header with status summary -->
684
721
  <${Box}>
685
722
  <${Text} color=${C.accent} bold>${g.icon} ${g.label}<//>
686
- <${Text} color=${C.muted}> <//>
687
- <${Text} color=${C.success}>${on}↑<//>
688
- <${Text} color=${C.muted}> <//>
689
- <${Text} color=${C.error}>${off}↓<//>
690
- ${other > 0 ? html`<${Text} color=${C.warning}> ${other}?<//>` : null}
723
+ <${Text} color=${C.muted}> (${g.items.length}) <//>
724
+ <${Text} color=${C.success}>${on} online<//>
725
+ ${off > 0 ? html`<${Text} color=${C.error}> ${off} offline<//>` : null}
726
+ ${other > 0 ? html`<${Text} color=${C.warning}> ${other} unknown<//>` : null}
727
+ <//>
728
+
729
+ <!-- Column header -->
730
+ <${Box}>
731
+ <${Text} color=${C.muted}> ${'Name'.padEnd(nameW)}${'IP Address'.padEnd(ipW)}Status Checked<//>
691
732
  <//>
733
+
692
734
  ${shown.map((d, i) => {
735
+ const st = devStatus(d);
693
736
  const { dot, color } = statusDot(d);
694
737
  const isLast = i === shown.length - 1 && hidden === 0;
695
738
  const tree = isLast ? '└─' : '├─';
739
+
696
740
  const name = (d.name || d.device_name || `#${d.id}`)
697
741
  .slice(0, nameW - 1).padEnd(nameW);
698
742
  const ip = (d.ip_address || '─').slice(0, ipW - 1).padEnd(ipW);
699
- const extra = d.model ? d.model.slice(0, tagW) : '';
743
+
744
+ // Status badge
745
+ const badgeText = st === 'online' ? ' ONLINE ' : st === 'offline' ? ' OFFLINE' : ' UNKNOWN';
746
+ const badgeColor = st === 'online' ? C.success : st === 'offline' ? C.error : C.warning;
747
+
748
+ // Last-check time
749
+ const checkTs = d.last_check || d.last_seen || d.created_at;
750
+ const ago = fmtAgo(checkTs);
751
+
700
752
  return html`
701
753
  <${Box} key=${d.id}>
702
- <${Text} color=${C.muted}>${tree}<//>
754
+ <${Text} color=${C.muted}>${tree} <//>
703
755
  <${Text} color=${color}>${dot} <//>
704
756
  <${Text}>${name}<//>
705
757
  <${Text} color=${C.muted}>${ip}<//>
706
- <${Text} color=${C.muted} dimColor>${extra}<//>
758
+ <${Text} color=${badgeColor} bold>${badgeText}<//>
759
+ ${ago ? html`<${Text} color=${C.muted} dimColor> ${ago}<//>` : null}
707
760
  <//>`;
708
761
  })}
709
762
  ${hidden > 0
710
- ? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …and ${hidden} more<//> <//>`
763
+ ? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …and ${hidden} more (press r to refresh)<//> <//>`
711
764
  : null}
712
765
  <//>`;
713
766
  })}
@@ -716,7 +769,7 @@ export function DevicesPanel({ data, loading, error, cols }) {
716
769
  <${Text} color=${C.separator}>${'─'.repeat(Math.max(1, bw))}<//>
717
770
  <//>
718
771
  <${Box} paddingX=${2}>
719
- <${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} Enter → Session<//>
772
+ <${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} ● online ○ offline ◌ unknown ${midDot()} Enter → Session<//>
720
773
  <//>
721
774
  <//>`;
722
775
  }
@@ -751,81 +804,130 @@ export function MapsPanel({ data, loading, error, cols }) {
751
804
  <//>`;
752
805
 
753
806
  const tiers = [
754
- { label: 'Routers', items: data.routers, icon: '⬡', color: C.brand },
755
- { label: 'OLTs', items: data.olts, icon: '◈', color: C.accent },
756
- { label: 'Ubiquiti APs', items: data.ubiquiti, icon: '◉', color: C.success },
757
- { label: 'Cambium APs', items: data.cambium, icon: '◉', color: C.warning },
807
+ { label: 'Routers', items: data.routers, icon: '⬡', color: C.brand },
808
+ { label: 'OLTs', items: data.olts, icon: '◈', color: C.accent },
809
+ { label: 'Ubiquiti APs', items: data.ubiquiti, icon: '◉', color: C.success },
810
+ { label: 'Cambium APs', items: data.cambium, icon: '◉', color: C.warning },
758
811
  ].filter((t) => t.items.length > 0);
759
812
 
760
- const maxPerTier = Math.max(4, Math.floor((cols > 100 ? 10 : 6)));
761
- const indent = ' ';
813
+ const maxPerTier = cols > 100 ? 12 : 8;
814
+ const nameW = Math.max(18, Math.floor(cols * 0.28));
815
+ const ipW = 17;
816
+
817
+ // Build the total fleet summary
818
+ const fleet = tiers.reduce((acc, t) => {
819
+ const { on, off, other } = statusCounts(t.items);
820
+ acc.on += on; acc.off += off; acc.other += other; acc.total += t.items.length;
821
+ return acc;
822
+ }, { on: 0, off: 0, other: 0, total: 0 });
823
+
824
+ const ts = data.fetchedAt
825
+ ? new Date(data.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
826
+ : '';
762
827
 
763
828
  return html`
764
829
  <${Box} flexDirection="column">
765
830
  ${header}
831
+
766
832
  <${Box} flexDirection="column" paddingX=${2} paddingY=${0}>
767
833
 
768
- <${Box} marginTop=${1} marginBottom=${0}>
769
- <${Text} color=${C.muted}>${indent}┌─────────────┐<//>
834
+ <!-- Fleet summary -->
835
+ <${Box} marginTop=${1} marginBottom=${1}>
836
+ <${Text} color=${C.muted}>Fleet: <//>
837
+ <${Text} bold>${fleet.total} devices<//>
838
+ <${Text} color=${C.muted}> <//>
839
+ <${Text} color=${C.success} bold>● ${fleet.on} up<//>
840
+ <${Text} color=${C.muted}> <//>
841
+ <${Text} color=${C.error} bold>○ ${fleet.off} down<//>
842
+ ${fleet.other > 0 ? html`<${Text} color=${C.warning} bold> ◌ ${fleet.other} unknown<//>` : null}
843
+ <//>
844
+
845
+ <!-- Internet cloud node -->
846
+ <${Box}>
847
+ <${Text} color=${C.muted}> ╔══════════════╗<//>
770
848
  <//>
771
849
  <${Box}>
772
- <${Text} color=${C.muted}>${indent}│<//>
773
- <${Text} color=${C.brand} bold> Internet <//>
774
- <${Text} color=${C.muted}>│<//>
850
+ <${Text} color=${C.muted}> ║ <//>
851
+ <${Text} color=${C.brand} bold> INTERNET <//>
852
+ <${Text} color=${C.muted}> ║<//>
775
853
  <//>
776
- <${Box} marginBottom=${0}>
777
- <${Text} color=${C.muted}>${indent}└──────┬──────┘<//>
854
+ <${Box}>
855
+ <${Text} color=${C.muted}> ╚══════╤═══════╝<//>
778
856
  <//>
779
857
 
858
+ <!-- Each tier rendered as a branch under the topology tree -->
780
859
  ${tiers.map((tier, ti) => {
781
- const { on, off } = statusCounts(tier.items);
782
- const shown = tier.items.slice(0, maxPerTier);
783
- const hidden = tier.items.length - shown.length;
860
+ const { on, off, other } = statusCounts(tier.items);
861
+ // Sort: online first, offline last
862
+ const sorted = [...tier.items].sort((a, b) => {
863
+ const order = { online: 0, unknown: 1, offline: 2 };
864
+ return (order[devStatus(a)] ?? 1) - (order[devStatus(b)] ?? 1);
865
+ });
866
+ const shown = sorted.slice(0, maxPerTier);
867
+ const hidden = sorted.length - shown.length;
784
868
  const isLast = ti === tiers.length - 1;
785
- const nameW = Math.max(18, Math.floor(cols * 0.3));
786
869
 
787
870
  return html`
788
871
  <${Box} key=${tier.label} flexDirection="column">
872
+
873
+ <!-- Vertical connector to this tier -->
874
+ <${Box}>
875
+ <${Text} color=${C.muted}> │<//>
876
+ <//>
877
+
878
+ <!-- Tier header box -->
879
+ <${Box}>
880
+ <${Text} color=${C.muted}> ┌──────┴──────────────────────────────<//>
881
+ <//>
789
882
  <${Box}>
790
- <${Text} color=${C.muted}>${indent} │<//>
883
+ <${Text} color=${C.muted}> │ <//>
884
+ <${Text} color=${tier.color} bold>${tier.icon} ${tier.label.padEnd(14)}<//>
885
+ <${Text} color=${C.success}> ● ${on} up<//>
886
+ ${off > 0 ? html`<${Text} color=${C.error}> ○ ${off} down<//>` : null}
887
+ ${other > 0 ? html`<${Text} color=${C.warning}> ◌ ${other} unk<//>` : null}
791
888
  <//>
792
889
  <${Box}>
793
- <${Text} color=${C.muted}>${indent} ┌────┴<//>
794
- <${Text} color=${tier.color} bold> ${tier.icon} ${tier.label} <//>
795
- <${Text} color=${C.success}>${on}↑ <//>
796
- <${Text} color=${C.error}>${off}↓<//>
890
+ <${Text} color=${C.muted}> └─┬────────────────────────────────<//>
797
891
  <//>
892
+
893
+ <!-- Device rows -->
798
894
  ${shown.map((d, i) => {
895
+ const st = devStatus(d);
799
896
  const { dot, color } = statusDot(d);
800
897
  const isLastItem = i === shown.length - 1 && hidden === 0;
801
- const tree = isLastItem ? ' └──' : ' ├──';
802
- const name = (d.name || d.device_name || `#${d.id}`).slice(0, nameW);
803
- const ip = d.ip_address ? ` ${d.ip_address}` : '';
804
- const st = devStatus(d) === 'offline' ? ' [offline]' : '';
898
+ const branch = isLastItem ? ' └─' : ' ├─';
899
+ const name = (d.name || d.device_name || `#${d.id}`).slice(0, nameW - 1).padEnd(nameW);
900
+ const ip = (d.ip_address || '─').slice(0, ipW - 1).padEnd(ipW);
901
+ const ago = fmtAgo(d.last_check || d.last_seen);
902
+ const badge = st === 'online' ? 'UP ' : st === 'offline' ? 'DOWN' : 'UNKN';
903
+ const badgeColor = st === 'online' ? C.success : st === 'offline' ? C.error : C.warning;
805
904
  return html`
806
905
  <${Box} key=${d.id}>
807
- <${Text} color=${C.muted}>${indent}${tree} <//>
906
+ <${Text} color=${C.muted}> ${branch} <//>
808
907
  <${Text} color=${color}>${dot} <//>
809
- <${Text} bold>${name}<//>
908
+ <${Text}>${name}<//>
810
909
  <${Text} color=${C.muted}>${ip}<//>
811
- <${Text} color=${C.error} dimColor>${st}<//>
910
+ <${Text} color=${badgeColor} bold>[${badge}]<//>
911
+ ${ago ? html`<${Text} color=${C.muted} dimColor> ${ago}<//>` : null}
812
912
  <//>`;
813
913
  })}
814
914
  ${hidden > 0
815
- ? html`<${Box}><${Text} color=${C.muted} dimColor>${indent} └── …+${hidden} more<//><//>`
915
+ ? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …+${hidden} more<//> <//>`
816
916
  : null}
817
- ${!isLast
818
- ? null
819
- : html`<${Box}><${Text} color=${C.muted}>${indent} │ (end)<//> <//>` }
820
917
  <//>`;
821
918
  })}
822
919
 
920
+ <!-- End cap -->
921
+ <${Box} marginTop=${1}>
922
+ <${Text} color=${C.muted} dimColor> (topology end)<//>
923
+ <//>
823
924
  <//>
925
+
824
926
  <${Box} paddingX=${2} marginTop=${1}>
825
927
  <${Text} color=${C.separator}>${'─'.repeat(Math.max(1, bw))}<//>
826
928
  <//>
827
929
  <${Box} paddingX=${2}>
828
- <${Text} color=${C.muted} dimColor>r refresh ${midDot()} /topology for AI-generated diagram ${midDot()} Enter Session<//>
930
+ <${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} up ○ down ◌ unknown ${midDot()} /topology for AI analysis<//>
829
931
  <//>
830
932
  <//>`;
831
933
  }