Compare commits

..

5 Commits

Author SHA1 Message Date
jason 87378e49ac Merge branch 'master' of https://github.com/jasonwitty/socktop
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
2025-08-08 12:42:37 -07:00
jason 100434fc3c Major refactor, additional comments, performance improvements, idle performance improvements, access token, port specification
Release highlights

Introduced split client/agent architecture with a ratatui-based TUI and a lightweight WebSocket agent.
Added adaptive (idle-aware) sampler: agent samples fast only when clients are connected; sleeps when idle.
Implemented metrics JSON caching for instant ws replies; cold-start does one-off collection.
Port configuration: --port/-p, positional PORT, or SOCKTOP_PORT env (default 3000).
Optional token auth: SOCKTOP_TOKEN on agent, ws://HOST:PORT/ws?token=VALUE in client.
Logging via tracing with RUST_LOG control.
CI workflow (fmt, clippy, build) for Linux and Windows.
Systemd unit example for always-on agent.
TUI features

CPU: overall sparkline + per-core history with trend arrows and color thresholds.
Memory/Swap gauges with humanized labels.
Disks panel with per-device usage and icons.
Network download/upload sparklines (KB/s) with peak tracking.
Top processes table (PID, name, CPU%, mem, mem%).
Header with hostname and CPU temperature indicator.
Agent changes

sysinfo 0.36.1 targeted refresh: refresh_cpu_all, refresh_memory, refresh_processes_specifics(ProcessesToUpdate::All, ProcessRefreshKind::new().with_cpu().with_memory(), true).
WebSocket handler: client counting with wake notifications, cold-start handling, proper Response returns.
Sampler uses MissedTickBehavior::Skip to avoid catch-up bursts.
Docs

README updates: running instructions, port configuration, optional token auth, platform notes, example JSON.
Added socktop-agent.service systemd unit.
Platform notes

Linux (AMD/Intel) supported; tested on AMD, targeting Intel next.
Raspberry Pi supported (availability of temps varies by model).
Windows builds/run; CPU temperature may be unavailable (shows N/A).
Known/next

Roadmap includes configurable refresh interval, TUI filtering/sorting, TLS/WSS, and export to file.
Add Context...
README.md
2025-08-08 12:41:32 -07:00
jason deef4702f4 Update README.md 2025-08-08 01:08:02 -07:00
jason 1c2415bc1b correct readme 2025-08-08 01:06:43 -07:00
jasonwitty fac09b381f initial checkin 2025-08-08 01:03:35 -07:00
113 changed files with 3766 additions and 1899 deletions
+19
View File
@@ -0,0 +1,19 @@
name: CI
on:
push:
pull_request:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Cargo fmt
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -D warnings
- name: Build
run: cargo build --release --workspace
+1
View File
@@ -0,0 +1 @@
/target
-423
View File
@@ -1,423 +0,0 @@
# APT Repository Setup Summary
## 🎉 What You Now Have
You now have a complete system for creating and hosting your own APT repository for socktop packages, **without needing a sponsor or official Debian/Ubuntu approval**.
## 📁 Files Created
### Scripts (in `scripts/`)
- **`init-apt-repo.sh`** - Initializes the APT repository directory structure
- **`add-package-to-repo.sh`** - Adds .deb packages to the repository and generates metadata
- **`sign-apt-repo.sh`** - Signs the repository with your GPG key
- **`setup-apt-repo.sh`** - All-in-one interactive wizard to set everything up
### Documentation
- **`QUICK_START_APT_REPO.md`** - Quick start guide (< 10 minutes)
- **`docs/APT_REPOSITORY.md`** - Comprehensive 600+ line guide covering everything
- **`APT_REPO_SUMMARY.md`** - This file
### GitHub Actions
- **`.github/workflows/publish-apt-repo.yml`** - Automated building, signing, and publishing
## 🚀 Quick Start (Choose One)
### Option 1: Interactive Setup (Recommended for First Time)
Run the setup wizard:
```bash
./scripts/setup-apt-repo.sh
```
This walks you through:
1. ✅ Checking prerequisites
2. 🔑 Setting up GPG key
3. 📦 Finding/building packages
4. 📝 Creating repository structure
5. ✍️ Signing the repository
6. 📋 Next steps to publish to gh-pages
### Option 2: Manual Step-by-Step
```bash
# 1. Initialize
./scripts/init-apt-repo.sh
# 2. Build packages
cargo deb --package socktop
cargo deb --package socktop_agent
# 3. Add packages
./scripts/add-package-to-repo.sh target/debian/socktop_*.deb
./scripts/add-package-to-repo.sh target/debian/socktop-agent_*.deb
# 4. Sign (replace YOUR-KEY-ID)
./scripts/sign-apt-repo.sh apt-repo stable YOUR-KEY-ID
# 5. Update URLs
sed -i 's/YOUR-USERNAME/your-github-username/g' apt-repo/*.{md,html}
# 6. Publish to gh-pages (see below)
```
### Option 3: Fully Automated (After Initial Setup)
Once gh-pages branch exists, just tag releases:
```bash
git tag v1.50.0
git push --tags
# GitHub Actions will:
# - Build packages for AMD64 and ARM64
# - Update APT repository
# - Sign with your GPG key
# - Push to gh-pages branch automatically
```
## 📤 Publishing to GitHub Pages (gh-pages branch)
**Why gh-pages branch?**
- ✅ Keeps main branch clean (source code only)
- ✅ Separate branch for published content
- ✅ GitHub Actions can auto-update it
- ✅ You can customize the landing page
**Initial Setup:**
```bash
# Create gh-pages branch
git checkout --orphan gh-pages
git rm -rf .
# Copy apt-repo CONTENTS to root (not the folder!)
cp -r apt-repo/* .
rm -rf apt-repo
# Commit and push
git add .
git commit -m "Initialize APT repository"
git push -u origin gh-pages
# Return to main
git checkout main
```
**Enable in GitHub:**
1. Settings → Pages
2. Source: **gh-pages****/ (root)**
3. Save
Your repo will be at: `https://your-username.github.io/socktop/`
**Note:** GitHub Pages only allows `/` (root) or `/docs`. Since we use gh-pages branch, contents go in the root of that branch.
See `SETUP_GITHUB_PAGES.md` for detailed step-by-step instructions.
### Alternative: Self-Hosted Server
Copy `apt-repo/` contents to your web server:
```bash
rsync -avz apt-repo/ user@example.com:/var/www/apt/
```
Configure Apache/Nginx to serve the directory. See `docs/APT_REPOSITORY.md` for details.
## 🤖 GitHub Actions Automation
### Required Secrets
Add these in GitHub Settings → Secrets → Actions:
1. **GPG_PRIVATE_KEY**
```bash
gpg --armor --export-secret-key YOUR-KEY-ID
# Copy entire output including BEGIN/END lines
```
2. **GPG_KEY_ID**
```bash
gpg --list-secret-keys --keyid-format LONG
# Use the ID after "rsa4096/"
```
3. **GPG_PASSPHRASE**
```bash
# Your GPG passphrase (leave empty if no passphrase)
```
### Triggers
The workflow runs on:
- **Version tags**: `git tag v1.50.0 && git push --tags`
- **Manual dispatch**: Actions tab → "Publish APT Repository" → Run workflow
### What It Does
1. ✅ Builds packages for AMD64 and ARM64
2. ✅ Initializes or updates APT repository
3. ✅ Generates Packages files and metadata
4. ✅ Signs with your GPG key
5. ✅ Commits and pushes to gh-pages branch
6. ✅ Creates GitHub Release with artifacts
7. ✅ Generates summary with installation instructions
## 👥 User Installation
Once published, users install with:
```bash
# Add repository
curl -fsSL https://your-username.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://your-username.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list
# Install
sudo apt update
sudo apt install socktop socktop-agent
# The agent service is automatically installed and configured
sudo systemctl enable --now socktop-agent
```
## 🔧 Maintenance
### Release New Version (Automated)
```bash
# Update version in Cargo.toml, commit changes
git add . && git commit -m "Bump version to 1.51.0"
git tag v1.51.0
git push origin main --tags
# GitHub Actions automatically:
# - Builds packages for AMD64 and ARM64
# - Updates apt-repo
# - Signs with GPG
# - Pushes to gh-pages branch
```
### Manual Update (if needed)
```bash
# On main branch
cargo deb --package socktop
./scripts/add-package-to-repo.sh target/debian/socktop_*.deb
./scripts/sign-apt-repo.sh
# Switch to gh-pages and update
git checkout gh-pages
cp -r apt-repo/* .
git add . && git commit -m "Release v1.51.0" && git push
git checkout main
```
### Remove Old Versions
```bash
# On gh-pages branch
git checkout gh-pages
rm pool/main/socktop_1.50.0_*.deb
# Regenerate metadata (re-add remaining packages)
git add . && git commit -m "Remove old versions" && git push
git checkout main
```
## 🎯 Key Benefits
✅ **No sponsor needed** - Host your own repository
✅ **Full control** - You decide when to release
✅ **Free hosting** - GitHub Pages at no cost
✅ **Automated** - GitHub Actions does the work
✅ **Professional** - Just like official repos
✅ **Multi-arch** - AMD64, ARM64 support built-in
✅ **Secure** - GPG signed packages
✅ **Easy updates** - Users get updates via `apt upgrade`
## 📊 Repository Structure
```
apt-repo/
├── dists/
│ └── stable/
│ ├── Release # Main metadata (checksums)
│ ├── Release.gpg # Detached signature
│ ├── InRelease # Clearsigned release
│ └── main/
│ ├── binary-amd64/
│ │ ├── Packages # Package list
│ │ ├── Packages.gz # Compressed
│ │ └── Release # Component metadata
│ ├── binary-arm64/
│ └── binary-armhf/
├── pool/
│ └── main/
│ ├── socktop_1.50.0_amd64.deb
│ ├── socktop-agent_1.50.1_amd64.deb
│ ├── socktop_1.50.0_arm64.deb
│ └── socktop-agent_1.50.1_arm64.deb
├── KEY.gpg # Public GPG key
├── README.md # Repository info
├── index.html # Web interface
└── packages.html # Package listing
```
## 🔑 GPG Key Management
### Create New Key
```bash
gpg --full-generate-key
# Choose RSA 4096, no expiration (or 2 years)
```
### Export Keys
```bash
# Public key (for users)
gpg --armor --export YOUR-KEY-ID > KEY.gpg
# Private key (for GitHub Secrets)
gpg --armor --export-secret-key YOUR-KEY-ID
```
### Backup Keys
```bash
# Backup to safe location
gpg --export-secret-keys YOUR-KEY-ID > gpg-private-backup.key
gpg --export YOUR-KEY-ID > gpg-public-backup.key
```
### Key Rotation
If your key expires or is compromised:
```bash
./scripts/sign-apt-repo.sh apt-repo stable NEW-KEY-ID
gpg --armor --export NEW-KEY-ID > apt-repo/KEY.gpg
# Users need to re-import the key
```
## 🐛 Troubleshooting
### "Repository not signed"
```bash
./scripts/sign-apt-repo.sh apt-repo stable YOUR-KEY-ID
ls apt-repo/dists/stable/Release* # Should show 3 files
```
### "Package not found"
```bash
cd apt-repo
dpkg-scanpackages --arch amd64 pool/main /dev/null > dists/stable/main/binary-amd64/Packages
gzip -9 -k -f dists/stable/main/binary-amd64/Packages
cd ..
./scripts/sign-apt-repo.sh
```
### "404 Not Found" on GitHub Pages
- Wait 2-3 minutes after pushing
- Check Settings → Pages is enabled
- Verify source branch/directory
### GitHub Actions not signing
- Check all 3 secrets are set correctly
- GPG_PRIVATE_KEY must include BEGIN/END lines
- Test signing locally first
## 📚 Documentation
| File | Purpose | Length |
|------|---------|--------|
| `QUICK_START_APT_REPO.md` | Get started in < 10 minutes | Quick |
| `SETUP_GITHUB_PAGES.md` | Detailed gh-pages setup guide | Step-by-step |
| `docs/APT_REPOSITORY.md` | Complete guide with all options | Comprehensive |
| `docs/DEBIAN_PACKAGING.md` | How .deb packages are built | Technical |
| `DEBIAN_PACKAGING_SUMMARY.md` | Overview of packaging work | Summary |
| `APT_REPO_SUMMARY.md` | This file | Overview |
## 🎓 Learning Path
1. **Start here**: `QUICK_START_APT_REPO.md` (10 min)
2. **Set up**: Run `./scripts/setup-apt-repo.sh` (15 min)
3. **Publish**: Follow `SETUP_GITHUB_PAGES.md` (5 min)
4. **Automate**: Set up GitHub Actions secrets (10 min)
5. **Advanced**: Read `docs/APT_REPOSITORY.md` as needed
## 🚦 Next Steps
Choose your path:
### Just Getting Started?
1. ✅ Read `QUICK_START_APT_REPO.md`
2. ✅ Run `./scripts/setup-apt-repo.sh`
3. ✅ Follow `SETUP_GITHUB_PAGES.md` to publish
4. ✅ Test installation on a VM
### Want Automation?
1. ✅ Generate/export GPG key
2. ✅ Add GitHub Secrets
3. ✅ Tag a release: `git tag v1.50.0 && git push --tags`
4. ✅ Watch GitHub Actions magic happen
### Want to Understand Everything?
1. ✅ Read `docs/APT_REPOSITORY.md` (comprehensive)
2. ✅ Study the scripts in `scripts/`
3. ✅ Examine `.github/workflows/publish-apt-repo.yml`
4. ✅ Learn about Debian repository format
### Ready for Production?
1. ✅ Set up monitoring/analytics
2. ✅ Create PPA for Ubuntu (Launchpad)
3. ✅ Apply to Debian mentors for official inclusion
4. ✅ Set up repository mirrors
5. ✅ Document best practices for users
## 🌟 Success Criteria
You'll know you're successful when:
- [ ] Users can `apt install socktop`
- [ ] Updates work with `apt upgrade`
- [ ] Multiple architectures supported
- [ ] Repository is GPG signed
- [ ] GitHub Actions publishes automatically
- [ ] Installation instructions in README
- [ ] Zero sponsor or approval needed
## 💡 Pro Tips
1. **Test first**: Always test on a fresh VM before publishing
2. **Keep versions**: Don't delete old .deb files immediately
3. **Backup GPG key**: Store it safely offline
4. **Monitor downloads**: Use GitHub Insights or server logs
5. **Document everything**: Help users troubleshoot
6. **Version consistently**: Use semantic versioning
7. **Sign always**: Never publish unsigned repositories
## 🔗 Resources
- [Debian Repository Format](https://wiki.debian.org/DebianRepository/Format)
- [GitHub Pages Docs](https://docs.github.com/en/pages)
- [cargo-deb](https://github.com/kornelski/cargo-deb)
- [Ubuntu PPA Guide](https://help.launchpad.net/Packaging/PPA)
- [Debian Mentors](https://mentors.debian.net/)
## 🎊 Congratulations!
You now have everything you need to:
- ✅ Create your own APT repository
- ✅ Host it for free on GitHub Pages
- ✅ Automate the entire process
- ✅ Distribute packages professionally
- ✅ Provide easy installation for users
**No sponsor required. No approval needed. You're in control!** 🚀
---
**Questions?** Check the docs or open an issue.
**Ready to publish?** Run `./scripts/setup-apt-repo.sh` and follow the wizard!
Generated
+2254
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
[workspace]
members = [
"socktop",
"socktop_agent"
]
[workspace.dependencies]
# async + streams
tokio = { version = "1", features = ["full"] }
futures = "0.3"
futures-util = "0.3"
anyhow = "1.0"
# websocket
tokio-tungstenite = "0.24"
tungstenite = "0.24"
url = "2.5"
# JSON + error handling
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
# system stats
sysinfo = "0.32"
# CLI UI
ratatui = "0.28"
crossterm = "0.27"
# date/time
chrono = { version = "0.4", features = ["serde"] }
# web server (remote-agent)
axum = { version = "0.7", features = ["ws"] }
-109
View File
@@ -1,109 +0,0 @@
# 🚀 Get Your APT Repository Live in 5 Minutes
## You're Here Because...
You want to publish socktop packages via APT, but GitHub Pages won't let you select `apt-repo/` folder. Here's why and how to fix it:
**The Issue:** GitHub Pages only serves from `/` (root) or `/docs`, not custom folders like `/apt-repo`.
**The Solution:** Use a `gh-pages` branch where `apt-repo` contents go in the root.
## Quick Setup (5 Steps)
### 1. Create apt-repo locally (if you haven't)
```bash
./scripts/setup-apt-repo.sh
```
This creates `apt-repo/` with your packages and signs them.
### 2. Create gh-pages branch
```bash
git checkout --orphan gh-pages
git rm -rf .
```
### 3. Copy apt-repo to root
```bash
cp -r apt-repo/* .
rm -rf apt-repo
ls
# You should see: dists/ pool/ KEY.gpg index.html README.md
```
### 4. Push to GitHub
```bash
git add .
git commit -m "Initialize APT repository"
git push -u origin gh-pages
git checkout main
```
### 5. Enable GitHub Pages
1. Go to: **Settings → Pages**
2. Source: **gh-pages****/ (root)**
3. Click **Save**
**Done!** ✅ Your repo will be live at `https://your-username.github.io/socktop/` in 1-2 minutes.
## Test It
```bash
curl -I https://your-username.github.io/socktop/KEY.gpg
# Should return: HTTP/2 200
```
## Install It (On Any Debian/Ubuntu System)
```bash
curl -fsSL https://your-username.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://your-username.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list
sudo apt update
sudo apt install socktop socktop-agent
```
## What's Next?
### Now (Optional):
- Customize `index.html` on gh-pages for a nice landing page
- Add installation instructions to your main README
### Later:
- Set up GitHub Actions automation (see `QUICK_START_APT_REPO.md`)
- Add more architectures (ARM64, ARMv7)
## Understanding the Setup
```
main branch: gh-pages branch:
├── src/ ├── dists/
├── Cargo.toml ├── pool/
├── scripts/ ├── KEY.gpg
└── apt-repo/ (local) └── index.html ← GitHub Pages serves this
Work here ↑ Published here ↑
```
- **main**: Your development work
- **gh-pages**: What users see/download
- **apt-repo/**: Local folder (ignored in git, see `.gitignore`)
## Need More Help?
- **Quick start**: `QUICK_START_APT_REPO.md`
- **Detailed setup**: `SETUP_GITHUB_PAGES.md`
- **Why gh-pages?**: `WHY_GHPAGES_BRANCH.md`
- **Full guide**: `docs/APT_REPOSITORY.md`
---
**You got this!** 🎉
-42
View File
@@ -1,42 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGkih7QBDADgX6sYMx2Lp6qcZxeCCizcy4TFsxcRJfp5mfbMplVES0hQToIP
EMC11JqPwQdLliXKjUr8Z2kgM2oqvH+dkdgzUGrw6kTK8YHc+qs37iJAOVS9D72X
tTld282NrtFwzb74nS2GKPkpWI7aSKBpHtWFPX/1ONsc56qGqFd3wwikEvCz8MeJ
HwCD1JZ9F+2DyyXWsTJNgDwPloJSUbtyVuk2gd6PeTg7AQdx92Pk/mggmYbHtP8N
wy072ku1g8K/hplmwIOGpSx1JWvAQkDU/Bb/jSqrYg2wSHO7IQnYE8I3x/zglYBl
FYNh47TVQr0zPVSYR1MQkHU5YLBTDc5UgDvtcsYUiTtq4D/m8HWmKja0/UKGxvDJ
P5sUPcp4dk77RdoCtUe5HImYGS8lo5N3+t0lz8sd9rYmRiIO4f7FJaJqJeHbUJyn
iw/GCQh5D5/D571dICrEq/QhL+k5KhJljPGoVMGPFXJIc7q+CxvGp2oOo5fOlbOn
3kSrM93AJPwT8FMAEQEAAbRFSmFzb24gV2l0dHkgKHNvY2t0b3AgYXB0IHNpZ25p
bmcga2V5KSA8amFzb25wd2l0dHkrc29ja3RvcEBwcm90b24ubWU+iQHOBBMBCgA4
FiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmkih7QCGwMFCwkIBwIGFQoJCAsCBBYC
AwECHgECF4AACgkQESwaeYRl+/KV+gwAzfZVZEhO7MQV2EmNeKVK1GycFSm2oUAl
ZbwNIEHu6+tOzqXJb8o65BtGlbLSGavsMpgRCK2SL83DdLOkutG1ahQiJr+5GaXC
zbQgX+VWqGPZtQ+I6/rVoYZPMTCrqpAmFgvVpqv0xod7w8/wny8/XmhQ37KY2/0l
B38oNTvdA7C8jzSrI6kr3XqurvQRW7z+MnC+nCp9Ob9bYtY0kpd4U3NrVdb8m32U
d5LVFwD1OGvzLOSqyJ33IKjSJc4KLvW+aEsHXe+fHO9UEzH8Nbo5MmVvX3QIHiyq
jD4zN16AGsGYqCK4irtQCiD3wBOdsG/RVkgIcdlmAH3EGEp7Ux8+7v1PXYI+UrSs
XE7f1xFTJ2r5TMex6W3he073Em4qhQsrnMF5syTZsM6N+5UqXVOM1RuDVVXr7929
hC3G8pK/A2W5Lwpxl2yzock2CxhvUn7M/xm4VbcPlWTCUd/QzU8VtsgaGHcuhi5e
xHY1AU07STLB9RinjBVf2bmk4oDQcmB6uQGNBGkih7QBDACrjE+xSWP92n931/5t
+tXcujwFlIpSZdbSQFr0B0YyjPRUP4FSzEGu8vuM5ChUfWKhmN1dDr5C4qFo9NgQ
6oCN2HubajSGyXNwnOMlMb5ck79Ubmy9yDV9/ZLqpJJiozGap2/EnNoDhaANlmUg
rfqUHpIB8XC2IZ0Itt05tp/u78dJiB+R6ReZn/bVUafNV4jIqYZfLRzI3FTJ4xvK
FGs/ER+JajAdJQ8LPfazmDQSGw0huguxhopZwKQ/qWZMn1OHq/ZaPvCqbQt3irLw
dLPDC4pEaYGRyADYeyuarG0DVyUQ9XRc/NufKDvOAn33LpBPBpcvNQAsVhWTCYl7
ogQ+suVYVN8Tu7v4bUSHKwzXKvLN/ojJX/Fh7eTW4TPsgLHNHAEDUkSQozIe9vO6
o+vydDqRxuXJgdkR7lqP6PQDYrhRYZGJf57eKf6VtTKYFaMbiMWPU+vcHeB0/iDe
Pv81qro2LD2PG5WCzDpNETBceCTjykb9r0VHx4/JsiojKmsAEQEAAYkBtgQYAQoA
IBYhBB51VqgFObg5S8KCDREsGnmEZfvyBQJpIoe0AhsMAAoJEBEsGnmEZfvyNp8M
AIH+6+hGB3qADdnhNgb+3fN0511eK9Uk82lxgGARLcD8GN1UP0HlvEqkxCHy3PUe
tHcsuYVz7i8pmpEGdFx9zv7MelenUsJniUQ++OZKx6iUG/MYqz//NxY+5lyRmcu2
aYvUxhkgf9zgxXTkTyV2VV32mX//cHcwc+c/089QAPzCMaSrHdNK+ED9+k8uquJ1
lSL9Bm15z/EV42v9Q/4KTM5OBLHpNw0Rvn9C0iuZVwHXBrrA/HSGXpA54AqNUMpZ
kRPgLQcy5yVE2y1aXLXt2XdTn6YPzrAjNoazYYuCWHYIZU7dGkIswpsDirDLKHdD
onb3VShmSpemYjsuFiqhfi6qwCkeHsz/CpQAp70SZ+z9oB8H80PJVKPbPIP3zEf3
i7bcsqHA7stF+8sJclXgxBUBeDJ3O2jN/scBOcvNA6xoRp7+oJbnjDRuxBmh+fVg
TIuw2++vTF2Ml0EMv7ePTpr7b1DofuJRNYGkuAIMVXHjLTqMiTJUce3OUy003zMg
Dg==
=AaPQ
-----END PGP PUBLIC KEY BLOCK-----
-221
View File
@@ -1,221 +0,0 @@
# Quick Start: Setting Up Your socktop APT Repository
This guide will get your APT repository up and running in **under 10 minutes**.
## Prerequisites
- [ ] Debian packages built (or use GitHub Actions to build them)
- [ ] GPG key for signing
- [ ] GitHub repository with Pages enabled
## Step 1: Create GPG Key (if needed)
```bash
# Generate a new key
gpg --full-generate-key
# Select:
# - RSA and RSA (default)
# - 4096 bits
# - Key does not expire (or 2 years)
# - Your name and email
# Get your key ID
gpg --list-secret-keys --keyid-format LONG
# Look for the part after "rsa4096/" - that's your KEY-ID
```
## Step 2: Initialize Repository Locally
```bash
cd socktop
# Create the repository structure
./scripts/init-apt-repo.sh
# Build packages (or download from GitHub Actions)
cargo install cargo-deb
cargo deb --package socktop
cargo deb --package socktop_agent
# Add packages to repository
./scripts/add-package-to-repo.sh target/debian/socktop_*.deb
./scripts/add-package-to-repo.sh target/debian/socktop-agent_*.deb
# Sign the repository (replace YOUR-KEY-ID with actual key ID)
./scripts/sign-apt-repo.sh apt-repo stable YOUR-KEY-ID
# Update URLs with your GitHub username
sed -i 's/YOUR-USERNAME/your-github-username/g' apt-repo/README.md apt-repo/index.html
```
## Step 3: Publish to GitHub Pages (gh-pages branch)
```bash
# Create gh-pages branch
git checkout --orphan gh-pages
git rm -rf .
# Copy apt-repo CONTENTS to root (not the folder itself)
cp -r apt-repo/* .
rm -rf apt-repo
# Commit and push
git add .
git commit -m "Initialize APT repository"
git push -u origin gh-pages
# Go back to main branch
git checkout main
```
Then in GitHub:
1. Go to **Settings → Pages**
2. Source: **Deploy from a branch**
3. Branch: **gh-pages****/ (root)** → **Save**
Wait 1-2 minutes, then visit: `https://your-username.github.io/socktop/`
## Step 4: Automate with GitHub Actions
Add these secrets to your repository (Settings → Secrets → Actions):
```bash
# Export your private key
gpg --armor --export-secret-key YOUR-KEY-ID
# Copy the ENTIRE output and save as secret: GPG_PRIVATE_KEY
```
Add these three secrets:
- **GPG_PRIVATE_KEY**: Your exported private key
- **GPG_KEY_ID**: Your key ID (e.g., `ABC123DEF456`)
- **GPG_PASSPHRASE**: Your key passphrase (leave empty if no passphrase)
The workflow in `.github/workflows/publish-apt-repo.yml` will now:
- Build packages for AMD64 and ARM64
- Update the APT repository
- Sign with your GPG key
- Push to gh-pages automatically
Trigger it by:
- Creating a version tag: `git tag v1.50.0 && git push --tags`
- Manual dispatch from GitHub Actions tab
## Step 5: Test It
On any Debian/Ubuntu system:
```bash
# Add your repository
curl -fsSL https://your-username.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://your-username.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list
# Install
sudo apt update
sudo apt install socktop socktop-agent
# Verify
socktop --version
socktop_agent --version
```
## Maintenance
### Add a New Version
```bash
# Build new packages
cargo deb --package socktop
cargo deb --package socktop_agent
# Add to repository
./scripts/add-package-to-repo.sh target/debian/socktop_*.deb
./scripts/add-package-to-repo.sh target/debian/socktop-agent_*.deb
# Re-sign
./scripts/sign-apt-repo.sh apt-repo stable YOUR-KEY-ID
# Publish
cd docs/apt # or wherever your apt-repo is
git add .
git commit -m "Release v1.51.0"
git push origin main
```
### Or Just Tag and Let GitHub Actions Do It
```bash
# Update version in Cargo.toml
# Commit changes
git add .
git commit -m "Bump version to 1.51.0"
# Tag and push
git tag v1.51.0
git push origin main --tags
# GitHub Actions will:
# - Build packages for AMD64 and ARM64
# - Update gh-pages branch automatically
# - Sign and publish!
```
## Troubleshooting
### "Repository not signed" error
Make sure you signed it:
```bash
./scripts/sign-apt-repo.sh apt-repo stable YOUR-KEY-ID
ls apt-repo/dists/stable/Release*
# Should show: Release, Release.gpg, InRelease, KEY.gpg
```
### "404 Not Found" on GitHub Pages
1. Check Settings → Pages is enabled
2. Wait 2-3 minutes for GitHub to deploy
3. Verify the URL structure matches your settings
### GitHub Actions not signing
Check that all three secrets are set correctly:
- Settings → Secrets and variables → Actions
- Make sure GPG_PRIVATE_KEY includes the BEGIN/END lines
- Test locally first
## What's Next?
✅ You now have a working APT repository!
**Share it:**
- Add installation instructions to your main README
- Tweet/blog about it
- Submit to awesome-rust lists
**Improve it:**
- Customize your GitHub Pages site (it's just HTML!)
- Add more architectures (ARMv7)
- Create multiple distributions (stable, testing)
- Set up download statistics
- Apply to Ubuntu PPA (Launchpad)
- Eventually submit to official Debian repos
## Full Documentation
For detailed information, see:
- `docs/APT_REPOSITORY.md` - Complete APT repository guide
- `docs/DEBIAN_PACKAGING.md` - Debian packaging details
- `DEBIAN_PACKAGING_SUMMARY.md` - Quick summary
## Questions?
Open an issue on GitHub or check the full documentation.
---
**Happy packaging! 📦**
+232 -22
View File
@@ -1,38 +1,248 @@
# socktop APT Repository
# socktop
This repository contains Debian packages for socktop and socktop-agent.
**socktop** is a remote system monitor with a rich TUI interface, inspired by `top` and `btop`, that communicates with a lightweight remote agent over WebSockets.
## Adding this repository
It lets you watch CPU, memory, disks, network, temperatures, and processes on another machine in real-time — from the comfort of your terminal.
Add the repository to your system:
![socktop screenshot](./docs/socktop-screenshot.png)
```bash
# Add the GPG key
curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
---
# Add the repository
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" | sudo tee /etc/apt/sources.list.d/socktop.list
## Features
# Update and install
sudo apt update
sudo apt install socktop socktop-agent
- 📡 **Remote monitoring** via WebSocket — lightweight agent sends JSON metrics
- 🖥 **Rich TUI** built with [ratatui](https://github.com/ratatui-org/ratatui)
- 🔍 **Detailed CPU view** — per-core history, current load, and trends
- 📊 **Memory, Swap, Disk usage** — human-readable units, color-coded
- 🌡 **Temperatures** — CPU temperature with visual indicators
- 📈 **Network throughput** — live sparkline graphs with peak tracking
- 🏷 **Top processes table** — PID, name, CPU%, memory, and memory%
- 🎨 Color-coded load, zebra striping for readability
-**Keyboard shortcuts**:
- `q` / `Esc` → Quit
---
## Architecture
`socktop` has **two components**:
1. **Agent** (remote side)
A small Rust WebSocket server that runs on the target machine and gathers metrics via [sysinfo](https://crates.io/crates/sysinfo).
2. **Client** (local side)
The TUI app (`socktop`) that connects to the agents `/ws` endpoint, receives JSON metrics, and renders them.
The two communicate over a persistent WebSocket connection.
---
## Adaptive (idle-aware) sampling
The socktop agent now samples system metrics only when at least one WebSocket client is connected. When idle (no clients), the sampler sleeps and CPU usage drops to ~0%.
How it works
- The WebSocket handler increments/decrements a client counter in `AppState` on connect/disconnect.
- A background sampler wakes when the counter transitions from 0 → >0 and sleeps when it returns to 0.
- The most recent metrics snapshot is cached as JSON for fast responses.
Cold start behavior
- If a client requests metrics while the cache is empty (e.g., just started or after a long idle), the agent performs a one-off synchronous collection to respond immediately.
Tuning
- Sampling interval (active): update `spawn_sampler(state, Duration::from_millis(500))` in `socktop_agent/src/main.rs`.
- Always-on or low-frequency idle sampling: replace the “sleep when idle” logic in `socktop_agent/src/sampler.rs` with a low-frequency interval. Example sketch:
```rust
// In sampler.rs (sketch): sample every 10s when idle, 500ms when active
let idle_period = Duration::from_secs(10);
loop {
let active = state.client_count.load(Ordering::Relaxed) > 0;
let period = if active { Duration::from_millis(500) } else { idle_period };
let mut ticker = tokio::time::interval(period);
ticker.tick().await;
if !active {
// wake early if a client connects
tokio::select! {
_ = ticker.tick() => {},
_ = state.wake_sampler.notified() => continue,
}
}
let m = collect_metrics(&state).await;
if let Ok(js) = serde_json::to_string(&m) {
*state.last_json.write().await = js;
}
}
```
## Manual Installation
---
You can also download and install packages manually from the `pool/main/` directory.
## Installation
### Prerequisites
- Rust 1.75+ (recommended latest stable)
- Cargo package manager
### Build from source
```bash
wget https://jasonwitty.github.io/socktop/pool/main/socktop_VERSION_ARCH.deb
sudo dpkg -i socktop_VERSION_ARCH.deb
git clone https://github.com/YOURNAME/socktop.git
cd socktop
cargo build --release
```
## Supported Architectures
### Install as a cargo binary
```bash
cargo install --path .
```
This will install the `socktop` binary into `~/.cargo/bin`.
- amd64 (x86_64)
- arm64 (aarch64)
- armhf (32-bit ARM)
---
## Building from Source
## Running
See the main repository at https://github.com/jasonwitty/socktop
### 1. Start the agent on the remote machine
The agent binary listens on a TCP port and serves `/ws`:
```bash
remote_agent 0.0.0.0:8080
```
> **Tip:** You can run the agent under `systemd`, inside a Docker container, or just in a tmux/screen session.
### 2. Connect with the client
From your local machine:
```bash
socktop ws://REMOTE_HOST:8080/ws
```
Example:
```bash
socktop ws://192.168.1.50:8080/ws
```
---
## Usage
When connected, `socktop` displays:
**Left column:**
- **CPU avg graph** — sparkline of recent overall CPU usage
- **Memory gauge** — total and used RAM
- **Swap gauge** — total and used swap
- **Disks** — usage per device (only devices with available space > 0)
- **Network Download/Upload** — sparkline in KB/s, with current & peak values
**Right column:**
- **Per-core history & trends** — each cores recent load, current %, and trend arrow
- **Top processes table** — top 20 processes with PID, name, CPU%, memory usage, and memory%
---
## Configuring the agent port
The agent listens on TCP port 3000 by default. You can override this via a CLI flag, a positional port argument, or an environment variable:
- CLI flag:
- socktop_agent --port 8080
- socktop_agent -p 8080
- Positional:
- socktop_agent 8080
- Environment variable:
- SOCKTOP_PORT=8080 socktop_agent
Help:
- socktop_agent --help
The TUI should point to ws://HOST:PORT/ws, e.g.:
- cargo run -p socktop -- ws://127.0.0.1:8080/ws
---
## Keyboard Shortcuts
| Key | Action |
|-------------|------------|
| `q` or `Esc`| Quit |
---
## Security (optional token)
By default, the agent exposes metrics over an unauthenticated WebSocket. For untrusted networks, set an auth token and pass it in the client URL:
- Server:
- SOCKTOP_TOKEN=changeme socktop_agent --port 3000
- Client:
- socktop ws://HOST:3000/ws?token=changeme
---
## Platform notes
- Linux x86_64/AMD/Intel: fully supported.
- Raspberry Pi:
- 64-bit: rustup target add aarch64-unknown-linux-gnu; build on-device for simplicity.
- 32-bit: rustup target add armv7-unknown-linux-gnueabihf.
- Windows:
- TUI and agent build/run with stable Rust. Use PowerShell:
- cargo run -p socktop_agent -- --port 3000
- cargo run -p socktop -- ws://127.0.0.1:3000/ws
- CPU temperature may be unavailable; display will show N/A.
---
## Example agent JSON
`socktop` expects the agent to send metrics in this shape:
```json
{
"cpu_total": 12.4,
"cpu_per_core": [11.2, 15.7, ...],
"mem_total": 33554432,
"mem_used": 18321408,
"swap_total": 0,
"swap_used": 0,
"process_count": 127,
"hostname": "myserver",
"cpu_temp_c": 42.5,
"disks": [{"name":"nvme0n1p2","total":512000000000,"available":320000000000}],
"networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
"top_processes": [
{"pid":1234,"name":"nginx","cpu_usage":1.2,"mem_bytes":12345678}
]
}
```
---
## Development
### Run in debug mode:
```bash
cargo run -- ws://127.0.0.1:8080/ws
```
### Code formatting & lint:
```bash
cargo fmt
cargo clippy
```
---
## Roadmap
- [ ] Configurable refresh interval
- [ ] Filter/sort top processes in the TUI
- [ ] Export metrics to file
- [ ] TLS / WSS support
- [ ] Agent authentication
---
## License
MIT License — see [LICENSE](LICENSE).
---
## Acknowledgements
- [`ratatui`](https://github.com/ratatui-org/ratatui) for terminal UI rendering
- [`sysinfo`](https://crates.io/crates/sysinfo) for system metrics
- [`tokio-tungstenite`](https://crates.io/crates/tokio-tungstenite) for WebSocket client/server
-351
View File
@@ -1,351 +0,0 @@
# Setting Up GitHub Pages for socktop APT Repository
This guide walks you through the initial setup of your APT repository on GitHub Pages using the `gh-pages` branch.
## Prerequisites
- [ ] You've run `./scripts/setup-apt-repo.sh` or manually created `apt-repo/`
- [ ] `apt-repo/` contains signed packages and metadata
- [ ] You have a GitHub repository for socktop
## Step-by-Step Setup
### 1. Verify Your Local Repository
First, make sure everything is ready:
```bash
# Check that apt-repo exists and has content
ls -la apt-repo/
# You should see:
# - dists/stable/Release, Release.gpg, InRelease
# - pool/main/*.deb
# - KEY.gpg
# - index.html, README.md
```
### 2. Create and Switch to gh-pages Branch
```bash
# Create a new orphan branch (no history from main)
git checkout --orphan gh-pages
# Remove all files from staging
git rm -rf .
```
**Important:** This creates a completely separate branch. Don't worry - your main branch is safe!
### 3. Copy APT Repository to Root
```bash
# Copy CONTENTS of apt-repo to root of gh-pages
cp -r apt-repo/* .
# Remove the apt-repo directory itself
rm -rf apt-repo
# Verify the structure
ls -la
# You should see in the current directory:
# - dists/
# - pool/
# - KEY.gpg
# - index.html
# - README.md
```
**Why root?** GitHub Pages can only serve from:
- `/` (root) - what we're doing
- `/docs` directory
- NOT from custom directories like `/apt-repo`
### 4. Commit and Push
```bash
# Add all files
git add .
# Commit
git commit -m "Initialize APT repository for GitHub Pages"
# Push to gh-pages branch
git push -u origin gh-pages
```
### 5. Return to Main Branch
```bash
# Switch back to your main development branch
git checkout main
# Verify you're back on main
git branch
# Should show: * main
```
### 6. Enable GitHub Pages
1. Go to your repository on GitHub
2. Click **Settings** (top right)
3. Click **Pages** (left sidebar)
4. Under "Build and deployment":
- Source: **Deploy from a branch**
- Branch: **gh-pages**
- Folder: **/ (root)**
- Click **Save**
### 7. Wait for Deployment
GitHub will deploy your site. This usually takes 1-2 minutes.
You can watch the progress:
- Go to **Actions** tab
- Look for "pages build and deployment" workflow
### 8. Verify Your Repository is Live
Once deployed, your repository will be at:
```
https://YOUR-USERNAME.github.io/socktop/
```
Test it:
```bash
# Check the public key is accessible
curl -I https://YOUR-USERNAME.github.io/socktop/KEY.gpg
# Should return: HTTP/2 200
# Check the Release file
curl -I https://YOUR-USERNAME.github.io/socktop/dists/stable/Release
# Should return: HTTP/2 200
```
### 9. Test Installation (Optional but Recommended)
On a Debian/Ubuntu VM or system:
```bash
# Add GPG key
curl -fsSL https://YOUR-USERNAME.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
# Add repository
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://YOUR-USERNAME.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list
# Update package lists
sudo apt update
# You should see:
# Get:1 https://YOUR-USERNAME.github.io/socktop stable InRelease [xxx B]
# Install packages
sudo apt install socktop socktop-agent
# Verify
socktop --version
```
## Understanding the Two Branches
After setup, you'll have two branches:
### `main` branch (development)
```
main/
├── src/
├── Cargo.toml
├── scripts/
├── docs/
├── apt-repo/ ← Local build artifact (not published)
└── ...
```
**Purpose:** Source code, development, building packages
### `gh-pages` branch (published)
```
gh-pages/
├── dists/
├── pool/
├── KEY.gpg
├── index.html ← Customize this for a nice landing page!
└── README.md
```
**Purpose:** Published APT repository served by GitHub Pages
## Workflow Going Forward
### Manual Updates
When you release a new version:
```bash
# 1. On main branch, build new packages
git checkout main
cargo deb --package socktop
cargo deb --package socktop_agent
# 2. Update local apt-repo
./scripts/add-package-to-repo.sh target/debian/socktop_*.deb
./scripts/add-package-to-repo.sh target/debian/socktop-agent_*.deb
./scripts/sign-apt-repo.sh apt-repo stable YOUR-GPG-KEY-ID
# 3. Switch to gh-pages and update
git checkout gh-pages
cp -r apt-repo/* .
git add .
git commit -m "Release v1.51.0"
git push origin gh-pages
# 4. Return to main
git checkout main
```
### Automated Updates (Recommended)
Set up GitHub Actions to do this automatically:
1. Add GitHub Secrets (Settings → Secrets → Actions):
- `GPG_PRIVATE_KEY` - Your exported private key
- `GPG_KEY_ID` - Your GPG key ID
- `GPG_PASSPHRASE` - Your GPG passphrase (if any)
2. Tag and push:
```bash
git tag v1.51.0
git push origin main --tags
```
3. GitHub Actions will automatically:
- Build packages for AMD64 and ARM64
- Update apt-repo
- Sign with your GPG key
- Push to gh-pages
- Create GitHub Release
See `.github/workflows/publish-apt-repo.yml` for details.
## Customizing Your GitHub Pages Site
The `gh-pages` branch contains `index.html` which users see when they visit:
`https://YOUR-USERNAME.github.io/socktop/`
You can customize this! On the `gh-pages` branch:
```bash
git checkout gh-pages
# Edit index.html
nano index.html
# Add features, badges, screenshots, etc.
git add index.html
git commit -m "Improve landing page"
git push origin gh-pages
git checkout main
```
## Troubleshooting
### "404 Not Found" on GitHub Pages
**Check:**
- Settings → Pages shows "Your site is live at..."
- Wait 2-3 minutes after pushing
- Verify branch is `gh-pages` and folder is `/`
- Check Actions tab for deployment errors
### "Repository not found" when installing
**Check:**
- URL is correct: `https://USERNAME.github.io/REPO/` (no trailing /apt-repo)
- Files exist at the URLs:
```bash
curl -I https://USERNAME.github.io/REPO/dists/stable/InRelease
curl -I https://USERNAME.github.io/REPO/KEY.gpg
```
### "GPG error" when installing
**Check:**
- Repository is signed: `ls gh-pages/dists/stable/Release.gpg`
- Users imported the key: `curl https://USERNAME.github.io/REPO/KEY.gpg | gpg --import`
### Changes not appearing
**Check:**
- You committed and pushed to `gh-pages` (not `main`)
- Wait 1-2 minutes for GitHub to redeploy
- Clear browser cache if viewing index.html
- For apt: `sudo apt clean && sudo apt update`
## Success Checklist
After completing this guide, you should have:
- [ ] `gh-pages` branch created and pushed
- [ ] GitHub Pages enabled and deployed
- [ ] Site accessible at `https://USERNAME.github.io/socktop/`
- [ ] `KEY.gpg` downloadable
- [ ] `dists/stable/InRelease` accessible
- [ ] Packages in `pool/main/*.deb` downloadable
- [ ] Successfully tested installation on a test system
- [ ] Understand the workflow for future updates
## Next Steps
1. **Update your main README.md** with installation instructions
2. **Set up GitHub Actions** for automated releases
3. **Customize index.html** on gh-pages for a nice landing page
4. **Test on multiple architectures** (AMD64, ARM64)
5. **Share your repository** with users
## Quick Reference
**Switch branches:**
```bash
git checkout main # Development
git checkout gh-pages # Published site
```
**Update published site manually:**
```bash
git checkout main
# ... build packages, update apt-repo ...
git checkout gh-pages
cp -r apt-repo/* .
git add . && git commit -m "Update" && git push
git checkout main
```
**Your repository URL:**
```
https://YOUR-USERNAME.github.io/socktop/
```
**User installation command:**
```bash
curl -fsSL https://YOUR-USERNAME.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://YOUR-USERNAME.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list
sudo apt update && sudo apt install socktop socktop-agent
```
---
**Need help?** See:
- `QUICK_START_APT_REPO.md` - Overall quick start
- `docs/APT_REPOSITORY.md` - Comprehensive guide
- `docs/APT_WORKFLOW.md` - Visual workflow diagrams
-119
View File
@@ -1,119 +0,0 @@
# Why We Use the gh-pages Branch
## The Problem
GitHub Pages has a limitation - it can only serve static sites from:
1. **`/` (root)** of a branch
2. **`/docs`** directory of a branch
3. **NOT** from custom directories like `/apt-repo`
## Why Not `/docs`?
When you tried to enable GitHub Pages with `apt-repo/` checked into main, you couldn't select it because:
```
main/
├── src/
├── Cargo.toml
├── apt-repo/ ← GitHub Pages can't serve from here!
└── ...
```
You could move it to `/docs`:
```
main/
├── src/
├── Cargo.toml
├── docs/ ← GitHub Pages CAN serve from here
│ ├── dists/
│ ├── pool/
│ └── ...
└── ...
```
But this has downsides:
- ❌ Mixed source code and published content
- ❌ Large .deb files bloat the main branch
- ❌ Can't easily customize the site without affecting source
- ❌ Messy git history with binary files
## Why gh-pages Branch (Our Solution)
Using a separate `gh-pages` branch is cleaner:
```
main branch (source code):
├── src/
├── Cargo.toml
├── scripts/
└── docs/ ← Documentation source
gh-pages branch (published):
├── dists/
├── pool/
├── KEY.gpg
├── index.html ← Customizable landing page
└── README.md
```
### Benefits
**Clean separation**: Source code stays in `main`, published content in `gh-pages`
**No binary bloat**: .deb files don't clutter your main branch history
**Easy automation**: GitHub Actions can push to gh-pages without affecting main
**Customizable**: You can make a beautiful landing page on gh-pages
**Standard practice**: Most GitHub Pages projects use gh-pages branch
**Root URL**: Your repo is at `https://username.github.io/socktop/` (not `/apt-repo`)
### Workflow
```
Developer (main branch)
Build packages
Update apt-repo/ (local)
Push to gh-pages branch
GitHub Pages serves
Users: apt install socktop
```
## The Setup
**One-time setup:**
```bash
git checkout --orphan gh-pages
git rm -rf .
cp -r apt-repo/* .
rm -rf apt-repo
git add . && git commit -m "Initialize APT repository"
git push -u origin gh-pages
git checkout main
```
**Going forward:**
- Work on `main` branch for development
- `gh-pages` branch gets updated by GitHub Actions (or manually)
- Never need to switch branches manually after automation is set up!
## Comparison
| Approach | Location | URL | Pros | Cons |
|----------|----------|-----|------|------|
| **gh-pages branch** ✅ | gh-pages:/ | `username.github.io/socktop/` | Clean, automated, customizable | Two branches |
| `/docs` on main | main:/docs | `username.github.io/socktop/` | One branch | Mixed content, binary bloat |
| `/apt-repo` on main | main:/apt-repo | ❌ Not possible | - | GitHub Pages won't allow it |
## Conclusion
The `gh-pages` branch approach is:
- The **cleanest** solution
- The **most flexible** for customization
- The **easiest to automate**
- **Industry standard** for GitHub Pages
That's why we chose it! 🚀
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-43
View File
@@ -1,43 +0,0 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Origin: socktop
Label: socktop
Suite: stable
Codename: stable
Architectures: amd64 arm64 armhf riscv64
Components: main
Description: socktop APT repository
Date: Mon, 24 Aug 2026 14:30:55 +0000
MD5Sum:
ada07cbd3ac088cecb5dc9d08518823d 1627 main/binary-amd64/Packages
fbb8924917960d65b5bcad003ccb90c2 830 main/binary-amd64/Packages.gz
71b8d2e69d19d4db385bc3f2be603b72 1627 main/binary-arm64/Packages
70831c731ff47542094fe3417e385aa6 830 main/binary-arm64/Packages.gz
129d1b34bf48c75861a188d6ae8fba77 1611 main/binary-armhf/Packages
54e691233b47bb1f0d89f82b00d9403f 814 main/binary-armhf/Packages.gz
4c3fc0b399405dd682dac1318adda05f 1623 main/binary-riscv64/Packages
15bcbe1806bf2577849090556c3a2a81 817 main/binary-riscv64/Packages.gz
SHA256:
88a907a84f4d292e34426d71e328561aba9658440e588f62f0f3b5d7f5c5f121 1627 main/binary-amd64/Packages
3a87b0b38cb79d231b8ed66e1fdc9a54369d0d3f2f0686f6513cece15f0dc967 830 main/binary-amd64/Packages.gz
84a43993bf58648174ef5bf1d4542e96f5d7903acedc0405f8f3aeab4fdbb4e7 1627 main/binary-arm64/Packages
fd8e6a7ec2b7c438659b0fe8fa825670ccfbeb2298c01b55020cbdad2acb3313 830 main/binary-arm64/Packages.gz
63a89cdee8e5c93867b2dd5193df162304ea97855ad68135c5c5d0753700102c 1611 main/binary-armhf/Packages
c81f19a798757bd1806bb860aba238757edf1b902338554e30bcddba86b3c02b 814 main/binary-armhf/Packages.gz
fa590edf01fbdd63817bd1cee4ab49d93bd47d60d74ecdae961cfa927e5b0526 1623 main/binary-riscv64/Packages
3815e37b15c4ac150e23ce76786bfda0165e325d00b3650466442356ac39221b 817 main/binary-riscv64/Packages.gz
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCgAdFiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmqMVaEACgkQESwaeYRl
+/JqUQv+PFvgvw2yMwX1jfCeyKAfc/BmWIitVo5eOYS4P4iMqnOLGP9M44okbhgH
Ss7l9kg13Bmtn3TdF6eWOoyfhb/Zchpf5O4xeaXLCBsaTjFtX2BAgg7Z441AkEVP
AzaGcmbev6+R7EM3EEqk0fvVLyYHWw9qY9ADC+ArZ7GxYgVgAkIcM2V1bzjJQtOw
dYR2XwGWiFZet62DXVsCs6g26tUyvmR062GcKmk1xQY/g9SbOqPXYP7Uou5hw+UZ
QAY9LFnCWDz0OATclIKZzqhp7oMjWe3T96d/25lV+fQ2Cmk7ayPGaTrpeTnvCfzO
u46DIQkkTpyalhZPcC0hgCg9tbh2rzAF6ETy+oKmYYE8wEA8wqIvJdGt1RZT/IUg
SQEma61GavK0nBVjPjEjchT5hfTgS03YBea15lA5sZiEAD0SOV4QHxbbVmszGEaQ
Ha4AnRLBji9Ea5E3848QTJLPq1BZ7zLvU6Tn8Nlutuh2doizSQlX3xSozvUV9BYp
bWU/J6Ye
=8uE3
-----END PGP SIGNATURE-----
-26
View File
@@ -1,26 +0,0 @@
Origin: socktop
Label: socktop
Suite: stable
Codename: stable
Architectures: amd64 arm64 armhf riscv64
Components: main
Description: socktop APT repository
Date: Mon, 24 Aug 2026 14:30:55 +0000
MD5Sum:
ada07cbd3ac088cecb5dc9d08518823d 1627 main/binary-amd64/Packages
fbb8924917960d65b5bcad003ccb90c2 830 main/binary-amd64/Packages.gz
71b8d2e69d19d4db385bc3f2be603b72 1627 main/binary-arm64/Packages
70831c731ff47542094fe3417e385aa6 830 main/binary-arm64/Packages.gz
129d1b34bf48c75861a188d6ae8fba77 1611 main/binary-armhf/Packages
54e691233b47bb1f0d89f82b00d9403f 814 main/binary-armhf/Packages.gz
4c3fc0b399405dd682dac1318adda05f 1623 main/binary-riscv64/Packages
15bcbe1806bf2577849090556c3a2a81 817 main/binary-riscv64/Packages.gz
SHA256:
88a907a84f4d292e34426d71e328561aba9658440e588f62f0f3b5d7f5c5f121 1627 main/binary-amd64/Packages
3a87b0b38cb79d231b8ed66e1fdc9a54369d0d3f2f0686f6513cece15f0dc967 830 main/binary-amd64/Packages.gz
84a43993bf58648174ef5bf1d4542e96f5d7903acedc0405f8f3aeab4fdbb4e7 1627 main/binary-arm64/Packages
fd8e6a7ec2b7c438659b0fe8fa825670ccfbeb2298c01b55020cbdad2acb3313 830 main/binary-arm64/Packages.gz
63a89cdee8e5c93867b2dd5193df162304ea97855ad68135c5c5d0753700102c 1611 main/binary-armhf/Packages
c81f19a798757bd1806bb860aba238757edf1b902338554e30bcddba86b3c02b 814 main/binary-armhf/Packages.gz
fa590edf01fbdd63817bd1cee4ab49d93bd47d60d74ecdae961cfa927e5b0526 1623 main/binary-riscv64/Packages
3815e37b15c4ac150e23ce76786bfda0165e325d00b3650466442356ac39221b 817 main/binary-riscv64/Packages.gz
-14
View File
@@ -1,14 +0,0 @@
-----BEGIN PGP SIGNATURE-----
iQGzBAABCgAdFiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmqMVZ8ACgkQESwaeYRl
+/JgAgwA1xKy+N7n4S4bjP1LxoLDa0fOHnxORd2dajYHkA6NqORSd2KPgWqBwtlA
TGA8wJ8Gurdluu6kaZ8rJ55PUVZFX2qU/UsXy0Pn1Kp2NMuMhdkzXUuYI/oKILof
TtYKqubJ4+LZvfrCdOWusDGhybA4DXaPGm+/EjJ6xKJDm0N6Ht4YhUlb9uTFH8jr
y/hzl9Qcp2GoaDIXoLQCYPfUePHSH7JRn77uuKu3DKC3rwP2QXyBDNfWRJY0X89L
fGBzAxgj2jE7/Gn/T3X3QGTsSAlZeuEIu88/+qwyi3kq7SeBWFf70kGHFS3RPAmh
ULMpQe44N0H3faBIC1wER/P12NIDCi633XkL5WVwA+HvSWk8DZGu+cV4s7BcmWbD
8KOcJe9dEoYpcGbxpZMqe0iCc6pr7vQ5FBlt0Zj0LzvONSL55kH8L7tG/uX4VXcM
g6LLLJjm6oLWT8U8QNskOgAVxTgyZgHUVa2cAG0jZp0BErSegg38mHLs3QXtdySc
xd4t41hM
=OG4V
-----END PGP SIGNATURE-----
-40
View File
@@ -1,40 +0,0 @@
Package: socktop
Version: 1.60.2-1
Architecture: amd64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 3441
Depends: libc6 (>= 2.34)
Filename: pool/main/socktop_1.60.2-1_amd64.deb
Size: 1313748
MD5sum: 24517013f7e9683f2bbbbf07debdffca
SHA1: c5c9498bfcc8a8d21342f1f4b5797e4c000462d7
SHA256: b5d21f5f322af61e7ea6846ba337e5e48697969cf40b098d01ad4927c912d6e6
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Remote system monitor over WebSocket, TUI like top
socktop is a remote system monitor with a rich terminal user interface (TUI)
that connects to remote hosts running the socktop_agent over WebSocket. It
provides real-time monitoring of CPU, memory, processes, and more with an
interface similar to the traditional 'top' command.
Package: socktop-agent
Version: 1.60.2-1
Architecture: amd64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 6537
Depends: libc6 (>= 2.34), libdrm-amdgpu1 (>= 2.4.80)
Filename: pool/main/socktop-agent_1.60.2-1_amd64.deb
Size: 1858208
MD5sum: fa4f1a29b22fe191140787da50c70cd1
SHA1: b310e7f5a29be4d18add88a6b30437eb34d2e6a1
SHA256: 328975156e28ffc2d38a23c317406b9220f0062992f97bfea327403ba6d26d71
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Socktop agent daemon. Serves host metrics over WebSocket.
socktop_agent is the daemon component that runs on remote hosts to collect and
serve system metrics over WebSocket. It gathers CPU, memory, disk, network,
GPU, and process information that can be monitored remotely by the socktop TUI
client.
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
Archive: stable
Component: main
Origin: socktop
Label: socktop
Architecture: amd64
-40
View File
@@ -1,40 +0,0 @@
Package: socktop
Version: 1.60.2-1
Architecture: arm64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 2994
Depends: libc6 (>= 2.34)
Filename: pool/main/socktop_1.60.2-1_arm64.deb
Size: 1176428
MD5sum: 6c628e55b118550964fdd3429ade9013
SHA1: 814407491af72cc8c03dfc4aca1fcd29676d84e7
SHA256: fb3f29fe439d7e8a6813c52811a4acb9dc5a390acbbe5c8b5a7d77301bf7df39
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Remote system monitor over WebSocket, TUI like top
socktop is a remote system monitor with a rich terminal user interface (TUI)
that connects to remote hosts running the socktop_agent over WebSocket. It
provides real-time monitoring of CPU, memory, processes, and more with an
interface similar to the traditional 'top' command.
Package: socktop-agent
Version: 1.60.2-1
Architecture: arm64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 5158
Depends: libc6 (>= 2.34), libdrm-amdgpu1 (>= 2.4.80)
Filename: pool/main/socktop-agent_1.60.2-1_arm64.deb
Size: 1648768
MD5sum: d40c11e1a82a4deee69a8f09b99b8a3c
SHA1: bb17940a085d9cdda0d0ae26aa73843f3b293705
SHA256: 9f0779dafe9e2c5029c65201e2d45abb2cc9b9dfae38deb6d650f6c9a5fd07ce
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Socktop agent daemon. Serves host metrics over WebSocket.
socktop_agent is the daemon component that runs on remote hosts to collect and
serve system metrics over WebSocket. It gathers CPU, memory, disk, network,
GPU, and process information that can be monitored remotely by the socktop TUI
client.
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
Package: socktop
Version: 1.60.2-1
Architecture: armhf
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 2764
Depends: libc6:armhf (>= 2.35)
Filename: pool/main/socktop_1.60.2-1_armhf.deb
Size: 1029920
MD5sum: 731d71c8450006624db51f802a1b75fa
SHA1: 7a797ecb089208171bc762a2602af8048b9d74fa
SHA256: bfd6d29c18826ed20a9f0e3af2e0f5cc2d24dd23f621110f9d58aed60f7ca1a5
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Remote system monitor over WebSocket, TUI like top
socktop is a remote system monitor with a rich terminal user interface (TUI)
that connects to remote hosts running the socktop_agent over WebSocket. It
provides real-time monitoring of CPU, memory, processes, and more with an
interface similar to the traditional 'top' command.
Package: socktop-agent
Version: 1.60.2-1
Architecture: armhf
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 3821
Depends: libc6:armhf (>= 2.35)
Filename: pool/main/socktop-agent_1.60.2-1_armhf.deb
Size: 1462340
MD5sum: b987e05b88a9a32fb3379c54413a1eb0
SHA1: c751d0bf6ced067c667895f36379df4bc7b1170c
SHA256: c2bcce2e679d96cc9076d118abd9a766ed413f306d88dcc59912814c1962ba04
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Socktop agent daemon. Serves host metrics over WebSocket.
socktop_agent is the daemon component that runs on remote hosts to collect and
serve system metrics over WebSocket. It gathers CPU, memory, disk, network,
GPU, and process information that can be monitored remotely by the socktop TUI
client.
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
Package: socktop
Version: 1.60.2-1
Architecture: riscv64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 2668
Depends: libc6:riscv64 (>= 2.35)
Filename: pool/main/socktop_1.60.2-1_riscv64.deb
Size: 1166192
MD5sum: 11e89c1f7909fc90047a74d2880d1362
SHA1: 4089962ff3864209a026b585fe2b6490cfe014a2
SHA256: e1fad743c74934a11e49808d0bba80c26f05e6d75c231f239870866ba50e1e38
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Remote system monitor over WebSocket, TUI like top
socktop is a remote system monitor with a rich terminal user interface (TUI)
that connects to remote hosts running the socktop_agent over WebSocket. It
provides real-time monitoring of CPU, memory, processes, and more with an
interface similar to the traditional 'top' command.
Package: socktop-agent
Version: 1.60.2-1
Architecture: riscv64
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
Installed-Size: 3846
Depends: libc6:riscv64 (>= 2.35)
Filename: pool/main/socktop-agent_1.60.2-1_riscv64.deb
Size: 1637096
MD5sum: 98347f203e942073464ff8b8d16796bb
SHA1: dec2ae6fdd5bea71f43f9e24842a0851150e24fd
SHA256: fd4070c6e0b29adf5bb9de32f098431e9dcf7456e1fc7f4460a5e9a5708496cc
Section: admin
Priority: optional
Homepage: https://github.com/jasonwitty/socktop
Description: Socktop agent daemon. Serves host metrics over WebSocket.
socktop_agent is the daemon component that runs on remote hosts to collect and
serve system metrics over WebSocket. It gathers CPU, memory, disk, network,
GPU, and process information that can be monitored remotely by the socktop TUI
client.
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Socktop agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/socktop_agent --port 3000
Environment=RUST_LOG=info
# Optional auth:
# Environment=SOCKTOP_TOKEN=changeme
Restart=on-failure
User=socktop
Group=socktop
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

-364
View File
@@ -1,364 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>socktop APT Repository</title>
<style>
/* Catppuccin Frappe Color Palette */
:root {
--ctp-base: #303446;
--ctp-mantle: #292c3c;
--ctp-crust: #232634;
--ctp-text: #c6d0f5;
--ctp-subtext1: #b5bfe2;
--ctp-subtext0: #a5adce;
--ctp-overlay2: #949cbb;
--ctp-overlay1: #838ba7;
--ctp-overlay0: #737994;
--ctp-surface2: #626880;
--ctp-surface1: #51576d;
--ctp-surface0: #414559;
--ctp-lavender: #babbf1;
--ctp-blue: #8caaee;
--ctp-sapphire: #85c1dc;
--ctp-sky: #99d1db;
--ctp-teal: #81c8be;
--ctp-green: #a6d189;
--ctp-yellow: #e5c890;
--ctp-peach: #ef9f76;
--ctp-maroon: #ea999c;
--ctp-red: #e78284;
--ctp-mauve: #ca9ee6;
--ctp-pink: #f4b8e4;
--ctp-flamingo: #eebebe;
--ctp-rosewater: #f2d5cf;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background-color: var(--ctp-base);
color: var(--ctp-text);
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: var(--ctp-mantle);
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
h1 {
color: var(--ctp-blue);
font-size: 2.5em;
margin-bottom: 10px;
border-bottom: 3px solid var(--ctp-surface0);
padding-bottom: 15px;
}
h2 {
color: var(--ctp-mauve);
font-size: 1.8em;
margin-top: 35px;
margin-bottom: 15px;
}
h3 {
color: var(--ctp-sapphire);
font-size: 1.3em;
margin-top: 25px;
margin-bottom: 10px;
}
p {
margin-bottom: 15px;
color: var(--ctp-subtext0);
}
.subtitle {
color: var(--ctp-subtext1);
font-size: 1.2em;
margin-bottom: 30px;
}
code {
background-color: var(--ctp-surface0);
color: var(--ctp-green);
padding: 3px 8px;
border-radius: 5px;
font-family: "Courier New", Courier, monospace;
font-size: 0.95em;
}
pre {
background-color: var(--ctp-crust);
border: 1px solid var(--ctp-surface0);
border-radius: 8px;
padding: 20px;
overflow-x: auto;
margin: 15px 0;
position: relative;
padding-top: 18px; /* leave space for top-right button */
}
pre code {
background: transparent;
color: var(--ctp-text);
display: block;
white-space: pre;
font-family: "Courier New", Courier, monospace;
font-size: 0.95em;
}
/* Copy button styles */
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: var(--ctp-surface1);
color: var(--ctp-text);
border: 1px solid var(--ctp-surface2);
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 6px;
transition:
background 0.12s ease,
transform 0.08s ease;
}
.copy-btn:hover {
background: var(--ctp-surface2);
transform: translateY(-1px);
}
.copy-btn:active {
transform: translateY(0);
}
.copy-btn.copied {
background: var(--ctp-green);
color: var(--ctp-crust);
border-color: transparent;
}
.badge {
display: inline-block;
background-color: var(--ctp-surface1);
color: var(--ctp-text);
padding: 5px 12px;
border-radius: 6px;
font-size: 0.9em;
margin: 5px 5px 5px 0;
border: 1px solid var(--ctp-surface2);
}
.badge.arch {
background-color: var(--ctp-surface0);
color: var(--ctp-lavender);
}
.note {
background-color: var(--ctp-surface0);
border-left: 4px solid var(--ctp-yellow);
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.note strong {
color: var(--ctp-yellow);
}
.footer {
margin-top: 50px;
padding-top: 20px;
border-top: 2px solid var(--ctp-surface0);
text-align: center;
color: var(--ctp-overlay1);
font-size: 0.9em;
}
.command-comment {
color: var(--ctp-overlay1);
display: block;
margin-bottom: 6px;
}
.highlight-blue {
color: var(--ctp-blue);
}
.highlight-green {
color: var(--ctp-green);
}
.highlight-yellow {
color: var(--ctp-yellow);
}
.highlight-mauve {
color: var(--ctp-mauve);
}
.highlight-peach {
color: var(--ctp-peach);
}
@media (max-width: 768px) {
.container {
padding: 25px;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>socktop APT Repository</h1>
<p class="subtitle">
System monitor with remote agent support for Linux systems
</p>
<h2>📦 Quick Installation</h2>
<p>Add this repository to your Debian/Ubuntu system:</p>
<h3>Step 1: Add GPG Key</h3>
<pre><code># Add the repository's GPG signing key
curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg</code></pre>
<h3>Step 2: Add Repository</h3>
<pre><code># Add the APT repository to your sources
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" | \
sudo tee /etc/apt/sources.list.d/socktop.list</code></pre>
<h3>Step 3: Install</h3>
<pre><code># Update package lists and install
sudo apt update
sudo apt install socktop socktop-agent</code></pre>
<h2>📋 What's Included</h2>
<ul>
<li>
<strong class="highlight-blue">socktop</strong> - Terminal
UI client for monitoring systems
</li>
<li>
<strong class="highlight-mauve">socktop-agent</strong> -
Background agent that reports system metrics
</li>
</ul>
<div class="note">
<strong>Note:</strong> The agent package automatically installs
and configures a systemd service. Enable it with:
<code>sudo systemctl enable --now socktop-agent</code>
</div>
<h2>🏗️ Supported Architectures</h2>
<div>
<span class="badge arch">amd64</span>
<span class="badge arch">arm64</span>
<span class="badge arch">armhf</span>
<span class="badge arch">riscv64</span>
</div>
<h2>🔧 Usage</h2>
<p>After installation:</p>
<pre><code># Start the TUI client
socktop
# Connect to a remote agent
socktop ws://hostname:3000
# Start the agent (if not using systemd)
socktop_agent</code></pre>
<h2>🔗 Links</h2>
<ul>
<li>
<a href="https://github.com/jasonwitty/socktop"
>Source Code on GitHub</a
>
</li>
<li>
<a href="https://github.com/jasonwitty/socktop/issues"
>Report Issues</a
>
</li>
<li><a href="README.md">Repository Documentation</a></li>
</ul>
<div class="footer">
<p>Hosted on GitHub Pages | Packages signed with GPG</p>
<p>
Theme:
<a href="https://github.com/catppuccin/catppuccin"
>Catppuccin Frappe</a
>
</p>
</div>
</div>
<script>
// Attach copy buttons to all <pre> blocks and enable copy-to-clipboard.
document.addEventListener("DOMContentLoaded", function () {
const pres = document.querySelectorAll("pre");
pres.forEach((pre) => {
// Create button
const btn = document.createElement("button");
btn.type = "button";
btn.className = "copy-btn";
btn.setAttribute("aria-label", "Copy code to clipboard");
// Use emoji for simplicity; you can replace with SVG if desired
btn.innerText = "📋";
// Append to pre
pre.appendChild(btn);
// Click handler
btn.addEventListener("click", async (e) => {
e.stopPropagation();
const code = pre.querySelector("code");
const text = code ? code.innerText : pre.innerText;
try {
if (!navigator.clipboard) {
// Fallback method
const textarea =
document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
} else {
await navigator.clipboard.writeText(text);
}
// feedback
btn.classList.add("copied");
const prior = btn.innerText;
btn.innerText = "✓ Copied";
setTimeout(() => {
btn.classList.remove("copied");
btn.innerText = "📋";
}, 1800);
} catch (err) {
btn.innerText = "✖";
setTimeout(() => (btn.innerText = "📋"), 1500);
console.error("Copy failed", err);
}
});
});
});
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "socktop"
version = "0.1.0"
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
description = "Remote system monitor over WebSocket, TUI like top"
edition = "2021"
[dependencies]
tokio = { workspace = true }
tokio-tungstenite = { workspace = true }
tungstenite = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
ratatui = { workspace = true }
crossterm = { workspace = true }
chrono = { workspace = true }
anyhow = { workspace = true }
+198
View File
@@ -0,0 +1,198 @@
//! App state and main loop: input handling, fetching metrics, updating history, and drawing.
use std::{collections::VecDeque, io, time::{Duration, Instant}};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction},
Terminal,
};
use tokio::time::sleep;
use crate::history::{push_capped, PerCoreHistory};
use crate::types::Metrics;
use crate::ui::{header::draw_header, cpu::{draw_cpu_avg_graph, draw_per_core_bars}, mem::draw_mem, swap::draw_swap, disks::draw_disks, net::draw_net_spark, processes::draw_top_processes};
use crate::ws::{connect, request_metrics};
pub struct App {
// Latest metrics + histories
last_metrics: Option<Metrics>,
// CPU avg history (0..100)
cpu_hist: VecDeque<u64>,
// Per-core history (0..100)
per_core_hist: PerCoreHistory,
// Network totals snapshot + histories of KB/s
last_net_totals: Option<(u64, u64, Instant)>,
rx_hist: VecDeque<u64>,
tx_hist: VecDeque<u64>,
rx_peak: u64,
tx_peak: u64,
// Quit flag
should_quit: bool,
}
impl App {
pub fn new() -> Self {
Self {
last_metrics: None,
cpu_hist: VecDeque::with_capacity(600),
per_core_hist: PerCoreHistory::new(60),
last_net_totals: None,
rx_hist: VecDeque::with_capacity(600),
tx_hist: VecDeque::with_capacity(600),
rx_peak: 0,
tx_peak: 0,
should_quit: false,
}
}
pub async fn run(&mut self, url: &str) -> Result<(), Box<dyn std::error::Error>> {
// Connect to agent
let mut ws = connect(url).await?;
// Terminal setup
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
// Main loop
let res = self.event_loop(&mut terminal, &mut ws).await;
// Teardown
disable_raw_mode()?;
let backend = terminal.backend_mut();
execute!(backend, LeaveAlternateScreen)?;
terminal.show_cursor()?;
res
}
async fn event_loop<B: ratatui::backend::Backend>(
&mut self,
terminal: &mut Terminal<B>,
ws: &mut crate::ws::WsStream,
) -> Result<(), Box<dyn std::error::Error>> {
loop {
// Input (non-blocking)
while event::poll(Duration::from_millis(10))? {
if let Event::Key(k) = event::read()? {
if matches!(k.code, KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc) {
self.should_quit = true;
}
}
}
if self.should_quit {
break;
}
// Fetch and update
if let Some(m) = request_metrics(ws).await {
self.update_with_metrics(m);
}
// Draw
terminal.draw(|f| self.draw(f))?;
// Tick rate
sleep(Duration::from_millis(500)).await;
}
Ok(())
}
fn update_with_metrics(&mut self, m: Metrics) {
// CPU avg history
let v = m.cpu_total.clamp(0.0, 100.0).round() as u64;
push_capped(&mut self.cpu_hist, v, 600);
// Per-core history (push current samples)
self.per_core_hist.ensure_cores(m.cpu_per_core.len());
self.per_core_hist.push_samples(&m.cpu_per_core);
// NET: sum across all ifaces, compute KB/s via elapsed time
let now = Instant::now();
let rx_total = m.networks.iter().map(|n| n.received).sum::<u64>();
let tx_total = m.networks.iter().map(|n| n.transmitted).sum::<u64>();
let (rx_kb, tx_kb) = if let Some((prx, ptx, pts)) = self.last_net_totals {
let dt = now.duration_since(pts).as_secs_f64().max(1e-6);
let rx = ((rx_total.saturating_sub(prx)) as f64 / dt / 1024.0).round() as u64;
let tx = ((tx_total.saturating_sub(ptx)) as f64 / dt / 1024.0).round() as u64;
(rx, tx)
} else { (0, 0) };
self.last_net_totals = Some((rx_total, tx_total, now));
push_capped(&mut self.rx_hist, rx_kb, 600);
push_capped(&mut self.tx_hist, tx_kb, 600);
self.rx_peak = self.rx_peak.max(rx_kb);
self.tx_peak = self.tx_peak.max(tx_kb);
self.last_metrics = Some(m);
}
fn draw(&mut self, f: &mut ratatui::Frame<'_>) {
let area = f.area();
let rows = ratatui::layout::Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Ratio(1, 3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(10),
])
.split(area);
draw_header(f, rows[0], self.last_metrics.as_ref());
let top = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[1]);
draw_cpu_avg_graph(f, top[0], &self.cpu_hist, self.last_metrics.as_ref());
draw_per_core_bars(f, top[1], self.last_metrics.as_ref(), &self.per_core_hist);
draw_mem(f, rows[2], self.last_metrics.as_ref());
draw_swap(f, rows[3], self.last_metrics.as_ref());
let bottom = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[4]);
let left_stack = ratatui::layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(6), Constraint::Length(4), Constraint::Length(4)])
.split(bottom[0]);
draw_disks(f, left_stack[0], self.last_metrics.as_ref());
draw_net_spark(
f,
left_stack[1],
&format!("Download (KB/s) — now: {} | peak: {}", self.rx_hist.back().copied().unwrap_or(0), self.rx_peak),
&self.rx_hist,
ratatui::style::Color::Green,
);
draw_net_spark(
f,
left_stack[2],
&format!("Upload (KB/s) — now: {} | peak: {}", self.tx_hist.back().copied().unwrap_or(0), self.tx_peak),
&self.tx_hist,
ratatui::style::Color::Blue,
);
draw_top_processes(f, bottom[1], self.last_metrics.as_ref());
}
}
+39
View File
@@ -0,0 +1,39 @@
//! Small utilities to manage bounded history buffers for charts.
use std::collections::VecDeque;
pub fn push_capped<T>(dq: &mut VecDeque<T>, v: T, cap: usize) {
if dq.len() == cap {
dq.pop_front();
}
dq.push_back(v);
}
// Keeps a history deque per core with a fixed capacity
pub struct PerCoreHistory {
pub deques: Vec<VecDeque<u16>>,
cap: usize,
}
impl PerCoreHistory {
pub fn new(cap: usize) -> Self {
Self { deques: Vec::new(), cap }
}
// Ensure we have one deque per core; resize on CPU topology changes
pub fn ensure_cores(&mut self, n: usize) {
if self.deques.len() == n {
return;
}
self.deques = (0..n).map(|_| VecDeque::with_capacity(self.cap)).collect();
}
// Push a new sample set for all cores (values 0..=100)
pub fn push_samples(&mut self, samples: &[f32]) {
self.ensure_cores(samples.len());
for (i, v) in samples.iter().enumerate() {
let val = v.clamp(0.0, 100.0).round() as u16;
push_capped(&mut self.deques[i], val, self.cap);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
//! Entry point for the socktop TUI. Parses args and runs the App.
mod app;
mod history;
mod types;
mod ui;
mod ws;
use std::env;
use app::App;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} ws://HOST:PORT/ws", args[0]);
std::process::exit(1);
}
let url = args[1].clone();
let mut app = App::new();
app.run(&url).await
}
+41
View File
@@ -0,0 +1,41 @@
//! Types that mirror the agent's JSON schema.
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct Disk {
pub name: String,
pub total: u64,
pub available: u64,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Network {
// cumulative totals; client diffs to compute rates
pub received: u64,
pub transmitted: u64,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ProcessInfo {
pub pid: u32,
pub name: String,
pub cpu_usage: f32,
pub mem_bytes: u64,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Metrics {
pub cpu_total: f32,
pub cpu_per_core: Vec<f32>,
pub mem_total: u64,
pub mem_used: u64,
pub swap_total: u64,
pub swap_used: u64,
pub process_count: usize,
pub hostname: String,
pub cpu_temp_c: Option<f32>,
pub disks: Vec<Disk>,
pub networks: Vec<Network>,
pub top_processes: Vec<ProcessInfo>,
}
+91
View File
@@ -0,0 +1,91 @@
//! CPU average sparkline + per-core mini bars.
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Sparkline},
};
use ratatui::style::Modifier;
use crate::history::PerCoreHistory;
use crate::types::Metrics;
pub fn draw_cpu_avg_graph(
f: &mut ratatui::Frame<'_>,
area: Rect,
hist: &std::collections::VecDeque<u64>,
m: Option<&Metrics>,
) {
let title = if let Some(mm) = m { format!("CPU avg (now: {:>5.1}%)", mm.cpu_total) } else { "CPU avg".into() };
let max_points = area.width.saturating_sub(2) as usize;
let start = hist.len().saturating_sub(max_points);
let data: Vec<u64> = hist.iter().skip(start).cloned().collect();
let spark = Sparkline::default()
.block(Block::default().borders(Borders::ALL).title(title))
.data(&data)
.max(100)
.style(Style::default().fg(Color::Cyan));
f.render_widget(spark, area);
}
pub fn draw_per_core_bars(
f: &mut ratatui::Frame<'_>,
area: Rect,
m: Option<&Metrics>,
per_core_hist: &PerCoreHistory,
) {
f.render_widget(Block::default().borders(Borders::ALL).title("Per-core"), area);
let Some(mm) = m else { return; };
let inner = Rect { x: area.x + 1, y: area.y + 1, width: area.width.saturating_sub(2), height: area.height.saturating_sub(2) };
if inner.height == 0 { return; }
let rows = inner.height as usize;
let show_n = rows.min(mm.cpu_per_core.len());
let constraints: Vec<Constraint> = (0..show_n).map(|_| Constraint::Length(1)).collect();
let vchunks = Layout::default().direction(Direction::Vertical).constraints(constraints).split(inner);
for i in 0..show_n {
let rect = vchunks[i];
let hchunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(6), Constraint::Length(12)])
.split(rect);
let curr = mm.cpu_per_core[i].clamp(0.0, 100.0);
let older = per_core_hist.deques.get(i)
.and_then(|d| d.iter().rev().nth(20).copied())
.map(|v| v as f32)
.unwrap_or(curr);
let trend = if curr > older + 0.2 { "" }
else if curr + 0.2 < older { "" }
else { "" };
let fg = match curr {
x if x < 25.0 => Color::Green,
x if x < 60.0 => Color::Yellow,
_ => Color::Red,
};
let hist: Vec<u64> = per_core_hist
.deques
.get(i)
.map(|d| {
let max_points = hchunks[0].width as usize;
let start = d.len().saturating_sub(max_points);
d.iter().skip(start).map(|&v| v as u64).collect()
})
.unwrap_or_default();
let spark = Sparkline::default()
.data(&hist)
.max(100)
.style(Style::default().fg(fg));
f.render_widget(spark, hchunks[0]);
let label = format!("cpu{:<2}{}{:>5.1}%", i, trend, curr);
let line = Line::from(Span::styled(label, Style::default().fg(fg).add_modifier(Modifier::BOLD)));
f.render_widget(Paragraph::new(line).right_aligned(), hchunks[1]);
}
}
+73
View File
@@ -0,0 +1,73 @@
//! Disk cards with per-device gauge and title line.
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::Style,
widgets::{Block, Borders, Gauge},
};
use crate::types::Metrics;
use crate::ui::util::{human, truncate_middle, disk_icon};
pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
f.render_widget(Block::default().borders(Borders::ALL).title("Disks"), area);
let Some(mm) = m else { return; };
let inner = Rect {
x: area.x + 1,
y: area.y + 1,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2),
};
if inner.height < 3 { return; }
let per_disk_h = 3u16;
let max_cards = (inner.height / per_disk_h).min(mm.disks.len() as u16) as usize;
let constraints: Vec<Constraint> = (0..max_cards).map(|_| Constraint::Length(per_disk_h)).collect();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(inner);
for (i, slot) in rows.iter().enumerate() {
let d = &mm.disks[i];
let used = d.total.saturating_sub(d.available);
let ratio = if d.total > 0 { used as f64 / d.total as f64 } else { 0.0 };
let pct = (ratio * 100.0).round() as u16;
let color = if pct < 70 { ratatui::style::Color::Green } else if pct < 90 { ratatui::style::Color::Yellow } else { ratatui::style::Color::Red };
let title = format!(
"{} {} {} / {} ({}%)",
disk_icon(&d.name),
truncate_middle(&d.name, (slot.width.saturating_sub(6)) as usize / 2),
human(used),
human(d.total),
pct
);
let card = Block::default().borders(Borders::ALL).title(title);
f.render_widget(card, *slot);
let inner_card = Rect {
x: slot.x + 1,
y: slot.y + 1,
width: slot.width.saturating_sub(2),
height: slot.height.saturating_sub(2),
};
if inner_card.height == 0 { continue; }
let gauge_rect = Rect {
x: inner_card.x,
y: inner_card.y + inner_card.height / 2,
width: inner_card.width,
height: 1,
};
let g = Gauge::default()
.percent(pct)
.gauge_style(Style::default().fg(color));
f.render_widget(g, gauge_rect);
}
}
+20
View File
@@ -0,0 +1,20 @@
//! Top header with hostname and CPU temperature indicator.
use ratatui::{
layout::Rect,
widgets::{Block, Borders},
};
use crate::types::Metrics;
pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let title = if let Some(mm) = m {
let temp = mm.cpu_temp_c.map(|t| {
let icon = if t < 50.0 { "😎" } else if t < 85.0 { "⚠️" } else { "🔥" };
format!("CPU Temp: {:.1}°C {}", t, icon)
}).unwrap_or_else(|| "CPU Temp: N/A".into());
format!("socktop — host: {} | {} (press 'q' to quit)", mm.hostname, temp)
} else {
"socktop — connecting... (press 'q' to quit)".into()
};
f.render_widget(Block::default().title(title).borders(Borders::BOTTOM), area);
}
+23
View File
@@ -0,0 +1,23 @@
//! Memory gauge.
use ratatui::{
layout::Rect,
style::{Color, Style},
widgets::{Block, Borders, Gauge},
};
use crate::types::Metrics;
use crate::ui::util::human;
pub fn draw_mem(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let (used, total, pct) = if let Some(mm) = m {
let pct = if mm.mem_total > 0 { (mm.mem_used as f64 / mm.mem_total as f64 * 100.0) as u16 } else { 0 };
(mm.mem_used, mm.mem_total, pct)
} else { (0, 0, 0) };
let g = Gauge::default()
.block(Block::default().borders(Borders::ALL).title("Memory"))
.gauge_style(Style::default().fg(Color::Magenta))
.percent(pct)
.label(format!("{} / {}", human(used), human(total)));
f.render_widget(g, area);
}
+10
View File
@@ -0,0 +1,10 @@
//! UI module root: exposes drawing functions for individual panels.
pub mod header;
pub mod cpu;
pub mod mem;
pub mod swap;
pub mod disks;
pub mod net;
pub mod processes;
pub mod util;

Some files were not shown because too many files have changed in this diff Show More