| Name |
Node ID |
- Type |
+
+
+ Type
+
+ |
Public key |
Last seen |
@@ -286,7 +355,7 @@ export const MeshCoreNodesView: React.FC = () => {
{renderHighlighted(String(n.prefix || n.id || ''), search)} |
{n.type} |
{renderHighlighted(formatPubKey(n.public_key), search)} |
- {n.last_heard_at ? new Date(n.last_heard_at).toLocaleString() : '-'} |
+ |
))}
@@ -351,8 +420,8 @@ export const MeshCoreNodesView: React.FC = () => {
Prefix: {renderHighlighted(String(selectedNode.prefix || ''), search)}
Type: {selectedNode.type}
Public key: {selectedNode.public_key}
- First seen: {selectedNode.first_heard_at ? new Date(selectedNode.first_heard_at).toLocaleString() : '-'}
- Last seen: {selectedNode.last_heard_at ? new Date(selectedNode.last_heard_at).toLocaleString() : '-'}
+ First seen:
+ Last seen:
{selectedNode.last_latitude !== undefined && selectedNode.last_longitude !== undefined && (
Location: {selectedNode.last_latitude}, {selectedNode.last_longitude}
)}
diff --git a/ui/src/services/API.ts b/ui/src/services/API.ts
index 1120b8f..2d20248 100644
--- a/ui/src/services/API.ts
+++ b/ui/src/services/API.ts
@@ -25,12 +25,16 @@ export class APIService {
return response.data as T;
}
- public async fetchPaginated(endpoint: string, page: number = 1, limit: number = 20): Promise> {
- const response = await this.client.get>(endpoint, {
- params: { page, limit },
- });
- return response.data as Pager;
- }
+ public async fetchPaginated(endpoint: string, page: number = 1, limit: number = 20, extraParams?: Record): Promise> {
+ const params: Record = { page, limit };
+ if (extraParams) {
+ Object.assign(params, extraParams);
+ }
+ const response = await this.client.get>(endpoint, {
+ params,
+ });
+ return response.data as Pager;
+ }
public async fetchRadios(protocol?: string): Promise {
const endpoint = protocol ? `/radios/${encodeURIComponent(protocol)}` : '/radios';
diff --git a/ui/src/services/MeshCoreService.ts b/ui/src/services/MeshCoreService.ts
index 7e8eb66..4f673ae 100644
--- a/ui/src/services/MeshCoreService.ts
+++ b/ui/src/services/MeshCoreService.ts
@@ -216,9 +216,10 @@ export class MeshCoreService {
return this.api.fetch(`/meshcore/stats/packets/${encodeURIComponent(view)}`);
}
- public async fetchNodes(page: number = 1, limit: number = 200): Promise> {
- return this.api.fetchPaginated('/meshcore/nodes', page, limit);
- }
+ public async fetchNodes(page: number = 1, limit: number = 200, type?: string): Promise> {
+ const extra: Record | undefined = type ? { type } : undefined;
+ return this.api.fetchPaginated('/meshcore/nodes', page, limit, extra);
+ }
public async fetchNodesCloseTo(hash: string): Promise {
return this.api.fetch(`/meshcore/nodes/close-to/${encodeURIComponent(hash)}`);
diff --git a/ui/src/styles/theme/_bootstrap-overrides.scss b/ui/src/styles/theme/_bootstrap-overrides.scss
index 0b2e52a..d4b1e37 100644
--- a/ui/src/styles/theme/_bootstrap-overrides.scss
+++ b/ui/src/styles/theme/_bootstrap-overrides.scss
@@ -67,3 +67,81 @@ pre {
.main-content a {
color: var(--app-accent-yellow) !important;
}
+
+// Dropdown overrides to match global app theme
+.dropdown,
+.dropup,
+.dropleft,
+.dropright {
+ // ensure dropdown toggles use accent color for icons/links
+ .dropdown-toggle {
+ color: var(--app-accent-primary);
+ &:hover,
+ &:focus {
+ color: var(--app-accent-blue);
+ background: transparent;
+ text-decoration: none;
+ }
+ }
+}
+
+.dropdown-menu {
+ background-color: var(--app-bg-elevated);
+ color: var(--app-text);
+ border: 1px solid var(--app-border-color);
+ box-shadow: 0 6px 18px rgba(2,10,26,0.6);
+ min-width: 10rem;
+ padding: 0.25rem 0;
+}
+
+.dropdown-item {
+ color: var(--app-text);
+ padding: 0.375rem 1rem;
+ font-size: 0.92em;
+}
+
+.dropdown-item:hover,
+.dropdown-item:focus,
+.dropdown-item.active {
+ background-color: var(--app-button-hover);
+ color: var(--app-text);
+}
+
+// Ensure svg icons inside dropdown toggles inherit theme color
+.dropdown-toggle svg {
+ color: currentColor;
+ fill: currentColor;
+}
+
+// Smaller caret spacing for compact header controls
+.data-table-header .dropdown-toggle {
+ padding: 0 0.25rem;
+ color: var(--app-accent-yellow);
+}
+
+.data-table-header .dropdown-toggle:hover,
+.data-table-header .dropdown-toggle:focus {
+ color: var(--app-accent-yellow-muted);
+}
+
+/* Some Dropdown.Toggle instances use custom bsPrefix and don't include
+ the `dropdown-toggle` class. Target the header button by id and title
+ so the icon and button color are always visible. */
+.data-table-header button#type-filter-toggle-header,
+.data-table-header button#type-filter-toggle,
+.data-table-header button[title="Filter by type"] {
+ color: var(--app-accent-yellow) !important;
+}
+
+.data-table-header button#type-filter-toggle-header:hover,
+.data-table-header button#type-filter-toggle:hover,
+.data-table-header button[title="Filter by type"]:hover {
+ color: var(--app-accent-yellow-muted) !important;
+}
+
+.data-table-header button#type-filter-toggle-header svg,
+.data-table-header button#type-filter-toggle svg,
+.data-table-header button[title="Filter by type"] svg {
+ color: currentColor;
+ fill: currentColor;
+}