mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-10-30 19:52:03 +01:00
feat: raw packet capture, browse, and classification (v0.13.0)
Add a first-class Raw Packets feature that captures every inbound MeshCore
packet from the LetsMesh `packets` feed exactly as received, independent of
how the collector later classifies it.
Capture & storage
- New `RawPacket` model + migration (raw_packets table) with single and
composite indexes for the dominant filter-then-sort-by-newest queries.
- Collector-side `RAW_PACKET_CAPTURE_ENABLED` flag (default off); capture hook
reuses the decoder's per-hex cache (no second decode), one row per observer
reception, never blocks event dispatch.
- Separate `RAW_PACKET_RETENTION_DAYS` (falls back to DATA_RETENTION_DAYS);
cleanup runs regardless of capture so disabling drains the table. Raw-packet
observers retained in the is_observer recompute union.
API
- `GET /packets` and `/packets/{id}` with rich filtering, role-aware Redis
cache key, and channel-visibility redaction (restricted-channel packets are
returned metadata-only, not hidden, so pagination counts stay stable).
Web
- `FEATURE_PACKETS` flag (default off). Responsive Packets page (table desktop,
cards mobile) plus a Packet Detail page (breadcrumb nav, raw hex + decoded).
- Nav entry after Messages on all three surfaces; home.js reordered so Map
precedes Members; new packets icon + colour.
Finer-grained classification
- Replace the single `letsmesh_packet` catch-all with per-payload-type event
types (req, ack, encrypted_direct, encrypted_channel, grp_data, multipart,
control, raw_custom, ...); letsmesh_packet kept only as the unresolved-type
safety net.
Link from structured tables
- Add `packet_hash` to advertisements and messages (populated at ingest);
exact `packet_hash` filter on /packets; cube-icon link on the Adverts and
Messages lists -> /packets?packet_hash=<hash>, shown only when the feature is
on and the row has a stored hash.
Docs/config: .env.example, docker-compose (collector + web), AGENTS.md,
SCHEMAS.md, docs/letsmesh.md, docs/upgrading.md (## v0.13.0), en/nl i18n, and a
plan/tasks doc under docs/plans/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
deb5af508c
commit
76f3dfa7eb
50 files changed
+3637
-539
No files matched your search
@@ -0,0 +1,117 @@
|
||||
"""add raw_packets table
|
||||
|
||||
Revision ID: e9f0c4079540
|
||||
Revises: 20260610_1200
|
||||
Create Date: 2026-06-12 19:47:12.406423+00:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e9f0c4079540"
|
||||
down_revision: Union[str, None] = "20260610_1200"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"raw_packets",
|
||||
sa.Column("observer_node_id", sa.String(), nullable=True),
|
||||
sa.Column("packet_hash", sa.String(length=32), nullable=True),
|
||||
sa.Column("raw_hex", sa.Text(), nullable=True),
|
||||
sa.Column("packet_type", sa.Integer(), nullable=True),
|
||||
sa.Column("payload_type", sa.Integer(), nullable=True),
|
||||
sa.Column("event_type", sa.String(length=50), nullable=True),
|
||||
sa.Column("channel_idx", sa.Integer(), nullable=True),
|
||||
sa.Column("source_pubkey_prefix", sa.String(length=12), nullable=True),
|
||||
sa.Column("route_type", sa.String(length=20), nullable=True),
|
||||
sa.Column("path_len", sa.Integer(), nullable=True),
|
||||
sa.Column("snr", sa.Float(), nullable=True),
|
||||
sa.Column("decoded", sqlite.JSON(), nullable=True),
|
||||
sa.Column("received_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["observer_node_id"], ["nodes.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
with op.batch_alter_table("raw_packets", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_channel_idx"), ["channel_idx"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
"ix_raw_packets_channel_idx_received_at",
|
||||
["channel_idx", "received_at"],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_event_type"), ["event_type"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
"ix_raw_packets_event_type_received_at",
|
||||
["event_type", "received_at"],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_observer_node_id"),
|
||||
["observer_node_id"],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_packet_hash"), ["packet_hash"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_packet_type"), ["packet_type"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
"ix_raw_packets_received_at", ["received_at"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_raw_packets_source_pubkey_prefix"),
|
||||
["source_pubkey_prefix"],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
"ix_raw_packets_source_pubkey_prefix_received_at",
|
||||
["source_pubkey_prefix", "received_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("raw_packets", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_raw_packets_source_pubkey_prefix_received_at")
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_source_pubkey_prefix"))
|
||||
batch_op.drop_index("ix_raw_packets_received_at")
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_packet_type"))
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_packet_hash"))
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_observer_node_id"))
|
||||
batch_op.drop_index("ix_raw_packets_event_type_received_at")
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_event_type"))
|
||||
batch_op.drop_index("ix_raw_packets_channel_idx_received_at")
|
||||
batch_op.drop_index(batch_op.f("ix_raw_packets_channel_idx"))
|
||||
|
||||
op.drop_table("raw_packets")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,52 @@
|
||||
"""add packet_hash to messages and advertisements
|
||||
|
||||
Revision ID: e8eb47c49062
|
||||
Revises: e9f0c4079540
|
||||
Create Date: 2026-06-12 21:11:17.664733+00:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e8eb47c49062"
|
||||
down_revision: Union[str, None] = "e9f0c4079540"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("advertisements", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("packet_hash", sa.String(length=32), nullable=True)
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_advertisements_packet_hash"), ["packet_hash"], unique=False
|
||||
)
|
||||
|
||||
with op.batch_alter_table("messages", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("packet_hash", sa.String(length=32), nullable=True)
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_messages_packet_hash"), ["packet_hash"], unique=False
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("messages", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_messages_packet_hash"))
|
||||
batch_op.drop_column("packet_hash")
|
||||
|
||||
with op.batch_alter_table("advertisements", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_advertisements_packet_hash"))
|
||||
batch_op.drop_column("packet_hash")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
Reference in new issue
Block a user