diff --git a/web/src/components/PacketList.tsx b/web/src/components/PacketList.tsx
index e9a7bde..e9e7994 100644
--- a/web/src/components/PacketList.tsx
+++ b/web/src/components/PacketList.tsx
@@ -2,10 +2,17 @@ import React, { useState, useCallback } from "react";
import { useAppSelector, useAppDispatch } from "../hooks";
import { PacketRenderer } from "./packets/PacketRenderer";
import { StreamControl } from "./StreamControl";
-import { Trash2, RefreshCw, Archive } from "lucide-react";
+import {
+ Trash2,
+ RefreshCw,
+ Archive,
+ ChevronLeft,
+ ChevronRight,
+} from "lucide-react";
import { clearPackets, toggleStreamPause } from "../store/slices/packetSlice";
import { Packet } from "../lib/types";
import { Separator } from "./Separator";
+import { Button } from "./ui/Button";
// Number of packets to show per page
const PACKETS_PER_PAGE = 100;
@@ -122,13 +129,9 @@ export const PacketList: React.FC = () => {
/>
{/* Clear button */}
-
+
{packets.length} packets received
@@ -183,12 +186,14 @@ export const PacketList: React.FC = () => {
messages.
-
+
)}
@@ -202,35 +207,31 @@ export const PacketList: React.FC = () => {
-
+ Previous
+
-
+ Next
+
>
diff --git a/web/src/components/ui/Button.tsx b/web/src/components/ui/Button.tsx
new file mode 100644
index 0000000..5d049dd
--- /dev/null
+++ b/web/src/components/ui/Button.tsx
@@ -0,0 +1,72 @@
+import React from "react";
+import { LucideIcon } from "lucide-react";
+import { cn } from "../../lib/cn";
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ /** Button content */
+ children: React.ReactNode;
+ /** Optional icon to display before the text */
+ icon?: LucideIcon;
+ /** Button variant */
+ variant?: "primary" | "secondary" | "danger" | "ghost";
+ /** Button size */
+ size?: "sm" | "md" | "lg";
+ /** Full width button */
+ fullWidth?: boolean;
+ /** Optional class name to extend styles */
+ className?: string;
+}
+
+export const Button = React.forwardRef(
+ (
+ {
+ children,
+ icon: Icon,
+ variant = "secondary",
+ size = "md",
+ fullWidth = false,
+ className,
+ disabled,
+ ...props
+ },
+ ref
+ ) => {
+ const variantClasses = {
+ primary: "bg-blue-600 text-white hover:bg-blue-700 border-blue-700/50",
+ secondary: "text-neutral-400 hover:bg-neutral-700/50 border-neutral-950/90",
+ danger: "bg-red-700/20 text-red-400 hover:bg-red-700/30 border-red-900/60",
+ ghost: "text-neutral-400 hover:bg-neutral-700/30 border-transparent",
+ };
+
+ const sizeClasses = {
+ sm: "px-2 py-1 text-xs",
+ md: "px-3 py-1.5 text-sm",
+ lg: "px-4 py-2 text-base",
+ };
+
+ const disabledClasses = disabled
+ ? "opacity-50 cursor-not-allowed"
+ : "transition-colors";
+
+ return (
+
+ );
+ }
+);
+
+Button.displayName = "Button";
\ No newline at end of file
diff --git a/web/src/routes/channel.$channelId.tsx b/web/src/routes/channel.$channelId.tsx
index 8400c82..83eab7b 100644
--- a/web/src/routes/channel.$channelId.tsx
+++ b/web/src/routes/channel.$channelId.tsx
@@ -4,45 +4,59 @@ import { PageWrapper, MessageBubble } from "../components";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { ChevronLeft } from "lucide-react";
-export const Route = createFileRoute('/channel/$channelId')({
+export const Route = createFileRoute("/channel/$channelId")({
component: ChannelPage,
});
// Generate a deterministic color based on channel ID
const getChannelColor = (channelId: string) => {
const colors = [
- 'bg-green-500', 'bg-blue-500', 'bg-amber-500', 'bg-purple-500',
- 'bg-pink-500', 'bg-indigo-500', 'bg-red-500', 'bg-teal-500'
+ "bg-green-500",
+ "bg-blue-500",
+ "bg-amber-500",
+ "bg-purple-500",
+ "bg-pink-500",
+ "bg-indigo-500",
+ "bg-red-500",
+ "bg-teal-500",
];
-
+
// Use a hash of the channel ID to pick a consistent color
- const hash = channelId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
+ const hash = channelId
+ .split("")
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
return colors[hash % colors.length];
};
function ChannelPage() {
const { channelId } = Route.useParams();
const navigate = useNavigate();
-
- const { channels, messages, nodes } = useAppSelector(state => state.aggregator);
+
+ const { channels, messages, nodes } = useAppSelector(
+ (state) => state.aggregator
+ );
const channel = channels[channelId];
// Create the channel key in the same format used by the aggregator
const channelKey = `channel_${channelId}`;
-
+
// Get channel messages and sort by timestamp (oldest to newest)
// This creates a shallow copy of the message array to sort it without modifying the original
- const channelMessages = messages[channelKey] ?
- [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp) :
- [];
-
+ const channelMessages = messages[channelKey]
+ ? [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp)
+ : [];
+
useEffect(() => {
if (!channel) {
console.log(`[Channel] Channel ${channelId} not found`);
// Navigate back to channels page if this channel doesn't exist
- setTimeout(() => navigate({ to: '/channels' }), 500);
+ setTimeout(() => navigate({ to: "/channels" }), 500);
} else {
- console.log(`[Channel] Displaying ${channelMessages.length} messages for channel ${channelId}`);
- console.log(`[Channel] Message count from channel data: ${channel.messageCount}`);
+ console.log(
+ `[Channel] Displaying ${channelMessages.length} messages for channel ${channelId}`
+ );
+ console.log(
+ `[Channel] Message count from channel data: ${channel.messageCount}`
+ );
console.log(`[Channel] Available messages:`, messages);
console.log(`[Channel] Looking for key:`, channelKey);
}
@@ -63,28 +77,33 @@ function ChannelPage() {
return (
-
+
{/* Header */}
-
-
+
{channelId.substring(0, 2).toUpperCase()}
-
{channelId}
+
+ {channelId}
+
- {channel.nodes.length} nodes · {channel.textMessageCount} messages
+ {channel.nodes.length} nodes · {channel.textMessageCount}{" "}
+ messages
-
+
{/* Message List */}
{channelMessages.length === 0 ? (
@@ -94,10 +113,12 @@ function ChannelPage() {
) : (
{channelMessages.map((message) => {
- const nodeName = nodes[message.from]?.shortName || nodes[message.from]?.longName;
-
+ const nodeName =
+ nodes[message.from]?.shortName ||
+ nodes[message.from]?.longName;
+
return (
-
);
-}
\ No newline at end of file
+}
diff --git a/web/src/routes/channels.tsx b/web/src/routes/channels.tsx
index 702e064..207a6e1 100644
--- a/web/src/routes/channels.tsx
+++ b/web/src/routes/channels.tsx
@@ -3,22 +3,29 @@ import { useAppSelector } from "../hooks";
import { PageWrapper, ChannelCard } from "../components";
import { createFileRoute } from "@tanstack/react-router";
-export const Route = createFileRoute('/channels')({
+export const Route = createFileRoute("/channels")({
component: ChannelsPage,
});
function ChannelsPage() {
- const { channels, messages } = useAppSelector(state => state.aggregator);
+ const { channels, messages } = useAppSelector((state) => state.aggregator);
useEffect(() => {
- console.log(`[Channels] Displaying ${Object.keys(channels).length} channels`);
+ console.log(
+ `[Channels] Displaying ${Object.keys(channels).length} channels`
+ );
console.log(`[Channels] Messages in store:`, messages);
-
+
// Check if any channels have text messages but none are displayed
Object.entries(channels).forEach(([channelId, channel]) => {
const channelKey = `channel_${channelId}`;
- if (channel.textMessageCount > 0 && (!messages[channelKey] || messages[channelKey].length === 0)) {
- console.log(`[Channels] Mismatch: Channel ${channelId} reports ${channel.textMessageCount} text messages but has ${messages[channelKey]?.length || 0} in store`);
+ if (
+ channel.textMessageCount > 0 &&
+ (!messages[channelKey] || messages[channelKey].length === 0)
+ ) {
+ console.log(
+ `[Channels] Mismatch: Channel ${channelId} reports ${channel.textMessageCount} text messages but has ${messages[channelKey]?.length || 0} in store`
+ );
}
});
}, [channels, messages]);
@@ -32,20 +39,21 @@ function ChannelsPage() {
// If one has activity and other doesn't, prioritize the active one
if (a.lastMessage) return -1;
if (b.lastMessage) return 1;
-
+
// Fall back to text message count if no last message time
return (b.textMessageCount || 0) - (a.textMessageCount || 0);
});
return (
-
-
Channels
-
+
{sortedChannels.length === 0 ? (
No channels discovered yet.
-
Channels will appear here as they are discovered on the Meshtastic network.
+
+ Channels will appear here as they are discovered on the Meshtastic
+ network.
+
) : (
@@ -57,4 +65,4 @@ function ChannelsPage() {
);
-}
\ No newline at end of file
+}
diff --git a/web/src/routes/home.tsx b/web/src/routes/home.tsx
index 3b99fcc..2793161 100644
--- a/web/src/routes/home.tsx
+++ b/web/src/routes/home.tsx
@@ -1,25 +1,18 @@
import { PageWrapper, NodeList, GatewayList } from "../components";
import { createFileRoute } from "@tanstack/react-router";
-export const Route = createFileRoute('/home')({
+export const Route = createFileRoute("/home")({
component: HomePage,
});
function HomePage() {
return (
-
-
Mesh Overview
-
- Real-time view of your Meshtastic mesh network traffic
-
-
-