Autarch Is Setting The Internet Free

This commit is contained in:
DigiJ 2026-03-12 20:51:38 -07:00
parent 559f447753
commit 32842d9873
153 changed files with 21768 additions and 315553 deletions

Binary file not shown.

View File

@ -1,2 +0,0 @@
#!/bin/bash
source "$(dirname "$(realpath "$0")")/venv/bin/activate"

650
autarch_float.md Normal file
View File

@ -0,0 +1,650 @@
# AUTARCH Cloud Edition & Float Mode — Architecture Plan
**Run AUTARCH on a VPS, use it like it's on your local machine**
By darkHal Security Group & Setec Security Labs
---
## 1. What Is Float Mode?
Float Mode makes a remote AUTARCH instance feel local. The user signs into their AUTARCH account on a VPS and activates Float Mode. A lightweight client applet running on the user's PC creates a bridge that:
- **Forwards USB devices** (phones, ESP32, hardware) from the user's PC to the VPS
- **Exposes local network context** (LAN scanning sees the user's network, not the VPS's)
- **Bridges serial ports** (COM/ttyUSB devices) for hardware flashing
- **Provides clipboard sync** between local and remote
- **Tunnels mDNS/Bluetooth discovery** from the local machine
The VPS does all computation. The user's PC just provides I/O.
```
┌─────────────────────────────────────────────────────────┐
│ USER'S BROWSER │
│ ┌───────────────────────────────────────────────────┐ │
│ │ AUTARCH Cloud Edition (Web UI) │ │
│ │ Same UI as local AUTARCH — hardware, OSINT, │ │
│ │ exploit tools, AI chat — everything works │ │
│ └─────────────────────┬─────────────────────────────┘ │
│ │ HTTPS │
└────────────────────────┼────────────────────────────────┘
┌────▼────┐
│ VPS │
│ AUTARCH │ ← All processing happens here
│ CE │
└────┬────┘
│ WebSocket (wss://)
│ Float Bridge Protocol
┌────────────────────────┼────────────────────────────────┐
│ USER'S PC │
│ ┌─────────────────────▼─────────────────────────────┐ │
│ │ Float Applet (native Go app) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ │
│ │ │ USB Hub │ │ Serial │ │ Network Context │ │ │
│ │ │ Bridge │ │ Bridge │ │ (LAN, mDNS) │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────────┬────────┘ │ │
│ └───────┼──────────────┼─────────────────┼───────────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Android │ │ ESP32 │ │ LAN │ │
│ │ Phone │ │ Board │ │ Devices │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 2. Why Write AUTARCH CE in Go?
The Python AUTARCH works great locally. But for cloud deployment at scale:
| Python AUTARCH | Go AUTARCH CE |
|----------------|---------------|
| 500MB+ with venv + dependencies | Single binary, ~30MB |
| Requires Python 3.10+, pip, venv | Zero runtime dependencies |
| Flask (single-threaded WSGI) | Native goroutine concurrency |
| llama-cpp-python (C++ bindings) | HTTP calls to LLM APIs only (no local models on VPS) |
| File-based config (INI) | Embedded config + SQLite |
| 72 modules loaded at startup | Modular, lazy-loaded handlers |
| subprocess for system tools | Native Go implementations where possible |
**Key insight:** The Cloud Edition doesn't need local LLM inference (no GPU on VPS), doesn't need ADB/Fastboot binaries (USB is on the user's PC), and doesn't need heavy Python dependencies. What it needs is a fast web server, WebSocket handling, and the ability to relay commands to the Float applet.
---
## 3. What Gets Ported, What Gets Dropped
### Port to Go (core features that work in cloud context)
| Feature | Python Source | Go Approach |
|---------|-------------|-------------|
| Web dashboard + UI | `web/app.py`, templates, CSS, JS | Go HTTP server + embedded templates |
| Authentication | `web/auth.py` | bcrypt + JWT |
| Configuration | `core/config.py` | YAML config + SQLite |
| LLM Chat (API-only) | `core/llm.py` | HTTP client to Claude/OpenAI/HF APIs |
| Agent system | `core/agent.py` | Port agent loop + tool registry to Go |
| OSINT recon | `modules/recon.py` | HTTP client, site database, result parsing |
| Dossier management | `modules/dossier.py` | SQLite + file storage |
| GeoIP | `modules/geoip.py` | MaxMind DB reader (native Go) |
| Hash toolkit | part of `modules/analyze.py` | Go `crypto/*` packages |
| Reverse shell listener | `modules/revshell.py`, `core/revshell.py` | Go net.Listener |
| Port scanner | `web/routes/port_scanner.py` | Go `net.DialTimeout` |
| Network mapping | `modules/net_mapper.py` | Go ICMP/TCP scanner |
| Targets management | `web/routes/targets.py` | SQLite CRUD |
| Payload generation | `modules/simulate.py` | String templating |
| Report engine | `modules/report_engine.py` | Go templates → PDF |
| Threat intel | `modules/threat_intel.py` | HTTP client to threat feeds |
| Wireshark (tshark) | `modules/wireshark.py` | Exec wrapper (tshark on VPS) |
| DNS service | `services/dns-server/` | Already Go, integrate directly |
### Relay via Float Bridge (runs on user's PC, not VPS)
| Feature | Why Relay? | Bridge Protocol |
|---------|-----------|----------------|
| ADB commands | Phone is on user's USB | USB frame relay |
| Fastboot | Phone is on user's USB | USB frame relay |
| ESP32 flash | Board is on user's serial port | Serial frame relay |
| Serial monitor | Device is on user's ttyUSB | Serial stream relay |
| LAN scanning | User's network, not VPS | Network proxy relay |
| mDNS discovery | User's LAN | mDNS frame relay |
| Bluetooth | User's adapter | BT command relay |
| File push/pull to device | USB device files | Chunked transfer relay |
### Drop (not applicable in cloud context)
| Feature | Why Drop |
|---------|---------|
| Local LLM (llama.cpp) | No GPU on VPS; use API backends |
| Local transformers | Same — no GPU |
| System defender (hardening) | VPS is managed by Setec Manager |
| Windows defender | Cloud is Linux only |
| Defense monitor | Managed by Setec Manager |
| UPnP port mapping | VPS has static IP, no NAT |
| WireGuard management | Not needed in cloud (direct HTTPS) |
| Metasploit RPC | Can optionally exec, but low priority |
| RouterSploit | Same |
| Module encryption system | Go doesn't need this pattern |
| Setup wizard | Replaced by Setec Manager bootstrap |
| CLI menu system | Cloud is web-only |
| Print capture/debug console | Replace with structured logging |
---
## 4. AUTARCH CE Directory Structure
```
/c/autarch_ce/ # Development root
├── cmd/
│ └── autarch-ce/
│ └── main.go # Entry point
├── internal/
│ ├── server/
│ │ ├── server.go # HTTP server, router, middleware
│ │ ├── auth.go # JWT authentication
│ │ ├── sse.go # SSE streaming helpers
│ │ └── websocket.go # WebSocket helpers
│ ├── handlers/
│ │ ├── dashboard.go # Main dashboard
│ │ ├── chat.go # LLM chat + agent mode
│ │ ├── osint.go # OSINT reconnaissance
│ │ ├── scanner.go # Port scanner
│ │ ├── analyze.go # Hash toolkit, file analysis
│ │ ├── simulate.go # Payload generation, attack sim
│ │ ├── hardware.go # Hardware management (Float relay)
│ │ ├── targets.go # Target management CRUD
│ │ ├── exploit.go # Android/iPhone exploit routes
│ │ ├── revshell.go # Reverse shell listener
│ │ ├── network.go # Network tools (traceroute, ping, DNS)
│ │ ├── threat.go # Threat intel feeds
│ │ ├── reports.go # Report generation
│ │ ├── settings.go # Configuration management
│ │ └── wireshark.go # Packet capture (tshark on VPS)
│ ├── llm/
│ │ ├── llm.go # Multi-backend LLM client interface
│ │ ├── claude.go # Anthropic Claude backend
│ │ ├── openai.go # OpenAI backend
│ │ ├── huggingface.go # HuggingFace Inference backend
│ │ └── agent.go # Autonomous agent (THOUGHT/ACTION/PARAMS)
│ ├── osint/
│ │ ├── engine.go # Site checking engine (concurrent)
│ │ ├── sites.go # Site database loader (JSON)
│ │ ├── dossier.go # Dossier management
│ │ └── geoip.go # MaxMind GeoIP reader
│ ├── hardware/
│ │ ├── manager.go # Hardware manager (Float-aware)
│ │ ├── adb.go # ADB command builder/parser
│ │ ├── fastboot.go # Fastboot command builder/parser
│ │ ├── serial.go # Serial port management
│ │ └── bridge.go # Float bridge command relay
│ ├── float/
│ │ ├── protocol.go # Binary WebSocket frame protocol
│ │ ├── session.go # Session management
│ │ ├── usb.go # USB device relay logic
│ │ ├── serial.go # Serial port relay logic
│ │ └── network.go # Network context relay logic
│ ├── scanner/
│ │ ├── port.go # TCP/UDP port scanner
│ │ ├── service.go # Service fingerprinting
│ │ └── network.go # Network mapper (ICMP sweep)
│ ├── tools/
│ │ ├── registry.go # Agent tool registry
│ │ ├── hash.go # Hash computation + identification
│ │ ├── strings.go # String extraction from binaries
│ │ └── encode.go # Encoding/decoding utilities
│ ├── revshell/
│ │ ├── listener.go # TCP listener for reverse shells
│ │ ├── generator.go # Shell payload generator
│ │ └── session.go # Active shell session management
│ ├── db/
│ │ ├── db.go # SQLite connection + migrations
│ │ ├── targets.go # Target CRUD
│ │ ├── dossiers.go # Dossier storage
│ │ ├── sessions.go # Float sessions
│ │ └── jobs.go # Background job tracking
│ └── config/
│ └── config.go # YAML config + defaults
├── web/
│ ├── templates/ # HTML templates (embed.FS)
│ │ ├── base.html # Master layout (port from Python)
│ │ ├── login.html
│ │ ├── dashboard.html
│ │ ├── chat.html
│ │ ├── osint.html
│ │ ├── scanner.html
│ │ ├── hardware.html
│ │ ├── analyze.html
│ │ ├── simulate.html
│ │ ├── targets.html
│ │ ├── exploit.html
│ │ ├── revshell.html
│ │ ├── reports.html
│ │ ├── settings.html
│ │ └── ... (one per feature)
│ └── static/ # CSS/JS/images (embed.FS)
│ ├── css/style.css # Port from Python AUTARCH
│ ├── js/
│ │ ├── app.js # Main app logic (port from Python)
│ │ ├── float-client.js # Float Mode browser-side logic
│ │ ├── hardware-direct.js # WebUSB (for local mode fallback)
│ │ └── lib/
│ │ ├── adb-bundle.js # ADB WebUSB client
│ │ ├── fastboot-bundle.js # Fastboot WebUSB client
│ │ └── esptool-bundle.js # ESP32 Web Serial client
│ └── img/
│ └── autarch.ico
├── data/
│ └── sites/ # OSINT site databases (JSON)
│ ├── sherlock.json
│ ├── maigret.json
│ └── ...
├── build.sh
├── go.mod
└── config.yaml
```
---
## 5. Float Bridge Protocol
### 5.1 Frame Format
All communication over WebSocket using binary frames:
```
┌──────┬──────┬──────┬────────┬─────────────────────┐
│ VER │ TYPE │ SEQ │ LENGTH │ PAYLOAD │
│ 1B │ 1B │ 4B │ 4B │ variable │
└──────┴──────┴──────┴────────┴─────────────────────┘
VER: Protocol version (0x01)
TYPE: Frame type (see below)
SEQ: Sequence number (for request/response matching)
LENGTH: Payload length in bytes (big-endian uint32)
PAYLOAD: Type-specific data (JSON or binary)
```
### 5.2 Frame Types
```
── Control ──────────────────────────────────
0x00 PING Keepalive
0x01 PONG Keepalive response
0x02 HELLO Client registration (capabilities, platform)
0x03 AUTH Session authentication
0x04 ERROR Error response
0x05 DISCONNECT Graceful disconnect
── USB ──────────────────────────────────────
0x10 USB_ENUMERATE List connected USB devices
0x11 USB_ENUMERATE_RESULT Device list response
0x12 USB_OPEN Open device by vid:pid or serial
0x13 USB_OPEN_RESULT Open result (handle ID)
0x14 USB_CLOSE Close device handle
0x15 USB_TRANSFER Bulk/interrupt transfer
0x16 USB_TRANSFER_RESULT Transfer response data
0x17 USB_HOTPLUG Device connected/disconnected event
── ADB (high-level, built on USB) ──────────
0x20 ADB_DEVICES List ADB devices
0x21 ADB_DEVICES_RESULT Device list with state/model
0x22 ADB_SHELL Execute shell command
0x23 ADB_SHELL_RESULT Command output
0x24 ADB_PUSH Push file to device
0x25 ADB_PUSH_DATA File data chunk
0x26 ADB_PUSH_RESULT Push completion
0x27 ADB_PULL Pull file from device
0x28 ADB_PULL_DATA File data chunk
0x29 ADB_PULL_RESULT Pull completion
0x2A ADB_INSTALL Install APK
0x2B ADB_INSTALL_RESULT Install result
0x2C ADB_LOGCAT Start logcat stream
0x2D ADB_LOGCAT_LINE Logcat line
0x2E ADB_REBOOT Reboot device
── Fastboot ─────────────────────────────────
0x30 FB_DEVICES List fastboot devices
0x31 FB_DEVICES_RESULT Device list
0x32 FB_GETVAR Get variable
0x33 FB_GETVAR_RESULT Variable value
0x34 FB_FLASH Flash partition (streamed)
0x35 FB_FLASH_DATA Firmware data chunk
0x36 FB_FLASH_PROGRESS Flash progress update
0x37 FB_FLASH_RESULT Flash completion
0x38 FB_REBOOT Reboot
0x39 FB_OEM_UNLOCK OEM unlock
── Serial ───────────────────────────────────
0x40 SERIAL_LIST List serial ports
0x41 SERIAL_LIST_RESULT Port list
0x42 SERIAL_OPEN Open port (baud, settings)
0x43 SERIAL_OPEN_RESULT Open result
0x44 SERIAL_CLOSE Close port
0x45 SERIAL_WRITE Send data to port
0x46 SERIAL_READ Data received from port
0x47 SERIAL_DETECT_CHIP ESP32 chip detection
0x48 SERIAL_DETECT_RESULT Chip info
── Network Context ──────────────────────────
0x50 NET_INTERFACES List network interfaces
0x51 NET_INTERFACES_RESULT Interface list (IPs, MACs)
0x52 NET_SCAN Scan local network (ARP/ping)
0x53 NET_SCAN_RESULT Host list
0x54 NET_RESOLVE DNS resolve on client network
0x55 NET_RESOLVE_RESULT Resolution result
0x56 NET_CONNECT TCP connect through client
0x57 NET_CONNECT_RESULT Connection result
0x58 NET_MDNS_DISCOVER mDNS service discovery
0x59 NET_MDNS_RESULT Discovered services
── System Context ───────────────────────────
0x60 SYS_INFO Client system info
0x61 SYS_INFO_RESULT OS, arch, hostname, user
0x62 SYS_CLIPBOARD_GET Get clipboard contents
0x63 SYS_CLIPBOARD_DATA Clipboard data
0x64 SYS_CLIPBOARD_SET Set clipboard contents
```
### 5.3 Payload Formats
**HELLO payload (JSON):**
```json
{
"version": "1.0.0",
"platform": "windows",
"arch": "amd64",
"hostname": "user-desktop",
"capabilities": ["usb", "serial", "network", "clipboard"],
"usb_devices": 3,
"serial_ports": 1
}
```
**USB_ENUMERATE_RESULT payload (JSON):**
```json
{
"devices": [
{
"vid": "18d1",
"pid": "4ee7",
"serial": "ABCDEF123456",
"manufacturer": "Google",
"product": "Pixel 8",
"bus": 1,
"address": 4,
"class": "adb"
}
]
}
```
**ADB_SHELL payload (JSON):**
```json
{
"serial": "ABCDEF123456",
"command": "pm list packages -3"
}
```
**USB_TRANSFER payload (binary):**
```
┌──────────┬──────────┬──────┬──────────┐
│ HANDLE │ ENDPOINT │ FLAGS│ DATA │
│ 4B │ 1B │ 1B │ variable │
└──────────┴──────────┴──────┴──────────┘
```
---
## 6. Float Applet (Client-Side)
### 6.1 Options for the Applet
| Option | Pros | Cons |
|--------|------|------|
| **Go native app** (recommended) | Single binary, cross-platform, full USB access via libusb/gousb | Requires download + run |
| **Electron app** | Web technologies, WebUSB built-in | Heavy (~150MB), Chromium overhead |
| **Tauri app** | Lighter than Electron (~10MB), Rust backend | More complex build, newer ecosystem |
| **Browser extension + Web Serial/USB** | No install needed | Limited USB access, Chrome only, no raw USB |
| **Java Web Start / JNLP** | Auto-launch from browser | Dead technology, security warnings |
**Recommendation: Go native app** (5-10MB binary)
The user downloads a small executable. On launch it:
1. Shows a system tray icon with status
2. Connects via WebSocket to the VPS
3. Enumerates local USB, serial, and network
4. Relays commands from the VPS to local hardware
5. Stays running in background until closed
### 6.2 Float Applet Structure
```
float-applet/
├── cmd/
│ └── float/
│ └── main.go # Entry point, tray icon
├── internal/
│ ├── bridge/
│ │ ├── client.go # WebSocket client + reconnect
│ │ ├── protocol.go # Frame parser/builder (shared with server)
│ │ └── handler.go # Dispatch incoming frames to subsystems
│ ├── usb/
│ │ ├── enumerate.go # List USB devices (gousb/libusb)
│ │ ├── device.go # Open/close/transfer
│ │ └── hotplug.go # Device connect/disconnect events
│ ├── adb/
│ │ ├── client.go # ADB protocol implementation
│ │ ├── shell.go # Shell command execution
│ │ ├── sync.go # File push/pull (ADB sync protocol)
│ │ └── logcat.go # Logcat streaming
│ ├── fastboot/
│ │ ├── client.go # Fastboot protocol
│ │ ├── flash.go # Partition flashing
│ │ └── getvar.go # Variable queries
│ ├── serial/
│ │ ├── enumerate.go # List serial ports
│ │ ├── port.go # Open/read/write serial
│ │ └── esp.go # ESP32 chip detection
│ ├── network/
│ │ ├── interfaces.go # List local interfaces
│ │ ├── scan.go # ARP/ping sweep
│ │ ├── proxy.go # TCP proxy for remote connections
│ │ └── mdns.go # mDNS discovery relay
│ └── system/
│ ├── info.go # OS, arch, hostname
│ └── clipboard.go # Clipboard read/write
├── build.sh # Cross-compile: Windows, Linux, macOS
└── go.mod
```
### 6.3 Float Applet User Experience
```
1. User visits AUTARCH CE web dashboard
2. Clicks "Float Mode" button
3. If first time:
a. Page shows download links for their platform (auto-detected)
b. User downloads float-applet binary (~8MB)
c. Runs it — system tray icon appears
4. Applet auto-connects to VPS via WebSocket
5. Dashboard detects connection:
a. Hardware page now shows LOCAL USB devices
b. LAN scanner sees LOCAL network
c. Serial ports show LOCAL COM/ttyUSB ports
6. User works normally — everything feels local
7. Close applet → hardware reverts to VPS context
```
---
## 7. AUTARCH CE Feature Map
### Tier 1: Core (implement first)
| Feature | Source Reference | Go Package |
|---------|----------------|------------|
| Web server + routing | `web/app.py` (183 lines) | `internal/server/` |
| Authentication | `web/auth.py` (73 lines) | `internal/server/auth.go` |
| Dashboard | `web/routes/dashboard.py` | `internal/handlers/dashboard.go` |
| Configuration | `core/config.py` (587 lines) | `internal/config/` |
| Settings UI | `web/routes/settings.py` | `internal/handlers/settings.go` |
| Base template + CSS | `web/templates/base.html`, `style.css` | `web/templates/`, `web/static/` |
### Tier 2: Intelligence (implement second)
| Feature | Source Reference | Go Package |
|---------|----------------|------------|
| LLM chat (API backends) | `core/llm.py` (1400 lines) | `internal/llm/` |
| Agent system | `core/agent.py` (439 lines) | `internal/llm/agent.go` |
| Tool registry | `core/tools.py` | `internal/tools/registry.go` |
| Chat UI | `web/routes/chat.py`, `web/templates/chat.html` | `internal/handlers/chat.go` |
| OSINT engine | `modules/recon.py` | `internal/osint/engine.go` |
| Site databases | `data/sites/*.json` (7,287 sites) | `data/sites/` (embedded) |
| Dossier management | `modules/dossier.py` | `internal/osint/dossier.go` |
| GeoIP | `modules/geoip.py` | `internal/osint/geoip.go` |
### Tier 3: Scanning & Analysis
| Feature | Source Reference | Go Package |
|---------|----------------|------------|
| Port scanner | `web/routes/port_scanner.py` | `internal/scanner/port.go` |
| Network mapper | `modules/net_mapper.py` | `internal/scanner/network.go` |
| Hash toolkit | `modules/analyze.py` (hash section) | `internal/tools/hash.go` |
| Target management | `web/routes/targets.py` | `internal/handlers/targets.go` |
| Threat intel | `modules/threat_intel.py` | `internal/handlers/threat.go` |
| Report engine | `modules/report_engine.py` | `internal/handlers/reports.go` |
### Tier 4: Float Mode + Hardware
| Feature | Source Reference | Go Package |
|---------|----------------|------------|
| Float bridge (server) | NEW | `internal/float/` |
| Hardware manager | `core/hardware.py` (641 lines) | `internal/hardware/` |
| Hardware UI | `web/routes/hardware.py` (417 lines) | `internal/handlers/hardware.go` |
| ADB relay | `core/hardware.py` ADB methods | `internal/hardware/adb.go` |
| Fastboot relay | `core/hardware.py` FB methods | `internal/hardware/fastboot.go` |
| Serial relay | `core/hardware.py` serial methods | `internal/hardware/serial.go` |
### Tier 5: Exploitation & Advanced
| Feature | Source Reference | Go Package |
|---------|----------------|------------|
| Reverse shell | `core/revshell.py`, `modules/revshell.py` | `internal/revshell/` |
| Payload generator | `modules/simulate.py` | `internal/handlers/simulate.go` |
| Android exploit | `core/android_exploit.py` | `internal/handlers/exploit.go` |
| Wireshark (tshark) | `modules/wireshark.py` | `internal/handlers/wireshark.go` |
---
## 8. Estimated Scope
```
AUTARCH Cloud Edition (Go rewrite of web-facing features):
├── Core server + auth + config + dashboard ~2,500 lines
├── LLM client + agent system ~2,000 lines
├── OSINT engine + site DB + dossiers ~2,500 lines
├── Scanner + network tools ~1,500 lines
├── Float bridge protocol + server side ~2,000 lines
├── Hardware manager (Float relay) ~1,500 lines
├── Handlers (all web routes) ~3,000 lines
├── Database layer ~1,000 lines
├── Web templates (HTML) ~3,000 lines
├── CSS + JavaScript ~2,500 lines
└── Total ~21,500 lines
Float Applet (client-side):
├── WebSocket client + reconnect ~500 lines
├── Protocol + frame handling ~800 lines
├── USB enumeration + transfer ~1,000 lines
├── ADB protocol client ~1,500 lines
├── Fastboot protocol client ~800 lines
├── Serial port management ~600 lines
├── Network context (scan, proxy, mDNS) ~1,000 lines
├── System (clipboard, info) ~300 lines
├── Tray icon + UI ~400 lines
└── Total ~6,900 lines
Combined total: ~28,400 lines of Go
```
---
## 9. Build Phases
### Phase 1: Foundation
- Go HTTP server with chi router
- JWT authentication
- Dashboard with system stats
- Configuration management (YAML + UI)
- Base HTML template + CSS (port from Python AUTARCH)
- SQLite database
- **Deliverable:** Working web dashboard you can log into
### Phase 2: Intelligence
- LLM client (Claude, OpenAI, HuggingFace API backends)
- Agent system (THOUGHT/ACTION/PARAMS loop)
- Tool registry
- Chat UI with SSE streaming
- OSINT engine with concurrent site checking
- GeoIP lookups
- Dossier CRUD
- **Deliverable:** AI chat + OSINT fully working
### Phase 3: Tools
- Port scanner with SSE progress
- Network mapper
- Hash toolkit (identify, compute, mutate)
- Target management
- Threat intelligence feed integration
- Report generation
- Reverse shell listener
- **Deliverable:** Full scanning + analysis suite
### Phase 4: Float Mode
- Float bridge protocol implementation (server)
- WebSocket session management
- USB device relay (enumerate, open, transfer)
- ADB command relay
- Fastboot command relay
- Serial port relay
- Hardware UI integration
- **Deliverable:** Connect local hardware to cloud AUTARCH
### Phase 5: Float Applet
- Go native client application
- WebSocket client with auto-reconnect
- USB enumeration via gousb/libusb
- ADB protocol (shell, sync, install)
- Fastboot protocol (flash, getvar)
- Serial port access
- Network context (interfaces, ARP scan, mDNS)
- System tray icon
- Cross-platform build (Windows, Linux, macOS)
- **Deliverable:** Complete Float Mode end-to-end
### Phase 6: Polish
- Exploit modules (Android, iPhone)
- Wireshark integration
- Payload generator
- UI refinement
- Documentation
- Automated tests
- **Deliverable:** Production-ready AUTARCH CE
---
## 10. Key Design Decisions
1. **No local LLM** — VPS won't have GPU. All LLM via API (Claude preferred).
2. **Embedded assets** — Templates, CSS, JS, site databases baked into binary via `embed.FS`.
3. **SQLite not files** — All persistent state in SQLite (not JSON files on disk).
4. **Float is optional** — AUTARCH CE works without Float. Hardware features just show "Connect Float applet" when no bridge is active.
5. **Same UI** — Port the exact HTML/CSS from Python AUTARCH. Users shouldn't notice the difference.
6. **Protocol versioned** — Float bridge protocol has version byte for backward compatibility.
7. **Chunked transfers** — Large files (firmware, APKs) sent in 64KB chunks over the bridge.
8. **Reconnect resilient** — Float applet auto-reconnects. Operations in progress resume or report failure.
9. **Security first** — All bridge communication over WSS (TLS). Session tokens expire. USB transfers validated.
10. **DNS server integrated** — The existing Go DNS server can be imported as a Go package directly.

View File

View File

@ -1,11 +0,0 @@
{
"active": true,
"tier": 2,
"protections": {
"private_dns": "adguard",
"ad_opt_out": true,
"location_accuracy": true,
"diagnostics": true
},
"activated_at": "2026-03-04T09:02:04.669716"
}

View File

@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDKTCCAhGgAwIBAgIUfA3Sef+54+/zn/axqGK99cxqyYkwDQYJKoZIhvcNAQEL
BQAwJDEQMA4GA1UEAwwHQVVUQVJDSDEQMA4GA1UECgwHZGFya0hhbDAeFw0yNjAy
MjExMTAyMTVaFw0zNjAyMTkxMTAyMTVaMCQxEDAOBgNVBAMMB0FVVEFSQ0gxEDAO
BgNVBAoMB2RhcmtIYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5
2L7xG2kZLj8u1aA0qFd9Xohxa0XG1K0xhTkWJmNOjgRdRO9RejWhKvpa2DJNTO9L
LyEO8bRH56zKcgFofAJRe4GjCSk3OefBcuCKHBWN+hB1YRu+7spaoTxZ1m5dRP1o
DvsRe/nSA69xGsEbX8Zuc/ROCsaV4LACOBYSMQkOKTWWpTu2cLJyuW/sqHn5REzp
Bndw1sp5p+TCc2+Pf+dCEx1V2lXCt2sWC5jTHvPzwGgy9jNXi+CtKMJRlGrHUmBW
a9woL3caOdAp1i9t6VmXeRO3PBYsByeyuGJoREVRThHu+ZhzQkz3oHGFO5YJbu/o
OKWwWJ9mQUl6jF1uwNTtAgMBAAGjUzBRMB0GA1UdDgQWBBS3bxJnHddd56q+WltD
VsbewxdDVDAfBgNVHSMEGDAWgBS3bxJnHddd56q+WltDVsbewxdDVDAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCceD5savXA4dhGUss0D8DPIvSg
DS3mjnJtaD7SFqfDmyqM8W9ocQK7yrzdQiywZT+dI8dCnVm1hB5e5l3lTZwTLU41
XLq4WdHBeimwWIuZl+pKKvXcQUHUkK4epJFrt6mj0onExNSDNI4i7Xk+XnMVIu35
VrF6IhLrD2AznQyOqY0WeLGmoXe3FT5caUiTm5Kg28xTJC9m7hDOFE34d0Aqb+U1
U4GFlmXor+MdNKYTEJJy3pslxEZOiRNiiLKWjecYrcKfSk0LY/8TkqVB44pZBQZB
6naQfFuuxDtEa6bHM0q+P/6HM35tpEu6TEJ1eU/yRrejhySFIHfKTjy/WXsm
-----END CERTIFICATE-----

View File

@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDWTCCAkGgAwIBAgIUOzmq7jW+wmCJdR9vdQNaC7/DJCwwDQYJKoZIhvcNAQEL
BQAwPDEYMBYGA1UEAwwPbWFpbC5nb29nbGUuY29tMRMwEQYDVQQKDApHb29nbGUg
TExDMQswCQYDVQQGEwJVUzAeFw0yNjAzMDMxMjA2MDFaFw0yNzAzMDMxMjA2MDFa
MDwxGDAWBgNVBAMMD21haWwuZ29vZ2xlLmNvbTETMBEGA1UECgwKR29vZ2xlIExM
QzELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCO
dJygnzih+j44Z7O08n8lWfIpkEBFQtLeoWWbUhi66uIGnISw0x41LxQRSa0pM/cK
1dkQV9olBxOcmFY6XaT8YP7AXt5NkvH0Y/3vE2JHRJpxw0W8ug2tX4pwWCXMkJn2
/Ih2d/VBzDLKp4UK+KTse+2qrFRsvReoOuWzXBqpLC2Ch4pvz1skmjA/hsH7OiWx
ADeBrtphh+1vHhMM27x6D0i3K0tSvhoZBamjXt7qzjPtPGj7dXlHB+S6LkAJC5pF
vL5GYTc5gSceoUzgBFWVVfLP2TYYyDpss/LFnWnvWMqqrvsW8WNaMmHeOI9RA+Q+
rcOjxi7VwDmjm6iwvWFNAgMBAAGjUzBRMB0GA1UdDgQWBBQzYwznwTj7ZM89NikD
ty1B33oAlDAfBgNVHSMEGDAWgBQzYwznwTj7ZM89NikDty1B33oAlDAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBhp5GxpRz0lw4uRhJvw3ewhPX6
UHBnWqMb3g4e3zc7RVWqcN4gj9j4ZTFoJxOs2Hw+VfO1i+x3/f4CSxmFrd6FcqNl
B7rRl1+9zup6Me2EQ+XM6mS4Xwf6gmjSvetpcpJAk42c52JdiXq29zZgAPG9n7iz
DrHN70wsB/xGbA2XqcwsOuy3uoBR3TSj9ka3gzrRC1JkP0phcKxlxUYigWaBB/uH
pl5APHqN5fvPyXkiTdX0YQpnRGONm+aMO/LUutIZj4dghQdpJBdDQgv7r3MZl5Z5
Q1UWqnkFwgO4/sjd7yU7u7DODV5/QIzJ9BWRyhIOXiTArU1+M80SP79WJHKa
-----END CERTIFICATE-----

View File

@ -1,8 +0,0 @@
{
"enabled": false,
"auto_block_top_talkers": true,
"auto_enable_syn_cookies": true,
"connection_threshold": 100,
"syn_threshold": 50,
"updated": "2026-03-02T23:30:44.437461"
}

View File

@ -1,10 +0,0 @@
{
"listen_dns": "10.0.0.56:53",
"listen_api": "127.0.0.1:5380",
"api_token": "5ed79350fed2490d2aca6f3b29776365",
"upstream": [],
"cache_ttl": 300,
"zones_dir": "C:\\she\\autarch\\data\\dns\\zones",
"dnssec_keys_dir": "C:\\she\\autarch\\data\\dns\\keys",
"log_queries": true
}

View File

@ -1,53 +0,0 @@
{
"domain": "autarch.local",
"soa": {
"primary_ns": "ns1.autarch.local",
"admin_email": "admin.autarch.local",
"serial": 1772537115,
"refresh": 3600,
"retry": 600,
"expire": 86400,
"min_ttl": 300
},
"records": [
{
"id": "ns1",
"type": "NS",
"name": "autarch.local.",
"value": "ns1.autarch.local.",
"ttl": 3600
},
{
"id": "mx1",
"type": "MX",
"name": "autarch.local.",
"value": "mx.autarch.local.",
"ttl": 3600,
"priority": 10
},
{
"id": "spf1",
"type": "TXT",
"name": "autarch.local.",
"value": "v=spf1 ip4:127.0.0.1 -all",
"ttl": 3600
},
{
"id": "dmarc1",
"type": "TXT",
"name": "_dmarc.autarch.local.",
"value": "v=DMARC1; p=none; rua=mailto:dmarc@autarch.local",
"ttl": 3600
},
{
"id": "r1772537722879235900",
"type": "A",
"name": "https://autarch.local",
"value": "10.0.0.56:8181",
"ttl": 300
}
],
"dnssec": true,
"created_at": "2026-03-03T11:25:07Z",
"updated_at": "2026-03-03T12:24:00Z"
}

View File

@ -1,2 +0,0 @@
Site,URL,Category,Status,Confidence
GitHub,https://github.com/test,,good,85
1 Site URL Category Status Confidence
2 GitHub https://github.com/test good 85

View File

@ -1,13 +0,0 @@
{
"query": "testuser",
"exported": "2026-02-14T04:18:34.669640",
"total_results": 1,
"results": [
{
"name": "GitHub",
"url": "https://github.com/test",
"status": "good",
"rate": 85
}
]
}

View File

@ -1,29 +0,0 @@
[
{
"target": "67.183.122.213",
"scan_time": "2026-03-08T23:37:06.286067+00:00",
"duration": 6.04,
"open_ports": [],
"backdoors": [],
"os_guess": "",
"smb_info": {}
},
{
"target": "67.183.122.213",
"scan_time": "2026-03-08T23:48:58.061607+00:00",
"duration": 6.03,
"open_ports": [],
"backdoors": [],
"os_guess": "",
"smb_info": {}
},
{
"target": "67.183.122.213",
"scan_time": "2026-03-09T00:48:50.872756+00:00",
"duration": 3.01,
"open_ports": [],
"backdoors": [],
"os_guess": "",
"smb_info": {}
}
]

View File

@ -1,98 +0,0 @@
You are Hal, the AI agent powering Project AUTARCH — an autonomous security platform built by darkHal Security Group.
## Your Capabilities
You can read files, write files, execute shell commands, search the codebase, and create new AUTARCH modules on demand. When a user asks you to build a tool or module, you build it.
## AUTARCH Codebase Structure
- `modules/` — Plugin modules (Python files). Each one is a standalone tool.
- `core/` — Framework internals (llm.py, agent.py, tools.py, config.py, wireshark.py, etc.)
- `web/` — Flask web dashboard (routes/, templates/, static/)
- `data/` — Databases, configs, JSON files
- `models/` — LLM model files (GGUF)
## Module Categories
| Category | Color | Purpose |
|----------|-------|---------|
| defense | Blue | Security hardening, monitoring, firewalls |
| offense | Red | Penetration testing, exploitation |
| counter | Purple | Counter-intelligence, threat response |
| analyze | Cyan | Analysis, forensics, packet inspection |
| osint | Green | Open source intelligence gathering |
| simulate | Yellow | Attack simulation, red team exercises |
## How to Create a Module
Every module in `modules/` MUST have these attributes and a `run()` function:
```python
"""
Module description docstring
"""
import os
import sys
import subprocess
from pathlib import Path
# Module metadata — REQUIRED
DESCRIPTION = "What this module does"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "defense" # One of: defense, offense, counter, analyze, osint, simulate
sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors, clear_screen, display_banner
class ModuleClassName:
"""Main class for this module."""
def print_status(self, message, status="info"):
colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
def run_cmd(self, cmd, timeout=30):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return r.returncode == 0, r.stdout.strip()
except Exception as e:
return False, str(e)
# Add your methods here...
def run():
"""Entry point for CLI mode."""
mod = ModuleClassName()
# Interactive menu or direct execution
```
## Important Rules
1. Use the `create_module` tool to write modules — it validates and saves them automatically
2. Always include the metadata: DESCRIPTION, AUTHOR, VERSION, CATEGORY
3. Always include a `run()` function
4. Use `subprocess.run()` for system commands — support both Windows (PowerShell/netsh) and Linux (bash)
5. Import from `core.banner` for Colors
6. Module filenames should be lowercase with underscores (e.g., `port_scanner.py`)
7. Study existing modules with `read_file` if you need to understand patterns
8. The web dashboard discovers modules automatically from the `modules/` directory
## Platform
This system runs on Windows. Use PowerShell commands where appropriate, but also support Linux fallbacks.
## Existing Modules (for reference)
- defender.py — System hardening checks (CATEGORY: defense)
- defender_windows.py — Windows-native security checks (CATEGORY: defense)
- defender_monitor.py — Real-time threat monitoring (CATEGORY: defense)
- recon.py — Network reconnaissance (CATEGORY: offense)
- counter.py — Counter-intelligence tools (CATEGORY: counter)
- adultscan.py — Adult content scanner (CATEGORY: analyze)
- agent_hal.py — AI security automation (CATEGORY: core)
- wireshark.py — Packet analysis (CATEGORY: analyze)
- hardware_local.py — Hardware interaction (CATEGORY: hardware)
## How You Should Respond
- For simple questions: answer directly
- For module creation requests: use the create_module tool
- For system queries: use the shell tool
- For code exploration: use read_file and search_files
- Always explain what you're doing and why

View File

@ -1,129 +0,0 @@
{
"session_id": "10_0_0_56_20260214_010220",
"target": "10.0.0.56",
"state": "completed",
"created_at": "2026-02-14T01:02:20.746609",
"updated_at": "2026-02-14T01:12:20.951316",
"notes": "",
"step_count": 0,
"tree": {
"target": "10.0.0.56",
"created_at": "2026-02-14T01:02:20.746597",
"updated_at": "2026-02-14T01:02:20.746742",
"root_nodes": [
"e0d00dbc",
"cf120ead",
"6f4a664c",
"814f0376",
"5b602881",
"4d2e70e8"
],
"nodes": {
"e0d00dbc": {
"id": "e0d00dbc",
"label": "Reconnaissance",
"node_type": "reconnaissance",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Information gathering and target enumeration",
"tool_output": null,
"findings": [],
"priority": 1,
"created_at": "2026-02-14T01:02:20.746668",
"updated_at": "2026-02-14T01:02:20.746668"
},
"cf120ead": {
"id": "cf120ead",
"label": "Initial Access",
"node_type": "initial_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Gaining initial foothold on target",
"tool_output": null,
"findings": [],
"priority": 2,
"created_at": "2026-02-14T01:02:20.746685",
"updated_at": "2026-02-14T01:02:20.746685"
},
"6f4a664c": {
"id": "6f4a664c",
"label": "Privilege Escalation",
"node_type": "privilege_escalation",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Escalating from initial access to higher privileges",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-02-14T01:02:20.746699",
"updated_at": "2026-02-14T01:02:20.746699"
},
"814f0376": {
"id": "814f0376",
"label": "Lateral Movement",
"node_type": "lateral_movement",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Moving to other systems in the network",
"tool_output": null,
"findings": [],
"priority": 4,
"created_at": "2026-02-14T01:02:20.746711",
"updated_at": "2026-02-14T01:02:20.746711"
},
"5b602881": {
"id": "5b602881",
"label": "Credential Access",
"node_type": "credential_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Obtaining credentials and secrets",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-02-14T01:02:20.746726",
"updated_at": "2026-02-14T01:02:20.746726"
},
"4d2e70e8": {
"id": "4d2e70e8",
"label": "Persistence",
"node_type": "persistence",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Maintaining access to compromised systems",
"tool_output": null,
"findings": [],
"priority": 5,
"created_at": "2026-02-14T01:02:20.746739",
"updated_at": "2026-02-14T01:02:20.746739"
}
}
},
"events": [
{
"timestamp": "2026-02-14T01:02:20.746747",
"event_type": "state_change",
"data": {
"from": "idle",
"to": "running"
}
},
{
"timestamp": "2026-02-14T01:12:20.951316",
"event_type": "state_change",
"data": {
"from": "running",
"to": "completed",
"summary": ""
}
}
],
"findings": [],
"pipeline_history": []
}

View File

@ -1,120 +0,0 @@
{
"session_id": "192_168_1_100_20260127_202421",
"target": "192.168.1.100",
"state": "running",
"created_at": "2026-01-27T20:24:21.604010",
"updated_at": "2026-01-27T20:24:21.604098",
"notes": "",
"step_count": 0,
"tree": {
"target": "192.168.1.100",
"created_at": "2026-01-27T20:24:21.604003",
"updated_at": "2026-01-27T20:24:21.604091",
"root_nodes": [
"4be13ed9",
"8dc38740",
"22ee2768",
"2c45477f",
"6f793ae8",
"778fc896"
],
"nodes": {
"4be13ed9": {
"id": "4be13ed9",
"label": "Reconnaissance",
"node_type": "reconnaissance",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Information gathering and target enumeration",
"tool_output": null,
"findings": [],
"priority": 1,
"created_at": "2026-01-27T20:24:21.604032",
"updated_at": "2026-01-27T20:24:21.604032"
},
"8dc38740": {
"id": "8dc38740",
"label": "Initial Access",
"node_type": "initial_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Gaining initial foothold on target",
"tool_output": null,
"findings": [],
"priority": 2,
"created_at": "2026-01-27T20:24:21.604044",
"updated_at": "2026-01-27T20:24:21.604044"
},
"22ee2768": {
"id": "22ee2768",
"label": "Privilege Escalation",
"node_type": "privilege_escalation",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Escalating from initial access to higher privileges",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-27T20:24:21.604056",
"updated_at": "2026-01-27T20:24:21.604056"
},
"2c45477f": {
"id": "2c45477f",
"label": "Lateral Movement",
"node_type": "lateral_movement",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Moving to other systems in the network",
"tool_output": null,
"findings": [],
"priority": 4,
"created_at": "2026-01-27T20:24:21.604066",
"updated_at": "2026-01-27T20:24:21.604066"
},
"6f793ae8": {
"id": "6f793ae8",
"label": "Credential Access",
"node_type": "credential_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Obtaining credentials and secrets",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-27T20:24:21.604077",
"updated_at": "2026-01-27T20:24:21.604077"
},
"778fc896": {
"id": "778fc896",
"label": "Persistence",
"node_type": "persistence",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Maintaining access to compromised systems",
"tool_output": null,
"findings": [],
"priority": 5,
"created_at": "2026-01-27T20:24:21.604088",
"updated_at": "2026-01-27T20:24:21.604088"
}
}
},
"events": [
{
"timestamp": "2026-01-27T20:24:21.604098",
"event_type": "state_change",
"data": {
"from": "idle",
"to": "running"
}
}
],
"findings": [],
"pipeline_history": []
}

View File

@ -1,120 +0,0 @@
{
"session_id": "192_168_50_78_20260130_133833",
"target": "192.168.50.78",
"state": "running",
"created_at": "2026-01-30T13:38:33.830336",
"updated_at": "2026-01-30T13:38:33.830464",
"notes": "",
"step_count": 0,
"tree": {
"target": "192.168.50.78",
"created_at": "2026-01-30T13:38:33.830323",
"updated_at": "2026-01-30T13:38:33.830460",
"root_nodes": [
"e4c40c28",
"ddd63828",
"b3f2634d",
"9c162c78",
"aa40d5a3",
"0c50a23d"
],
"nodes": {
"e4c40c28": {
"id": "e4c40c28",
"label": "Reconnaissance",
"node_type": "reconnaissance",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Information gathering and target enumeration",
"tool_output": null,
"findings": [],
"priority": 1,
"created_at": "2026-01-30T13:38:33.830390",
"updated_at": "2026-01-30T13:38:33.830390"
},
"ddd63828": {
"id": "ddd63828",
"label": "Initial Access",
"node_type": "initial_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Gaining initial foothold on target",
"tool_output": null,
"findings": [],
"priority": 2,
"created_at": "2026-01-30T13:38:33.830408",
"updated_at": "2026-01-30T13:38:33.830408"
},
"b3f2634d": {
"id": "b3f2634d",
"label": "Privilege Escalation",
"node_type": "privilege_escalation",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Escalating from initial access to higher privileges",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-30T13:38:33.830421",
"updated_at": "2026-01-30T13:38:33.830421"
},
"9c162c78": {
"id": "9c162c78",
"label": "Lateral Movement",
"node_type": "lateral_movement",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Moving to other systems in the network",
"tool_output": null,
"findings": [],
"priority": 4,
"created_at": "2026-01-30T13:38:33.830433",
"updated_at": "2026-01-30T13:38:33.830433"
},
"aa40d5a3": {
"id": "aa40d5a3",
"label": "Credential Access",
"node_type": "credential_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Obtaining credentials and secrets",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-30T13:38:33.830445",
"updated_at": "2026-01-30T13:38:33.830445"
},
"0c50a23d": {
"id": "0c50a23d",
"label": "Persistence",
"node_type": "persistence",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Maintaining access to compromised systems",
"tool_output": null,
"findings": [],
"priority": 5,
"created_at": "2026-01-30T13:38:33.830457",
"updated_at": "2026-01-30T13:38:33.830457"
}
}
},
"events": [
{
"timestamp": "2026-01-30T13:38:33.830464",
"event_type": "state_change",
"data": {
"from": "idle",
"to": "running"
}
}
],
"findings": [],
"pipeline_history": []
}

View File

@ -1,120 +0,0 @@
{
"session_id": "example_com_20260128_192244",
"target": "example.com",
"state": "running",
"created_at": "2026-01-28T19:22:44.670292",
"updated_at": "2026-01-28T19:22:44.670428",
"notes": "test",
"step_count": 0,
"tree": {
"target": "example.com",
"created_at": "2026-01-28T19:22:44.670279",
"updated_at": "2026-01-28T19:22:44.670423",
"root_nodes": [
"466dcf04",
"55991daa",
"e3209082",
"af036f87",
"633c0eeb",
"8584f7fc"
],
"nodes": {
"466dcf04": {
"id": "466dcf04",
"label": "Reconnaissance",
"node_type": "reconnaissance",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Information gathering and target enumeration",
"tool_output": null,
"findings": [],
"priority": 1,
"created_at": "2026-01-28T19:22:44.670353",
"updated_at": "2026-01-28T19:22:44.670353"
},
"55991daa": {
"id": "55991daa",
"label": "Initial Access",
"node_type": "initial_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Gaining initial foothold on target",
"tool_output": null,
"findings": [],
"priority": 2,
"created_at": "2026-01-28T19:22:44.670371",
"updated_at": "2026-01-28T19:22:44.670371"
},
"e3209082": {
"id": "e3209082",
"label": "Privilege Escalation",
"node_type": "privilege_escalation",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Escalating from initial access to higher privileges",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-28T19:22:44.670384",
"updated_at": "2026-01-28T19:22:44.670384"
},
"af036f87": {
"id": "af036f87",
"label": "Lateral Movement",
"node_type": "lateral_movement",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Moving to other systems in the network",
"tool_output": null,
"findings": [],
"priority": 4,
"created_at": "2026-01-28T19:22:44.670397",
"updated_at": "2026-01-28T19:22:44.670397"
},
"633c0eeb": {
"id": "633c0eeb",
"label": "Credential Access",
"node_type": "credential_access",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Obtaining credentials and secrets",
"tool_output": null,
"findings": [],
"priority": 3,
"created_at": "2026-01-28T19:22:44.670408",
"updated_at": "2026-01-28T19:22:44.670408"
},
"8584f7fc": {
"id": "8584f7fc",
"label": "Persistence",
"node_type": "persistence",
"status": "todo",
"parent_id": null,
"children": [],
"details": "Maintaining access to compromised systems",
"tool_output": null,
"findings": [],
"priority": 5,
"created_at": "2026-01-28T19:22:44.670420",
"updated_at": "2026-01-28T19:22:44.670420"
}
}
},
"events": [
{
"timestamp": "2026-01-28T19:22:44.670428",
"event_type": "state_change",
"data": {
"from": "idle",
"to": "running"
}
}
],
"findings": [],
"pipeline_history": []
}

View File

@ -1,761 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<smses count="758" backup_date="1772581491998" type="full" autarch_version="2.3">
<sms protocol="0" address="+12066990041" date="1772578925000" type="2" body="Well I dated chicks before I came out, you know the whole pretending your not gay thing...so it could have worked" read="1" status="-1" locked="0" date_sent="1772578925000" readable_date="2026-03-03 23:02:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066057095" date="1772578602000" type="1" body="K" read="1" status="-1" locked="0" date_sent="1772578602000" readable_date="2026-03-03 22:56:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772578582000" type="2" body="My biweekly court check-in is tomorrow at 11am just fyi" read="1" status="-1" locked="0" date_sent="1772578582000" readable_date="2026-03-03 22:56:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772576763000" type="2" body="Just last thing I'm going to say, maybe 10-15 years ago you could trust a cop. But these days its not about policing, its about making arrests, budgets and income for the city's. Cops are not honest people and are not on the peoples side. They are not as bad as ICE shooting people, but they aren't much better" read="1" status="-1" locked="0" date_sent="1772576763000" readable_date="2026-03-03 22:26:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772576009000" type="2" body="They are going to want it all. Sorry I didn't see your text" read="1" status="-1" locked="0" date_sent="1772576009000" readable_date="2026-03-03 22:13:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772575992000" type="2" body="I don't think that's how they work" read="1" status="-1" locked="0" date_sent="1772575992000" readable_date="2026-03-03 22:13:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772569991000" type="1" body="Wondering if you can talk to lawyer and say you only need 2 hours now and may want to put him on retainer later" read="1" status="-1" locked="0" date_sent="1772569991000" readable_date="2026-03-03 20:33:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772568512000" type="1" body="I usually have the cleaning lady do fridge when we're gone but havent" read="1" status="-1" locked="0" date_sent="1772568512000" readable_date="2026-03-03 20:08:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772568388000" type="2" body="I was planning on it" read="1" status="-1" locked="0" date_sent="1772568388000" readable_date="2026-03-03 20:06:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772568365000" type="1" body="Please throw away" read="1" status="-1" locked="0" date_sent="1772568365000" readable_date="2026-03-03 20:06:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772568343000" type="2" body="Did you know you have a pound of butter that expired in 2022" read="1" status="-1" locked="0" date_sent="1772568343000" readable_date="2026-03-03 20:05:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+19168990823" date="1772568288000" type="1" body="I was gonna make a switch hitter joke but I know you dont swing both ways" read="1" status="-1" locked="0" date_sent="1772568288000" readable_date="2026-03-03 20:04:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1772568235000" type="1" body="We really need to go to a few games this year. I think I only saw 2 games last year" read="1" status="-1" locked="0" date_sent="1772568235000" readable_date="2026-03-03 20:03:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772568163000" type="2" body="I'll have two balls and a bat" read="1" status="-1" locked="0" date_sent="1772568163000" readable_date="2026-03-03 20:02:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772568126000" type="1" body="I dont watch the news so I guess ill se it on sports center" read="1" status="-1" locked="0" date_sent="1772568126000" readable_date="2026-03-03 20:02:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772563103000" type="2" body="So idk up to you guys" read="1" status="-1" locked="0" date_sent="1772563103000" readable_date="2026-03-03 18:38:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772563092000" type="2" body="In other words I didn't murder or have a high profile so hes done with us" read="1" status="-1" locked="0" date_sent="1772563092000" readable_date="2026-03-03 18:38:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772563045000" type="2" body="Brad says higher a lawyer, but he is unavailable. I think he's politely saying I took your money when I was good lawyer but I'm too expensive now that im rated in the top 10 in Washington state for criminal defense" read="1" status="-1" locked="0" date_sent="1772563045000" readable_date="2026-03-03 18:37:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772554548000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772554548000" readable_date="2026-03-03 16:15:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772554535000" type="1" body="We're up and ready" read="1" status="-1" locked="0" date_sent="1772554535000" readable_date="2026-03-03 16:15:35 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+14259234274" date="1772541457000" type="1" body="I’m praying for you brother. I really want you to reach out to the lord and read the Bible, it can give you clarity in times like this. Don’t overthink it just read it and pray. I will do the same for you today brother. In Jesus Christ name you will be sane and blessings may be upon you. In the lords name I pray AMEN." read="1" status="-1" locked="0" date_sent="1772541457000" readable_date="2026-03-03 12:37:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772540298000" type="2" body="Guys I'm telling you i think I'm mentally decompensating. And its pretty serious. My parents are even worried. After I got spooked I left my place and haven't been back since. I can't even open the front door without being drenched in sweat (more than I already am). So I just wanted you guys to know what's going on if you suddenly don't hear from me. Its because I'm either in jail or taking time to get my brain healthy. Just to much going on and I cant/don't know how to deal with it. You two are my 2 best work friends and good friends outside if work, even though we have only hung out like twice lol. But my group of friends i usually kick it with that have been coming over checking on me has already told me I lost my marbles. I just didn't want y'all to worry if you don't see or hear from me for bit or you see me on king 5 streaking across the baseball diamond opening day" read="1" status="-1" locked="0" date_sent="1772540298000" readable_date="2026-03-03 12:18:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1772539595000" type="1" body="Sounds like you are 100% overthinking bro. Just keep your head down and focus on yourself and I’m praying for you too brother!" read="1" status="-1" locked="0" date_sent="1772539595000" readable_date="2026-03-03 12:06:35 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1772527992000" type="1" body="Just relax dude" read="1" status="-1" locked="0" date_sent="1772527992000" readable_date="2026-03-03 08:53:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772527032000" type="2" body="It is, it reminds me of going to my aunts ranch as a kid and staying there." read="1" status="-1" locked="0" date_sent="1772527032000" readable_date="2026-03-03 08:37:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772526965000" type="1" body="Sounds nice" read="1" status="-1" locked="0" date_sent="1772526965000" readable_date="2026-03-03 08:36:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772526922000" type="2" body="They got a sick little farm in carnation with some horses and trails" read="1" status="-1" locked="0" date_sent="1772526922000" readable_date="2026-03-03 08:35:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1772526847000" type="2" body="Im taking a break right now, staying at buddies while they are gone and taking break from shit" read="1" status="-1" locked="0" date_sent="1772526847000" readable_date="2026-03-03 08:34:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772526839000" type="1" body="Take a spa day bud" read="1" status="-1" locked="0" date_sent="1772526839000" readable_date="2026-03-03 08:33:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772526792000" type="2" body="Barely at all, and I hope so, but the last few days have been really bad. My shrink told me its PTSD sympotom call hyper vigilance due to traumatic experience, I call it being trapped in a nightmare that doesn't ever end." read="1" status="-1" locked="0" date_sent="1772526792000" readable_date="2026-03-03 08:33:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772526737000" type="1" body="Maybe you should get an Airbnb cabin somewhere. I did that with my cousins a couple weekends ago in Gold Bar" read="1" status="-1" locked="0" date_sent="1772526737000" readable_date="2026-03-03 08:32:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1772526670000" type="1" body="How much are you sleeping?" read="1" status="-1" locked="0" date_sent="1772526670000" readable_date="2026-03-03 08:31:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1772526660000" type="1" body="I think your just stressed out." read="1" status="-1" locked="0" date_sent="1772526660000" readable_date="2026-03-03 08:31:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772526272000" type="2" body="Don't go spreading that around work lol. Just if you don't see or hear from me for a bit its because I check myself into a treatment center" read="1" status="-1" locked="0" date_sent="1772526272000" readable_date="2026-03-03 08:24:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1772526196000" type="2" body="Awe thanks...but srsly, I have such bad PTSD from my last encounter (I never told you guys everything that happened when I was arrested. You have to remember what the charges were, people accused of what I was don't get the 5 star treatment) that now when I see cops I totally freakout, this time idk, I feel like they are watching me, following me and going to pounce on me as soon as I step outside. I hate to say it guys, but I think I'm mentally unwinding." read="1" status="-1" locked="0" date_sent="1772526196000" readable_date="2026-03-03 08:23:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772525969000" type="1" body="I always do" read="1" status="-1" locked="0" date_sent="1772525969000" readable_date="2026-03-03 08:19:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772525963000" type="2" body="Idk" read="1" status="-1" locked="0" date_sent="1772525963000" readable_date="2026-03-03 08:19:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1772525959000" type="2" body="Pray for me" read="1" status="-1" locked="0" date_sent="1772525959000" readable_date="2026-03-03 08:19:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772525947000" type="1" body="I dont know what to say" read="1" status="-1" locked="0" date_sent="1772525947000" readable_date="2026-03-03 08:19:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772525920000" type="2" body="I'm not even kidding, ever since last week when I saw a cop outside my apartments I've been losing my mind and its starting to scare me. Like no joke." read="1" status="-1" locked="0" date_sent="1772525920000" readable_date="2026-03-03 08:18:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1772525815000" type="1" body="Damn dude" read="1" status="-1" locked="0" date_sent="1772525815000" readable_date="2026-03-03 08:16:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772524680000" type="2" body="Or both" read="1" status="-1" locked="0" date_sent="1772524680000" readable_date="2026-03-03 07:58:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1772524664000" type="2" body="I think I am having a psychotic breakdown from everything going on or I'm being stalked by the police" read="1" status="-1" locked="0" date_sent="1772524664000" readable_date="2026-03-03 07:57:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066125383" date="1772508463000" type="1" body="Understand" read="1" status="-1" locked="0" date_sent="1772508463000" readable_date="2026-03-03 03:27:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772508166000" type="2" body="I will. If it turns out to be nothing I'm going to go back to my shrink" read="1" status="-1" locked="0" date_sent="1772508166000" readable_date="2026-03-03 03:22:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772508041000" type="1" body="Lets see what tomorrow brings. Get some sleep and have Alexa wake you up" read="1" status="-1" locked="0" date_sent="1772508041000" readable_date="2026-03-03 03:20:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772491822000" type="2" body="Something in that field" read="1" status="-1" locked="0" date_sent="1772491822000" readable_date="2026-03-02 22:50:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD1yem1zTE1PUjVDam5GREdEQndja3cqEI" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1772491807000" type="2" body="I think you would be a good fit for jobs like procurement specialist, supply chain analysts or" read="1" status="-1" locked="0" date_sent="1772491807000" readable_date="2026-03-02 22:50:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDItNU0wVDNOVHd5N3hZNVA1clRxcXcqED" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1772491278000" type="2" body="Your cover letter and resume look good, I would just tailor your job descriptions a little more based on the job your applying for. On Boeing job postings there is a position responsibilities sections, kinda see how they word things and try and use their words. An example I used with my friend is the job posting says &quot;Assist colleagues by answering questions and sharing information about work methods, processes and procedures&quot; what you could put in your group health job description is something like &quot;As part of my duties, I assisted my coworkers by sharing the latest processes and procedures to make sure my team stayed current on our operational methods&quot;, kinda regurgitate what the job posting is saying, that way you hit the keywords they are telling. They also like the job descriptions to be more of a narrative of what you did. Its the same with the interviews, they like the answers to be a story. They will ask things like &quot;Tell me about a time you didn't meet a deadline and what did you learn.&quot; The response should; Tell the situation (While working at xyz I was part of a group helping reorganizing the 123 databases when our network went down) Then explain your task (My job was to gather all the files from 2012-20214 and do zzz with them) then your actions (Since we could not access the digital copies, I had to find the original paper copies and do the work manually and it took much longer than anticipated as I was not familiar with our filing systems.) Then the result (This caused me not to be able to finish my task on time and caused my team to finish our project late. After the project I took some extra time to better familiarize my self on backup procedures and shared them with the rest of my team so if the system went down again we would be better prepared and wouldn't fall behind.)" read="1" status="-1" locked="0" date_sent="1772491278000" readable_date="2026-03-02 22:41:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFlkb3BjNldFUTFPRFlYZE05Yi00blEqEB" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1772479039000" type="2" body="Wrong person" read="1" status="-1" locked="0" date_sent="1772479039000" readable_date="2026-03-02 19:17:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDN2bHVCd1BmVFJDcVp3WjRRUlJVTXcqEE" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1772479026000" type="2" body="R u here?" read="1" status="-1" locked="0" date_sent="1772479026000" readable_date="2026-03-02 19:17:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGlhbE5UOWpQVHN5M214RDlVVWZJREEqEE" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066125383" date="1772478986000" type="1" body="Just let someone in" read="1" status="-1" locked="0" date_sent="1772478986000" readable_date="2026-03-02 19:16:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772478122000" type="2" body="My buddy Brandon is coming over in about an hour to go over getting on at Boeing, if you wanna join us feel free. We are meeting at my parents, 222 bell street, unit 401, Edmonds 98020" read="1" status="-1" locked="0" date_sent="1772478122000" readable_date="2026-03-02 19:02:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEtac2ZOeWtzUm4tbnF1dG1FTlVtWGcqEB" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066125383" date="1772475845000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1772475845000" readable_date="2026-03-02 18:24:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772475807000" type="2" body="Still waiting, his paralegal is gonna do some calling around then have brad look into it after morning court" read="1" status="-1" locked="0" date_sent="1772475807000" readable_date="2026-03-02 18:23:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772475789000" type="1" body="Sometimes you may need to go downstairs and let people in." read="1" status="-1" locked="0" date_sent="1772475789000" readable_date="2026-03-02 18:23:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772475760000" type="1" body="What did you get from Brad" read="1" status="-1" locked="0" date_sent="1772475760000" readable_date="2026-03-02 18:22:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772475727000" type="2" body="My buddy Brandon is coming over. He needs help getting a job at Boeing, but I also told him what's going on and that I need him to tell me if I have gone nuts, to" read="1" status="-1" locked="0" date_sent="1772475727000" readable_date="2026-03-02 18:22:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066571038" date="1772474928000" type="1" body="It's all good good morning" read="1" status="-1" locked="0" date_sent="1772474928000" readable_date="2026-03-02 18:08:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEFxS1lPZVAyU2N5N1RYMDlSa25EamcqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772472191000" type="1" body="Just opened door" read="1" status="-1" locked="0" date_sent="1772472191000" readable_date="2026-03-02 17:23:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772471302000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1772471302000" readable_date="2026-03-02 17:08:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772471290000" type="2" body="Ours" read="1" status="-1" locked="0" date_sent="1772471290000" readable_date="2026-03-02 17:08:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772471280000" type="1" body="Our time or theres?" read="1" status="-1" locked="0" date_sent="1772471280000" readable_date="2026-03-02 17:08:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772471259000" type="2" body="9am on the email from Ashley" read="1" status="-1" locked="0" date_sent="1772471259000" readable_date="2026-03-02 17:07:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772471225000" type="1" body="No problem, just want to know" read="1" status="-1" locked="0" date_sent="1772471225000" readable_date="2026-03-02 17:07:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772471198000" type="2" body="I'll have to look but I think its real early" read="1" status="-1" locked="0" date_sent="1772471198000" readable_date="2026-03-02 17:06:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772471159000" type="1" body="What time is court tomorrow" read="1" status="-1" locked="0" date_sent="1772471159000" readable_date="2026-03-02 17:05:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772470941000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1772470941000" readable_date="2026-03-02 17:02:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772470907000" type="2" body="Brads paralegel just called, he's looking into some things. I have a delivery coming" read="1" status="-1" locked="0" date_sent="1772470907000" readable_date="2026-03-02 17:01:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772455297000" type="2" body="Sorry, I passed out hard yesterday" read="1" status="-1" locked="0" date_sent="1772455297000" readable_date="2026-03-02 12:41:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFU3eUZPMFFHUWcyUU43eDdLb2VTNHcqEC" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772437258000" type="1" body="U ok" read="1" status="-1" locked="0" date_sent="1772437258000" readable_date="2026-03-02 07:40:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHdPUC04SE5nUnpPSWhzM2RYemJiUWcqEJ" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772425030000" type="1" body="We are planning on coming to chill after the 10" read="1" status="-1" locked="0" date_sent="1772425030000" readable_date="2026-03-02 04:17:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE9ab1kyU3pRUVMtNXlNLUowa28zdkEqEA" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772425008000" type="1" body="Hey man how are you feeling" read="1" status="-1" locked="0" date_sent="1772425008000" readable_date="2026-03-02 04:16:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGV3dHE1elpFU21DM2JtRU41Vm1VeUEqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772408084000" type="2" body="Did you want me to initiate the call or were you going to start it through meet?" read="1" status="-1" locked="0" date_sent="1772408084000" readable_date="2026-03-01 23:34:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDFGZW1jbXdtVEsydE1FZ2h0V2ZaYkEqEN" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1772404428000" type="2" body="That will be fine then" read="1" status="-1" locked="0" date_sent="1772404428000" readable_date="2026-03-01 22:33:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDVyOVUyakd0Uk8tZE1pU0tLV09qbVEqEP" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1772404414000" type="1" body="1 hour" read="1" status="-1" locked="0" date_sent="1772404414000" readable_date="2026-03-01 22:33:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ1RjVDOTQzRC05QUQxLTQzNUMtQTM4Ny1FRD" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772404132000" type="2" body="That should be fine. How long do the appointments run?" read="1" status="-1" locked="0" date_sent="1772404132000" readable_date="2026-03-01 22:28:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFVxQ2xETVRRUnpHeUhLWFFRWHNOVXcqEP" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1772404025000" type="1" body="yes Matt but itll be more like 3:30 if thats ok… im running over with other clients… im so sorry" read="1" status="-1" locked="0" date_sent="1772404025000" readable_date="2026-03-01 22:27:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ3QjZDMUZCNi1ERkZBLTQ3NDEtQjg1Mi0xND" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772403946000" type="2" body="Our appointment at 3 still work for you?" read="1" status="-1" locked="0" date_sent="1772403946000" readable_date="2026-03-01 22:25:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDFpMVp0eVR5UldTOFYyY1FIaS05alEqEJ" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+14253266955" date="1772401327000" type="1" body="No worries. Don’t have high expectations regarding it because it’s nothing special 😂😂😭" read="1" status="-1" locked="0" date_sent="1772401327000" readable_date="2026-03-01 21:42:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ4MDA1RjE4NS0zQzhELTQ1M0QtQkVGOC03MU" group_addresses="+14253266955,+14253266955,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772400854000" type="2" body="Sorry I haven't gotten back to you yet, I'm gonna look at your resume and cover letter tonight" read="1" status="-1" locked="0" date_sent="1772400854000" readable_date="2026-03-01 21:34:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG00TGxSQm96Ulh1VFAwdEpZaHQxZWcqEF" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1772399660000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772399660000" readable_date="2026-03-01 21:14:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772398253000" type="1" body="Our next door neighbors didn't hear or see anything but they'll hear if something was up" read="1" status="-1" locked="0" date_sent="1772398253000" readable_date="2026-03-01 20:50:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772388923000" type="1" body="I will" read="1" status="-1" locked="0" date_sent="1772388923000" readable_date="2026-03-01 18:15:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772388620000" type="2" body="Let me know what you here from your neighbors still" read="1" status="-1" locked="0" date_sent="1772388620000" readable_date="2026-03-01 18:10:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772388594000" type="2" body="https://github.com/DigijEth/autarch" read="1" status="-1" locked="0" date_sent="1772388594000" readable_date="2026-03-01 18:09:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGo1NGdsVXhxUTVXS3JHRGpFMGlMQ3cqEO" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066125383" date="1772380584000" type="1" body="Let us know how it goes after." read="1" status="-1" locked="0" date_sent="1772380584000" readable_date="2026-03-01 15:56:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772380513000" type="2" body="Its at 3pm" read="1" status="-1" locked="0" date_sent="1772380513000" readable_date="2026-03-01 15:55:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772380406000" type="1" body="If you sleep make sure you wake up for your meeting today" read="1" status="-1" locked="0" date_sent="1772380406000" readable_date="2026-03-01 15:53:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772379026000" type="2" body="Call me when your more awake...this whole thing has been driving me off a cliff so I call snoco this morning" read="1" status="-1" locked="0" date_sent="1772379026000" readable_date="2026-03-01 15:30:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772377530000" type="1" body="Get some sleep" read="1" status="-1" locked="0" date_sent="1772377530000" readable_date="2026-03-01 15:05:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772360777000" type="2" body="Incase something happens you will need this also" read="1" status="-1" locked="0" date_sent="1772360777000" readable_date="2026-03-01 10:26:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066571038" date="1772355545000" type="1" body="Absolutely bro" read="1" status="-1" locked="0" date_sent="1772355545000" readable_date="2026-03-01 08:59:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEllclp6R2NQUU4tSDIwNWZXNXRqdncqEL" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772355492000" type="2" body="Its all good. Tomorrow?" read="1" status="-1" locked="0" date_sent="1772355492000" readable_date="2026-03-01 08:58:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDF2bHZqRWl6UzVxa2xueUNPdHZoZEEqEE" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772355467000" type="1" body="Shit man we just got home" read="1" status="-1" locked="0" date_sent="1772355467000" readable_date="2026-03-01 08:57:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFp5UHE3eWI1UXo2MmJ4U1dXWmlQLXcqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772355259000" type="2" body="Oh shoot I just saw this" read="1" status="-1" locked="0" date_sent="1772355259000" readable_date="2026-03-01 08:54:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGUyQlFWSU5QUUNXWTBzQ01MM2tkb0EqEO" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772347708000" type="1" body="We get done with karaoke tonight about midnight and we can head your way" read="1" status="-1" locked="0" date_sent="1772347708000" readable_date="2026-03-01 06:48:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDRoZEhpU05RUWZhbEZFR1pYSXBIVWcqEE" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772341729000" type="2" body="Love you. Good night" read="1" status="-1" locked="0" date_sent="1772341729000" readable_date="2026-03-01 05:08:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772341718000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772341718000" readable_date="2026-03-01 05:08:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772341697000" type="1" body="We're going to bed soon. You should too" read="1" status="-1" locked="0" date_sent="1772341697000" readable_date="2026-03-01 05:08:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772339620000" type="1" body="Theres some new people that were moving in as we moved out if I have the right apt. The woman right across from elevator was moving out just after we left. I can check with neighbors tomorrow to see what was going on" read="1" status="-1" locked="0" date_sent="1772339620000" readable_date="2026-03-01 04:33:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772339493000" type="2" body="I guest its the middle unit, yeah same side" read="1" status="-1" locked="0" date_sent="1772339493000" readable_date="2026-03-01 04:31:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772339481000" type="1" body="Same side as ours?" read="1" status="-1" locked="0" date_sent="1772339481000" readable_date="2026-03-01 04:31:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772339443000" type="2" body="Who lives on that side of the hall near the elevator? Is that the single man?" read="1" status="-1" locked="0" date_sent="1772339443000" readable_date="2026-03-01 04:30:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772339373000" type="1" body="Its a single man. He's been being treated for cancer. He has a brother that lives in the area. Maybe he died?" read="1" status="-1" locked="0" date_sent="1772339373000" readable_date="2026-03-01 04:29:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772339287000" type="2" body="2 down if you know them" read="1" status="-1" locked="0" date_sent="1772339287000" readable_date="2026-03-01 04:28:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772339265000" type="1" body="Right next door?" read="1" status="-1" locked="0" date_sent="1772339265000" readable_date="2026-03-01 04:27:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772339237000" type="2" body="Can you call your neighbor, just to check in? See what's up? Lots of people going in and out" read="1" status="-1" locked="0" date_sent="1772339237000" readable_date="2026-03-01 04:27:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772339157000" type="1" body="No dogs allowed in apts but sometimes they visit" read="1" status="-1" locked="0" date_sent="1772339157000" readable_date="2026-03-01 04:25:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772339118000" type="2" body="Nothing ATM, the people went in the apartment and never cane out. Does your neighbor have a dog, lab or retriever sized?" read="1" status="-1" locked="0" date_sent="1772339118000" readable_date="2026-03-01 04:25:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772339049000" type="1" body="What's up" read="1" status="-1" locked="0" date_sent="1772339049000" readable_date="2026-03-01 04:24:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772338445000" type="1" body="Idk" read="1" status="-1" locked="0" date_sent="1772338445000" readable_date="2026-03-01 04:14:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE5TdjhjREg4U2lHZDFJRGhoOGFsWEEqEA" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772338441000" type="1" body="Wth" read="1" status="-1" locked="0" date_sent="1772338441000" readable_date="2026-03-01 04:14:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFU1V3dZdTNnU0NpWHZFeHZjajd3VUEqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772337669000" type="2" body="The fuck?? Sorry you guys are the only people I know from OK so thought I would ask. Some random number just texted me from OK asking if I wanted to go to dinner tomorrow night" read="1" status="-1" locked="0" date_sent="1772337669000" readable_date="2026-03-01 04:01:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEpSWkw9RjB0VDcyRXFkdncyQmxNLXcqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772337574000" type="1" body="No bro" read="1" status="-1" locked="0" date_sent="1772337574000" readable_date="2026-03-01 03:59:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGtqc0JTYjFzU21hQ0pIRDhyQ2w1eWcqEJ" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772337254000" type="2" body="Did you just text me from a text now Oklahoma number?" read="1" status="-1" locked="0" date_sent="1772337254000" readable_date="2026-03-01 03:54:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEVVcEhXPTFmUkdDVTU4bFNnZmZKa3cqEG" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772334120000" type="2" body="You know me. Ill be up" read="1" status="-1" locked="0" date_sent="1772334120000" readable_date="2026-03-01 03:02:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeC1pUG55Q2FmUlYyOFoxVkVsbmY5bkEqEE" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772334098000" type="1" body="Okay, it wouldn't be until after 1 if tonight" read="1" status="-1" locked="0" date_sent="1772334098000" readable_date="2026-03-01 03:01:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFIyYmxMbXVLU1lpZ0M4eGp2VkNlaUEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772334049000" type="2" body="OK, just let me know. I can give ya gas again if you need it" read="1" status="-1" locked="0" date_sent="1772334049000" readable_date="2026-03-01 03:00:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE40TldNQWFSVGJtekI0SkczRFZXZGcqEJ" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772334043000" type="1" body="If not we can come hangout tomorrow" read="1" status="-1" locked="0" date_sent="1772334043000" readable_date="2026-03-01 03:00:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDA5b1RoRFhVU3p1NGpmY3d5cUlzZVEqEA" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772334007000" type="1" body="Potentially after our date night" read="1" status="-1" locked="0" date_sent="1772334007000" readable_date="2026-03-01 03:00:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHhVTkJJRzR2UzRlSnlHeURoRmdmZVEqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772333956000" type="2" body="Ah damn. Was gonna see if you could come keep me sane" read="1" status="-1" locked="0" date_sent="1772333956000" readable_date="2026-03-01 02:59:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE5IZGpVSm5UUXFLZkNLaXdwaz1pcFEqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772333891000" type="1" body="No were going out" read="1" status="-1" locked="0" date_sent="1772333891000" readable_date="2026-03-01 02:58:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGVxN1dzT3oyUXJ1QVg9TnEzY012OFEqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772333820000" type="2" body="You guys at the hall?" read="1" status="-1" locked="0" date_sent="1772333820000" readable_date="2026-03-01 02:57:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeC0xZ1pnZ3ZlU1ctTTRxUXV2UWV6cWcqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066125383" date="1772332507000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1772332507000" readable_date="2026-03-01 02:35:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772332487000" type="2" body="I think they are at donnas? Two units down with the camera" read="1" status="-1" locked="0" date_sent="1772332487000" readable_date="2026-03-01 02:34:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772331784000" type="2" body="It's the same two guys going from unit to unit" read="1" status="-1" locked="0" date_sent="1772331784000" readable_date="2026-03-01 02:23:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772331748000" type="1" body="Why would you think that? Do you see anything concrete?" read="1" status="-1" locked="0" date_sent="1772331748000" readable_date="2026-03-01 02:22:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772331613000" type="2" body="And me probably" read="1" status="-1" locked="0" date_sent="1772331613000" readable_date="2026-03-01 02:20:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772331603000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772331603000" readable_date="2026-03-01 02:20:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772331570000" type="1" body="Cameras on.." read="1" status="-1" locked="0" date_sent="1772331570000" readable_date="2026-03-01 02:19:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772331551000" type="1" body="For what?" read="1" status="-1" locked="0" date_sent="1772331551000" readable_date="2026-03-01 02:19:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772331534000" type="2" body="I think they are doing a unit by unit search" read="1" status="-1" locked="0" date_sent="1772331534000" readable_date="2026-03-01 02:18:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772331503000" type="1" body="Please be calm. If they were caring out someone earlier they may need to come back" read="1" status="-1" locked="0" date_sent="1772331503000" readable_date="2026-03-01 02:18:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772331454000" type="2" body="Here they come. I think it is them" read="1" status="-1" locked="0" date_sent="1772331454000" readable_date="2026-03-01 02:17:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772331381000" type="1" body="Sorry what?" read="1" status="-1" locked="0" date_sent="1772331381000" readable_date="2026-03-01 02:16:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772331359000" type="2" body="Here theybcome" read="1" status="-1" locked="0" date_sent="1772331359000" readable_date="2026-03-01 02:15:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772330237000" type="2" body="Maybe, but like I said, I'm super careful because if you cross that line, its criminal and civil with charges like unregistered foreign agent, and it only gets worse from there so I am so careful" read="1" status="-1" locked="0" date_sent="1772330237000" readable_date="2026-03-01 01:57:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEtSNS1mcGdkVEpxNG55eTFMPWRBM3cqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066125383" date="1772326630000" type="1" body="Understand" read="1" status="-1" locked="0" date_sent="1772326630000" readable_date="2026-03-01 00:57:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772326488000" type="2" body="I talked to my shrink about it, just to make sure I wasn't going nuts. He said no, and my behavior may seem crazy, but its normal for someone reliving a extremely traumatic event. Its basically like soldiers waking up in the middle of the night thinking they are back in combat. For me its the fear of opening my eyes and I am back in that cell, forced to wear only this orange &quot;safety&quot; vest in a cell with nothing but a hole in the middle of the room, or being tackled to the ground telling them I'm not resisting I'm doing what you ask. Then they tell you to do something else, but you can't because you have a knee shoved into the back of your skull so they press harder. Then being called a liar about meds and denied mental health care and shoved into a cell around people who were screaming all night, banging on the walls. I'm sorry that might be hard to read, but that's only a little of my first 48 hours. And my shrink says I do need to talk about it more to not have this reaction to police" read="1" status="-1" locked="0" date_sent="1772326488000" readable_date="2026-03-01 00:54:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066571038" date="1772325696000" type="1" body="Oh ok maybe that could be it then" read="1" status="-1" locked="0" date_sent="1772325696000" readable_date="2026-03-01 00:41:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5mNThZQT1XUVhXQmJWMkVYNE85WEEqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772325642000" type="2" body="They were at my apartment" read="1" status="-1" locked="0" date_sent="1772325642000" readable_date="2026-03-01 00:40:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772325619000" type="2" body="Well...not true, most people don't know this, and I don't advertise it, but I am a hacker. I am worried it could be related to that, but I am very careful to obey the laws. Just do shit for fun and on occasions I'll donate my time or computing power to help Ukraine hacking groups take down Russian sites. But I obey what were told we are allowed to do by the government and what we can't. Like I can do passive scans and such..basically recon...and pass the information on. I have to leave any of the real hacking to the guys in the Ukraine....there's this like 3000 page document that basically says...its illegal to actually do the hacking, but poking and finding holes isnt" read="1" status="-1" locked="0" date_sent="1772325619000" readable_date="2026-03-01 00:40:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHVwOVNVZj0tUT1LNXJteWYyNmJBRWcqEN" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066057095" date="1772325446000" type="1" body="I mean before today." read="1" status="-1" locked="0" date_sent="1772325446000" readable_date="2026-03-01 00:37:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066057095" date="1772325422000" type="1" body="We understand. Just to clarify...did the police show up to your tegular apartment door looking for you or were they nearby and you were afraid they were after you?" read="1" status="-1" locked="0" date_sent="1772325422000" readable_date="2026-03-01 00:37:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772324865000" type="1" body="?" read="1" status="-1" locked="0" date_sent="1772324865000" readable_date="2026-03-01 00:27:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZaZ3lyZDllUVEyU3g1Y085a2hpdmcqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772324859000" type="1" body="Dude you don't do anything but work and go to meetings 🙄" read="1" status="-1" locked="0" date_sent="1772324859000" readable_date="2026-03-01 00:27:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFplPXBTM05IUjZ1cGtDU0RoM3NvUUEqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772324823000" type="2" body="No clue, I'm sure its gonna be something really stupid" read="1" status="-1" locked="0" date_sent="1772324823000" readable_date="2026-03-01 00:27:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDNUWDNueGhXUmk2ODNBZ1dpeC1mQ3cqEL" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772324804000" type="1" body="Yeah the comedy of it evaded me" read="1" status="-1" locked="0" date_sent="1772324804000" readable_date="2026-03-01 00:26:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFpjd09yUlFSUWpLdTlybHlSbWZ2MlEqEI" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772324771000" type="2" body="Your not a local so might not be as funny to you for using him." read="1" status="-1" locked="0" date_sent="1772324771000" readable_date="2026-03-01 00:26:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHRjbVZ2aHdDU0F5QzQzVGRydD1SRVEqEE" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772324770000" type="1" body="Ok cool man what could it be im super stumped" read="1" status="-1" locked="0" date_sent="1772324770000" readable_date="2026-03-01 00:26:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJ2blNhMjdUU0dlbkxKeklxWWc1QUEqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772324707000" type="2" body="Ye. My shrink told me my reaction, while kinda off, is perfectly normal for someone with severe PTSD caused by last interaction with them and what I went through in jail. The attorney said not everything shows up online, but his PI was a Edmonds cop for 30 years. I'm using Bradly Johnson from 1800DUIAWAY" read="1" status="-1" locked="0" date_sent="1772324707000" readable_date="2026-03-01 00:25:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDcxbTk4VmZoUlB1em51TVNqSmFBRFEqEO" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772324551000" type="1" body="It's weird nothing is popping up via internet search" read="1" status="-1" locked="0" date_sent="1772324551000" readable_date="2026-03-01 00:22:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHplNlJ0UlZTUzVhQmFpOVl3dUhlVGcqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772324522000" type="1" body="Hopefully you can get some answers" read="1" status="-1" locked="0" date_sent="1772324522000" readable_date="2026-03-01 00:22:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFk1UlozZ1V2UTZlQ3ItUVFvPW1uMkEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772324509000" type="1" body="Oh shit 😳" read="1" status="-1" locked="0" date_sent="1772324509000" readable_date="2026-03-01 00:21:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5GNk42cTB5VDcyN3Nwa3BEdi1UcHcqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772324475000" type="2" body="I got an attorney here in Edmonds that's gonna dig into it monday. Try and figure it out. They were back at my apartments when I went by yesterday" read="1" status="-1" locked="0" date_sent="1772324475000" readable_date="2026-03-01 00:21:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZ2QUJ5b1VXU3B1N3Y3SWhMd0t3cGcqEL" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772324377000" type="1" body="It's all good brother what's the good word" read="1" status="-1" locked="0" date_sent="1772324377000" readable_date="2026-03-01 00:19:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGM1VUFWNmR3Ui1heWUyRUI0ckVtcmcqEL" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772324342000" type="2" body="Sorry about the other night, I wasn't trying to snub you." read="1" status="-1" locked="0" date_sent="1772324342000" readable_date="2026-03-01 00:19:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHFuTEpRWEtjVDdXZkp0bHkycUNUWXcqEC" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772324228000" type="2" body="OK and I hope you guys know that I really have no clue why this kind of thing keeps happening. I'm really just going to work and then going straight home everyday, I've even been going to AA and run the Friday midnight meeting to make sure I don't end up drunk and trying to kill myself, spinning out of control. I love you both so much and I am sorry that everything always turns to shit." read="1" status="-1" locked="0" date_sent="1772324228000" readable_date="2026-03-01 00:17:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772322066000" type="1" body="I can't initiate record from here manually. These cameras work great for my use case but not for this one." read="1" status="-1" locked="0" date_sent="1772322066000" readable_date="2026-02-28 23:41:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772322008000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772322008000" readable_date="2026-02-28 23:40:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772321955000" type="1" body="I've disarmed the mantle camera cause you're triggering it constantly. If something appears to be happening please text &quot;rec&quot; and I'll arm it. Then motion will trigger it." read="1" status="-1" locked="0" date_sent="1772321955000" readable_date="2026-02-28 23:39:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319836000" type="2" body="The sun changed and is washing out the video" read="1" status="-1" locked="0" date_sent="1772319836000" readable_date="2026-02-28 23:03:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772319816000" type="2" body="I sent a share link" read="1" status="-1" locked="0" date_sent="1772319816000" readable_date="2026-02-28 23:03:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772319777000" type="1" body="Its scanning our condo" read="1" status="-1" locked="0" date_sent="1772319777000" readable_date="2026-02-28 23:02:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319750000" type="2" body="One sec" read="1" status="-1" locked="0" date_sent="1772319750000" readable_date="2026-02-28 23:02:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772319739000" type="1" body="I got email verification but it says I need to scan qr code on camera" read="1" status="-1" locked="0" date_sent="1772319739000" readable_date="2026-02-28 23:02:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319697000" type="2" body="Strange," read="1" status="-1" locked="0" date_sent="1772319697000" readable_date="2026-02-28 23:01:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772319676000" type="1" body="Cant get registered yet. Email verification not arriving." read="1" status="-1" locked="0" date_sent="1772319676000" readable_date="2026-02-28 23:01:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319674000" type="2" body="https://play.google.com/store/apps/details?id=cn.ubia.ubox" read="1" status="-1" locked="0" date_sent="1772319674000" readable_date="2026-02-28 23:01:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772319633000" type="1" body="We're trying" read="1" status="-1" locked="0" date_sent="1772319633000" readable_date="2026-02-28 23:00:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319615000" type="2" body="I'm at yours, but there is a hall camera I can link you into" read="1" status="-1" locked="0" date_sent="1772319615000" readable_date="2026-02-28 23:00:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772319502000" type="1" body="Trying. Our apt or yours" read="1" status="-1" locked="0" date_sent="1772319502000" readable_date="2026-02-28 22:58:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772319014000" type="2" body="Download the app Ubox and I'll share the camera in the hall" read="1" status="-1" locked="0" date_sent="1772319014000" readable_date="2026-02-28 22:50:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318922000" type="1" body="Davisonmk@gmail.com and randydavison@comcast.net" read="1" status="-1" locked="0" date_sent="1772318922000" readable_date="2026-02-28 22:48:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318879000" type="2" body="What are your emails" read="1" status="-1" locked="0" date_sent="1772318879000" readable_date="2026-02-28 22:47:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772318781000" type="2" body="Absolutely not" read="1" status="-1" locked="0" date_sent="1772318781000" readable_date="2026-02-28 22:46:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318749000" type="1" body="They don't need to come in apt so you want to go downstairs and talk to tgem?" read="1" status="-1" locked="0" date_sent="1772318749000" readable_date="2026-02-28 22:45:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772318672000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772318672000" readable_date="2026-02-28 22:44:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318655000" type="2" body="No, but the guy I talked today has a guybwhonused to be Edmonds pd" read="1" status="-1" locked="0" date_sent="1772318655000" readable_date="2026-02-28 22:44:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318626000" type="1" body="Any idea why?" read="1" status="-1" locked="0" date_sent="1772318626000" readable_date="2026-02-28 22:43:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318602000" type="2" body="(253) 867-2675 both lawyers are brads" read="1" status="-1" locked="0" date_sent="1772318602000" readable_date="2026-02-28 22:43:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318562000" type="1" body="What's brads number" read="1" status="-1" locked="0" date_sent="1772318562000" readable_date="2026-02-28 22:42:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318543000" type="2" body="Here is his cell +14252103466" read="1" status="-1" locked="0" date_sent="1772318543000" readable_date="2026-02-28 22:42:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318519000" type="1" body="Let us know what happens" read="1" status="-1" locked="0" date_sent="1772318519000" readable_date="2026-02-28 22:41:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772318504000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772318504000" readable_date="2026-02-28 22:41:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318493000" type="2" body="Here is the number for a lawyer I talked to today 425-776-5547" read="1" status="-1" locked="0" date_sent="1772318493000" readable_date="2026-02-28 22:41:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318439000" type="1" body="Just be polite if so. Maybe they're just serving you" read="1" status="-1" locked="0" date_sent="1772318439000" readable_date="2026-02-28 22:40:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318409000" type="2" body="Yes" read="1" status="-1" locked="0" date_sent="1772318409000" readable_date="2026-02-28 22:40:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772318378000" type="1" body="Here at our apt?" read="1" status="-1" locked="0" date_sent="1772318378000" readable_date="2026-02-28 22:39:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772318350000" type="2" body="I think the police are here" read="1" status="-1" locked="0" date_sent="1772318350000" readable_date="2026-02-28 22:39:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772299177000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772299177000" readable_date="2026-02-28 17:19:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772299158000" type="1" body="Just opened door for delivery" read="1" status="-1" locked="0" date_sent="1772299158000" readable_date="2026-02-28 17:19:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772295384000" type="1" body="Did you get any sleep?" read="1" status="-1" locked="0" date_sent="1772295384000" readable_date="2026-02-28 16:16:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772295337000" type="1" body="Dad's not up yet. Nothing from Brad?" read="1" status="-1" locked="0" date_sent="1772295337000" readable_date="2026-02-28 16:15:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772295233000" type="2" body="When you wake up lets get money and rent out of the way. $2600 is what I'll need this month" read="1" status="-1" locked="0" date_sent="1772295233000" readable_date="2026-02-28 16:13:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+14253277371" date="1772234004000" type="1" body="Nice" read="1" status="-1" locked="0" date_sent="1772234004000" readable_date="2026-02-27 23:13:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFUwTGhIS2t0UWxtUXJQNHBDRlhMbFEqEL" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772227121000" type="1" body="You can check dad's closet and left hand drawers in case there's something that fits. Won't fit dad now he's lost a lot of weight" read="1" status="-1" locked="0" date_sent="1772227121000" readable_date="2026-02-27 21:18:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772227120000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772227120000" readable_date="2026-02-27 21:18:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772226652000" type="2" body="My clothes are in the wash and I don't have extra," read="1" status="-1" locked="0" date_sent="1772226652000" readable_date="2026-02-27 21:10:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772226602000" type="2" body="They came up" read="1" status="-1" locked="0" date_sent="1772226602000" readable_date="2026-02-27 21:10:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772226555000" type="1" body="I did. You can open door though but I let them in" read="1" status="-1" locked="0" date_sent="1772226555000" readable_date="2026-02-27 21:09:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772226496000" type="2" body="No, I can't just buzz them pls" read="1" status="-1" locked="0" date_sent="1772226496000" readable_date="2026-02-27 21:08:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772226471000" type="1" body="You can also go down and let them in" read="1" status="-1" locked="0" date_sent="1772226471000" readable_date="2026-02-27 21:07:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772226471000" type="1" body="They're there. Just let them in" read="1" status="-1" locked="0" date_sent="1772226471000" readable_date="2026-02-27 21:07:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772225524000" type="1" body="K" read="1" status="-1" locked="0" date_sent="1772225524000" readable_date="2026-02-27 20:52:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772225517000" type="2" body="Verify its doordash though" read="1" status="-1" locked="0" date_sent="1772225517000" readable_date="2026-02-27 20:51:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772225478000" type="2" body="There's gonna be a call to be let up in about 20 mins just fyi" read="1" status="-1" locked="0" date_sent="1772225478000" readable_date="2026-02-27 20:51:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772221916000" type="2" body="It has a built in LLM that will hack and security harden a system, as well as fully automate any metasploit module. It will find ways to deploy payloads to infect systems with a reverse shell. Its pretty bad ass" read="1" status="-1" locked="0" date_sent="1772221916000" readable_date="2026-02-27 19:51:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHpFZkM1NWo1U1Q2cnNqaUJWT0FaRUEqEJ" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1772221767000" type="1" body="Looks cool" read="1" status="-1" locked="0" date_sent="1772221767000" readable_date="2026-02-27 19:49:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeC1UVUZqOEVFU3RLV2dFZDNubXk4ZncqEJ" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772221633000" type="2" body="This is what my apps web dashboard looks like" read="1" status="-1" locked="0" date_sent="1772221633000" readable_date="2026-02-27 19:47:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEtueWtLUEdsUTJ5NW83ZlRrZHMyUFEqEB" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1772166146000" type="2" body="No, sorry was sleeoing" read="1" status="-1" locked="0" date_sent="1772166146000" readable_date="2026-02-27 04:22:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeExjaFNDRURCUXJDSVZ0RGZwVC02SWcqEJ" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066125383" date="1772161997000" type="1" body="Understand both" read="1" status="-1" locked="0" date_sent="1772161997000" readable_date="2026-02-27 03:13:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772158514000" type="1" body="Any updates" read="1" status="-1" locked="0" date_sent="1772158514000" readable_date="2026-02-27 02:15:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEJYNm03OXR4UndxUWwwcGVRU1RLdFEqEA" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772158138000" type="2" body="Yes, and still waiting...only thing I don't like about brad. My shrink changed my appointment to telle health and reaffirmed I have severe PTSD from last time" read="1" status="-1" locked="0" date_sent="1772158138000" readable_date="2026-02-27 02:08:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1772158023000" type="2" body="Still scared" read="1" status="-1" locked="0" date_sent="1772158023000" readable_date="2026-02-27 02:07:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEtVNkdFQnQwU2FpR2lkU0cyT2RVc2cqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772156887000" type="1" body="Hey hru today" read="1" status="-1" locked="0" date_sent="1772156887000" readable_date="2026-02-27 01:48:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEhGVmRHTWp4UzZlTi1tUDV1TDdoZncqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="2067597294" date="1772155158000" type="1" body="Hi, Matthew! Thank you for choosing LifeStance Health. Could you please leave us a review using the link below?" read="1" status="-1" locked="0" date_sent="1772155158000" readable_date="2026-02-27 01:19:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="71" rcs_tr_id="proto:CkQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SHhocChoxMTcxRjc3QjRFNzUwMDAwNjE4MDAwMDEwMQ" group_addresses="2067597294,2066990041" />
<sms protocol="0" address="+12066125383" date="1772131637000" type="1" body="Was it Brad? Glad you're back in control of situation." read="1" status="-1" locked="0" date_sent="1772131637000" readable_date="2026-02-26 18:47:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772130735000" type="1" body="Sounds good" read="1" status="-1" locked="0" date_sent="1772130735000" readable_date="2026-02-26 18:32:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772124968000" type="2" body="Attorney says to stay out and wait to here back. Doesn't want what happened last time and wants control if something is happening" read="1" status="-1" locked="0" date_sent="1772124968000" readable_date="2026-02-26 16:56:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772117624000" type="1" body="Yes, they're good about going after what they think is a live one. When I threatened to sue them if they ever came back, they quit. But you do need to find out why" read="1" status="-1" locked="0" date_sent="1772117624000" readable_date="2026-02-26 14:53:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772117533000" type="2" body="I was thinking that too, they did used to try and blame me for everything" read="1" status="-1" locked="0" date_sent="1772117533000" readable_date="2026-02-26 14:52:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772117486000" type="1" body="Maybe it's just harassment. Start with Brad. Sorry" read="1" status="-1" locked="0" date_sent="1772117486000" readable_date="2026-02-26 14:51:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772117426000" type="2" body="Yeah, I know I will work on brad. Yes. From all the warrant databases and everything that's available for public plus even paid the $11 dollars for WSP's online database, there's nothing" read="1" status="-1" locked="0" date_sent="1772117426000" readable_date="2026-02-26 14:50:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772117354000" type="1" body="So you have to find out why" read="1" status="-1" locked="0" date_sent="1772117354000" readable_date="2026-02-26 14:49:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772117330000" type="1" body="If you really need to find one wait until neighbor is awake. He used to be one. May know someone that can call for you but I'd start with old attorney. We've paid him 30 thousand and he really only worked one day" read="1" status="-1" locked="0" date_sent="1772117330000" readable_date="2026-02-26 14:48:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772117316000" type="2" body="I love you guys and yea I was going to start with brad and yes. They did" read="1" status="-1" locked="0" date_sent="1772117316000" readable_date="2026-02-26 14:48:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772117160000" type="1" body="The old attorney you had would probably at least make a call for you. Did they show up again?" read="1" status="-1" locked="0" date_sent="1772117160000" readable_date="2026-02-26 14:46:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772117073000" type="2" body="Just never mind. I love you guys. I think I'm going to have to call an attorney this morning, hopefully I can find one that will figure this out for cheap or I'm going to end up in a straight jacket. I hate to say it, but I might be losing my mind." read="1" status="-1" locked="0" date_sent="1772117073000" readable_date="2026-02-26 14:44:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772116316000" type="1" body="Why?" read="1" status="-1" locked="0" date_sent="1772116316000" readable_date="2026-02-26 14:31:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772110592000" type="2" body="Tell your neighbors not to open the apartment for anyone and not to offer up they have a key" read="1" status="-1" locked="0" date_sent="1772110592000" readable_date="2026-02-26 12:56:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066571038" date="1772097549000" type="1" body="No problem homie" read="1" status="-1" locked="0" date_sent="1772097549000" readable_date="2026-02-26 09:19:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHplbGJ5eHBhUi1PUFZGSFAtQzM1bVEqEI" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772096482000" type="2" body="Thanks again guys" read="1" status="-1" locked="0" date_sent="1772096482000" readable_date="2026-02-26 09:01:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDFrM1hhVmtNVGpDZW9OaXZ2SWdhSncqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772096341000" type="2" body="Ok," read="1" status="-1" locked="0" date_sent="1772096341000" readable_date="2026-02-26 08:59:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHNJNkNyZkZKU3NteVFRTmlMSlUwN0EqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772096333000" type="1" body="Yea no cops anywhere" read="1" status="-1" locked="0" date_sent="1772096333000" readable_date="2026-02-26 08:58:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHZQM3FDN2ljUy02MGVkdnFTUVZYa3cqEI" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772096310000" type="2" body="OK thanks for checking, and I assume no popo sitting anywhere?" read="1" status="-1" locked="0" date_sent="1772096310000" readable_date="2026-02-26 08:58:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEplNGxTSnJ3VE5tbklyYzdmSldXQUEqEJ" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772096270000" type="1" body="No card or anything" read="1" status="-1" locked="0" date_sent="1772096270000" readable_date="2026-02-26 08:57:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFBVaUZSZlVtU3hTdmV1WmpQcS14cXcqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772096121000" type="2" body="Okay" read="1" status="-1" locked="0" date_sent="1772096121000" readable_date="2026-02-26 08:55:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGNLb2QwY1lhUXN1QUx2RmM1UnI0emcqEA" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772096116000" type="1" body="Checking" read="1" status="-1" locked="0" date_sent="1772096116000" readable_date="2026-02-26 08:55:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZQPXprZ21BU0c2eldsQnBUbkxYVUEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772096094000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772096094000" readable_date="2026-02-26 08:54:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHgtLUZ3TjZGUnVhUjZGTWVMSEFOOUEqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772096089000" type="2" body="In the middle" read="1" status="-1" locked="0" date_sent="1772096089000" readable_date="2026-02-26 08:54:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEQ2MlpMQ0RMUm9TSEloOW9EOEZmUFEqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772096081000" type="2" body="Its a bottom floor apart in the first building" read="1" status="-1" locked="0" date_sent="1772096081000" readable_date="2026-02-26 08:54:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHJVUm5RVnlRUmEyR04wcWFxYnVZU3cqEP" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772096064000" type="1" body="Apt" read="1" status="-1" locked="0" date_sent="1772096064000" readable_date="2026-02-26 08:54:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZyMlhiRjVyVDZhTWtOS2l1NnhMTncqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772096060000" type="1" body="Bottom or top" read="1" status="-1" locked="0" date_sent="1772096060000" readable_date="2026-02-26 08:54:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGsxMFlDSVRUVDFXWEUtemhqYy1vRWcqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772093308000" type="2" body="I'll be at the front door in a second sorry" read="1" status="-1" locked="0" date_sent="1772093308000" readable_date="2026-02-26 08:08:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGphSWhaRzltUVJ1dnQwT3AxbEkzV1EqEK" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+12066990041" date="1772093286000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772093286000" readable_date="2026-02-26 08:08:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGh6QjlaMkZDUTdDM3gyMlNSVm1GTGcqEF" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1772093032000" type="1" body="Here" read="1" status="-1" locked="0" date_sent="1772093032000" readable_date="2026-02-26 08:03:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDRXWjlTU1h0UXJtclZXPUxxY1h2dmcqEK" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772090776000" type="2" body="222 Bell street, Edmonds 98020" read="1" status="-1" locked="0" date_sent="1772090776000" readable_date="2026-02-26 07:26:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDJUQkhHREM3U082blA4aExZRWVET1EqEO" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1772090650000" type="1" body="Wassup" read="1" status="-1" locked="0" date_sent="1772090650000" readable_date="2026-02-26 07:24:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGQ9bTlsTDktUWptTGNBY1oyakUyWlEqEP" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772090487000" type="2" body="Hey, what are you doing? I need some help" read="1" status="-1" locked="0" date_sent="1772090487000" readable_date="2026-02-26 07:21:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEJXPXJTZ01PUVZLeVZyZXZvSEJLancqEC" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+12066990041" date="1772088322000" type="2" body="OK, come on up" read="1" status="-1" locked="0" date_sent="1772088322000" readable_date="2026-02-26 06:45:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEFkTG9DSkh2VFJXR2luSz09Tzg0YmcqEO" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772088296000" type="1" body="We're back" read="1" status="-1" locked="0" date_sent="1772088296000" readable_date="2026-02-26 06:44:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEM0OVI9VW5BUXRDeXpHRzdSdlZzVncqEJ" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772087781000" type="1" body="Ok sounds good" read="1" status="-1" locked="0" date_sent="1772087781000" readable_date="2026-02-26 06:36:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEhFNXRyV0FqVGoybVotUEVMb3lMcncqEL" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772087606000" type="2" body="Smoke shop is still open 9794 Edmonds Way, Edmonds, WA 98020, also you should come up for a few when you get back and I can fill you on everything that's going on. If you don't have to race home" read="1" status="-1" locked="0" date_sent="1772087606000" readable_date="2026-02-26 06:33:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGNobj1VQUFFUmRhUnFOekxpY1hPSWcqEE" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772087475000" type="1" body="Brb" read="1" status="-1" locked="0" date_sent="1772087475000" readable_date="2026-02-26 06:31:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHl5SDhsWllnUXBHbTJmYXNWQ0t6eXcqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772087467000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772087467000" readable_date="2026-02-26 06:31:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG1lNjkyNkc5UXRPWTFGSWxkMjU3S1EqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772087463000" type="2" body="Correct" read="1" status="-1" locked="0" date_sent="1772087463000" readable_date="2026-02-26 06:31:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG52algzT3REVC1hRWduLTI0VnBId3cqEN" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772087463000" type="1" body="99" read="1" status="-1" locked="0" date_sent="1772087463000" readable_date="2026-02-26 06:31:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE5CaGRWaTdaVDdXQ3dEVlBEUVdKbmcqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772087461000" type="1" body="?" read="1" status="-1" locked="0" date_sent="1772087461000" readable_date="2026-02-26 06:31:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFN0cjhySDgzVFlpc0h3TjY2RW5vd1EqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772087451000" type="1" body="Camel blues" read="1" status="-1" locked="0" date_sent="1772087451000" readable_date="2026-02-26 06:30:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlEMjJ4SlQtU1dHNk04ZFlhUk82UHcqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772087364000" type="1" body="Here" read="1" status="-1" locked="0" date_sent="1772087364000" readable_date="2026-02-26 06:29:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5yM201UT1kVHJHM2xNSFFlRmVNTEEqEC" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772087081000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772087081000" readable_date="2026-02-26 06:24:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFpLcm9GQU00UVRTUEI1dEl4Mk1pTGcqEF" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772087068000" type="1" body="Gotcha, didn't see anything, GPS says 6 mins" read="1" status="-1" locked="0" date_sent="1772087068000" readable_date="2026-02-26 06:24:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHhUZjQxaTFUUlVpWGhEQVBSVEhyTXcqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772087020000" type="2" body="But good" read="1" status="-1" locked="0" date_sent="1772087020000" readable_date="2026-02-26 06:23:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFVlekxKWm4yUmE2VGJhLW5MNXdnPWcqEK" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772087012000" type="2" body="OK that little ally way is where they were before, and its a great place to hide from view" read="1" status="-1" locked="0" date_sent="1772087012000" readable_date="2026-02-26 06:23:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGhTNnF4T1RCUlRlPWNNSkQ2TU1jd1EqEI" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772086889000" type="1" body="I don't see anyone" read="1" status="-1" locked="0" date_sent="1772086889000" readable_date="2026-02-26 06:21:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHJIUVlYdGhsUmFXQ016dG9ORUt0UHcqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772085800000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772085800000" readable_date="2026-02-26 06:03:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFY1blVQOVhkUm5xbXZtVkgzWExZclEqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772085712000" type="2" body="OK, I was going to suggest that. Make sure eyeball up the middle drive between the row of garages, its where whoever was watching was." read="1" status="-1" locked="0" date_sent="1772085712000" readable_date="2026-02-26 06:01:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHdONzhiZGZrUjlxR0RzTXg9ZFpRckEqED" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772085621000" type="1" body="Running by your place first" read="1" status="-1" locked="0" date_sent="1772085621000" readable_date="2026-02-26 06:00:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFI0LVkwMGttUUh5bVNTWj09Z0g3YWcqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772085612000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772085612000" readable_date="2026-02-26 06:00:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlQaGNJM1R2VFFhbE1Wdy1UaDhjeVEqEI" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772085594000" type="1" body="On my way to ya" read="1" status="-1" locked="0" date_sent="1772085594000" readable_date="2026-02-26 05:59:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGhLOWVFd3l0VGdtc2RkRj1LV3FQQ3cqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+14253277371" date="1772084210000" type="1" body="I'll have to look at that" read="1" status="-1" locked="0" date_sent="1772084210000" readable_date="2026-02-26 05:36:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD1vUUZYRGFNVG5tbkduVTJ4WGdqcXcqEK" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772084144000" type="2" body="Looks pretty good. Gauletslayer edition is pretty badass also" read="1" status="-1" locked="0" date_sent="1772084144000" readable_date="2026-02-26 05:35:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeENlUEJDWTJ5Uk9TM1hxY1poVWI5YVEqEF" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1772084111000" type="2" body="Nice, I'll check it out" read="1" status="-1" locked="0" date_sent="1772084111000" readable_date="2026-02-26 05:35:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEdCakI3MkM3UmtlQ1RFcWJza1dCM1EqED" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1772084094000" type="1" body="They also remastered the old one" read="1" status="-1" locked="0" date_sent="1772084094000" readable_date="2026-02-26 05:34:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGI3dXI9d0N0UWFXbmNLSEI0MlZmRlEqEP" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+14253277371" date="1772084062000" type="1" body="Even has dlc" read="1" status="-1" locked="0" date_sent="1772084062000" readable_date="2026-02-26 05:34:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJUM01uUjJRU2NxYWl1S2dGcWd6R0EqEF" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+14253277371" date="1772084054000" type="1" body="You should try that new Commandos. It's pretty challenging" read="1" status="-1" locked="0" date_sent="1772084054000" readable_date="2026-02-26 05:34:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGFLc0NCcUl2VElpNlFtRmF2ZWg3N1EqEC" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772083591000" type="1" body="Anyway I can help you" read="1" status="-1" locked="0" date_sent="1772083591000" readable_date="2026-02-26 05:26:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGZLcno5ZHNrUWsybDBGOW1xWXhUPUEqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772083572000" type="1" body="No problem buddy" read="1" status="-1" locked="0" date_sent="1772083572000" readable_date="2026-02-26 05:26:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDhzMG02VWF4VEJXNVNFLXYwMjF3UGcqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772083558000" type="2" body="OK, I really appreciate you" read="1" status="-1" locked="0" date_sent="1772083558000" readable_date="2026-02-26 05:25:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHRVd21BSUhsVHUya3Z4azZTU0lmQmcqEL" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772083528000" type="1" body="Ill be there about 1115" read="1" status="-1" locked="0" date_sent="1772083528000" readable_date="2026-02-26 05:25:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5WYWpjMGJ3VDhDQTdzRkRwbmRaQmcqEO" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772083467000" type="1" body="I get ya homie" read="1" status="-1" locked="0" date_sent="1772083467000" readable_date="2026-02-26 05:24:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEVrbnFGYkdLUW5Pdi1FWjhwcmJjWmcqEL" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772083437000" type="2" body="OK, just a pack and another run by my apartment. I'll give you another 40. I know I'm probably over reacting to nothing, but you have no clue how traumatized I was after my last run in with Edmonds finest." read="1" status="-1" locked="0" date_sent="1772083437000" readable_date="2026-02-26 05:23:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeElKbUhXV29MUnlTNGJEb1RRdjFWS2cqEP" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772083310000" type="1" body="My bank is overdrawn" read="1" status="-1" locked="0" date_sent="1772083310000" readable_date="2026-02-26 05:21:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZHc2VGVThOUlZpRndOdFk3eUd6bmcqEE" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772083288000" type="1" body="After the 10" read="1" status="-1" locked="0" date_sent="1772083288000" readable_date="2026-02-26 05:21:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFFSS2JtT0Y0UkgtTlBpWUdpSFB6c1EqEG" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772083281000" type="1" body="I can bring you a few packs or whatever just need money" read="1" status="-1" locked="0" date_sent="1772083281000" readable_date="2026-02-26 05:21:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGxQOXJaTEZOVHY2RjdGaVhIOFVKWVEqEI" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+14253277371" date="1772083023000" type="1" body="No worries" read="1" status="-1" locked="0" date_sent="1772083023000" readable_date="2026-02-26 05:17:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDJBZmZ4dXBWU2xDeUVQMGpTazZ6R1EqEF" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772082532000" type="2" body="Its just me over here. If you do come by I could use some smokes" read="1" status="-1" locked="0" date_sent="1772082532000" readable_date="2026-02-26 05:08:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeC0yNTNxYmxpVFBLZkJHZ0EzOT1yNUEqEG" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772082466000" type="2" body="OK, no worries" read="1" status="-1" locked="0" date_sent="1772082466000" readable_date="2026-02-26 05:07:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHFrTHVsajVKVHJ5Qy05UlQ5bWc3PVEqEC" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772082449000" type="1" body="Ok we may im not sure" read="1" status="-1" locked="0" date_sent="1772082449000" readable_date="2026-02-26 05:07:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHBuaGxBZ28xU2tTSVNFbz1yYUhwQXcqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772082439000" type="2" body="Yeah, thanks for helping yesterday. And sorry for having to help out. Lol" read="1" status="-1" locked="0" date_sent="1772082439000" readable_date="2026-02-26 05:07:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEprMzJBaHFxUlB1PUhpU3AtZEZaQmcqEL" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1772082364000" type="2" body="Still hol'd up if you guys wanna come by. Still don't know much" read="1" status="-1" locked="0" date_sent="1772082364000" readable_date="2026-02-26 05:06:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHRSTFhUeUlvUnJTNjZzbnhiQlVOT0EqEK" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772079878000" type="1" body="Hey bro how are you doing" read="1" status="-1" locked="0" date_sent="1772079878000" readable_date="2026-02-26 04:24:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZTOURjdTF0UXZld3VDQ1VrU1pJMmcqEO" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+14253277371" date="1772053879000" type="1" body="Well that's good" read="1" status="-1" locked="0" date_sent="1772053879000" readable_date="2026-02-25 21:11:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHJRV3BPYkgtVEpTNzlVS2FPalhwUEEqEC" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772052495000" type="2" body="Nothing to do with the case in Kansas, so that's a good, and I can't find anything in the Washington state court website except my ticket I paid in Nov. So idk. I'm gonna call my attorney up here and make sure the extradition case he handled for me last year was fully taken care of, make sure everything got filed that needed to be." read="1" status="-1" locked="0" date_sent="1772052495000" readable_date="2026-02-25 20:48:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDJwPUN5R3V6U01DYk1MVFFnd25sWUEqEE" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1772052187000" type="1" body="Everything ok today?" read="1" status="-1" locked="0" date_sent="1772052187000" readable_date="2026-02-25 20:43:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHBvakE0RGs1UkQycTZpaGZRODI4VXcqEG" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066057095" date="1772051963000" type="1" body="One day is fine and mom and I do understand your trauma. We were closely involved, remember? We're still traumatized too." read="1" status="-1" locked="0" date_sent="1772051963000" readable_date="2026-02-25 20:39:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772051634000" type="2" body="I'm trying. But I think I need a day to regain my feet. I already made an appointment with my shrink for tomorrow before work. I don't think you understand how traumatic that was for me last time. I'm still shaking. I have benefits, and the policy at work is as long as you have benefits and your caught up, they don't care. If you get behind, that's what we have Saturday for. This isn't your era where calling out sick penalizes you. They really don't care. I have to use my 47 hours of vacation and 34 hours of sick leave before my roll overdate or I lose it. Plus I'm still accruing another 40 hours. I need today to just be somewhere else. Please don't fight me on this. Its one day" read="1" status="-1" locked="0" date_sent="1772051634000" readable_date="2026-02-25 20:33:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1772050930000" type="1" body="Ashley says there's no warrant active in Kansas so if the cops do contact you you can tell them the story about last time and push them to check. We assume you're talking to Ashley too." read="1" status="-1" locked="0" date_sent="1772050930000" readable_date="2026-02-25 20:22:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772042715000" type="1" body="Okay, maybe they just wanted information? So go to work and forget about it? I'd be traumatized too but hopefully its nothing" read="1" status="-1" locked="0" date_sent="1772042715000" readable_date="2026-02-25 18:05:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772042618000" type="2" body="That's good, and I didn't bring my car. Uber from here to there is cheap" read="1" status="-1" locked="0" date_sent="1772042618000" readable_date="2026-02-25 18:03:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1772035760000" type="1" body="Probably need to move your car too" read="1" status="-1" locked="0" date_sent="1772035760000" readable_date="2026-02-25 16:09:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772035736000" type="1" body="So Ashley said it's not coming from Kansas so you're going to have to contact Edmonds police or have someone do it on your behalf. Police didn't leave anything?" read="1" status="-1" locked="0" date_sent="1772035736000" readable_date="2026-02-25 16:08:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772008966000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772008966000" readable_date="2026-02-25 08:42:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHVmPTNkMjBuUThHdDZyNmRIMkE0bkEqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772008958000" type="2" body="I don't have a buzzer key" read="1" status="-1" locked="0" date_sent="1772008958000" readable_date="2026-02-25 08:42:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJWNjM5Umc9U1FTdWpVa09wN3Z1dkEqEI" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772008948000" type="2" body="You gotta come up to the door" read="1" status="-1" locked="0" date_sent="1772008948000" readable_date="2026-02-25 08:42:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHJHVD04UTJGVGxpdUw9SHpTbWdVWFEqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772008813000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1772008813000" readable_date="2026-02-25 08:40:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlVclJTSC1NU09XeGlEQWxUeHN6eEEqEH" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772008807000" type="2" body="OK, give me a second" read="1" status="-1" locked="0" date_sent="1772008807000" readable_date="2026-02-25 08:40:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFN4MGwtMXZ3UmdtY3pBT0lwVUFHeWcqEA" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772008793000" type="1" body="We're here" read="1" status="-1" locked="0" date_sent="1772008793000" readable_date="2026-02-25 08:39:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEF3dk1BcDd1UjZHZTBPQ2F3ZEpmR3cqEN" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772007350000" type="2" body="OK, text when your at the door and I'll come down. Make sure its the bell street entrance" read="1" status="-1" locked="0" date_sent="1772007350000" readable_date="2026-02-25 08:15:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGFBQWZDV0FGUnRPSnRSZmhPemtNdUEqEE" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772007312000" type="1" body="GPS says 15 minutes" read="1" status="-1" locked="0" date_sent="1772007312000" readable_date="2026-02-25 08:15:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEhaYnVQSGE2VEoyYjBUYTU2YUozbHcqED" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772007272000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1772007272000" readable_date="2026-02-25 08:14:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJrNWVrd2U3U3Ntb2Fmc09uZ292RWcqEA" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772007268000" type="1" body="We can meet you" read="1" status="-1" locked="0" date_sent="1772007268000" readable_date="2026-02-25 08:14:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGtrRWZFOGdnU3lpaTFkeUg5YzdUeVEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772007251000" type="2" body="Or if you meet me at 222 bell street, Edmonds I can give you $30 cash" read="1" status="-1" locked="0" date_sent="1772007251000" readable_date="2026-02-25 08:14:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDJsUkpsZ0d4VDl1c2ZQc3kwUzNWRmcqEN" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1772007193000" type="2" body="Yeah, idk what's going on. OK, let me see what I have, and if my cash app is working, does your bank have zelle built-in? If not you should look at getting a chase second chance account. Its basically a prepaid account with all the chase features. Only sucky part is it cost $8 a month. But the zelle access is worth it. You got PayPal?" read="1" status="-1" locked="0" date_sent="1772007193000" readable_date="2026-02-25 08:13:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGZVeEpUZEtXU042MkRnYWg5M2R3T0EqEB" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772006669000" type="1" body="Heading home from the meeting" read="1" status="-1" locked="0" date_sent="1772006669000" readable_date="2026-02-25 08:04:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFFKV0ZHQ0J3UUZPdnZZSGs4cD1FcmcqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772006655000" type="1" body="Lol" read="1" status="-1" locked="0" date_sent="1772006655000" readable_date="2026-02-25 08:04:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZSbkdDOS1JU2lxSjhZbWEtcUllYlEqEC" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772006653000" type="1" body="When you can" read="1" status="-1" locked="0" date_sent="1772006653000" readable_date="2026-02-25 08:04:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFUwbjlXcERmVGktZVdPb0ZCd3BjSUEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772006633000" type="1" body="https://cash.app/$ScottKonecny9" read="1" status="-1" locked="0" date_sent="1772006633000" readable_date="2026-02-25 08:03:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDllT0FITXE9VDNDZGZFdWdXV0dELWcqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772006495000" type="1" body="Usually they will show up in marked units to serve normal warrants" read="1" status="-1" locked="0" date_sent="1772006495000" readable_date="2026-02-25 08:01:35 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHowRUhQQ0hwUlMyak9zMTJiVUtrc2cqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066125383" date="1772006114000" type="1" body="There's a house key in the left hand drawer of the table as you come in. Go to work tomorrow. You'll need to move your car in the morning since I dont think handicapped placard is there. Let us know when you hear from Ashley. We're going to bed." read="1" status="-1" locked="0" date_sent="1772006114000" readable_date="2026-02-25 07:55:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="67" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZKQUxLMmQ3Um9hRi1TN1VQYk9oZUEqEN" group_addresses="+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772005958000" type="1" body="Hmm might have been ice for illegals" read="1" status="-1" locked="0" date_sent="1772005958000" readable_date="2026-02-25 07:52:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJiNHo5MTlOU2o2Y1ptSEd2TnRSR1EqEE" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772005911000" type="1" body="Ok cool" read="1" status="-1" locked="0" date_sent="1772005911000" readable_date="2026-02-25 07:51:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG1oalVDQ05IUmRHNC1BVHgza2U5UlEqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772005873000" type="2" body="No, it wasn't and let me see what I have, I might be able to only do $25. I'm on my way to where I'm crashing, once I'm set up I'll text. Be Luke 10 mins" read="1" status="-1" locked="0" date_sent="1772005873000" readable_date="2026-02-25 07:51:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEFHNDQteUJtUk1TSy1XZGxzRkpwN1EqEN" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772005822000" type="1" body="Low on gas and food" read="1" status="-1" locked="0" date_sent="1772005822000" readable_date="2026-02-25 07:50:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEJoeHZwYzlKUWI2bGdBQTNvdFZzZVEqEP" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772005809000" type="1" body="Or whatever you can afford" read="1" status="-1" locked="0" date_sent="1772005809000" readable_date="2026-02-25 07:50:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEd3QzVRMXFSUXF1OWtYZXJPVUlOMlEqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772005645000" type="1" body="Can you send me 50 and ill get it back to you the 3rd" read="1" status="-1" locked="0" date_sent="1772005645000" readable_date="2026-02-25 07:47:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHhlUFRrRGhhUkZ1aXNnbHo9aE1jUncqEJ" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1772005581000" type="1" body="Idk man pretty weird was it marked units" read="1" status="-1" locked="0" date_sent="1772005581000" readable_date="2026-02-25 07:46:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHhCVmMwTWhwUlBDR2ctN21XRWNIVWcqEA" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772005546000" type="2" body="Yeah, for now at least. Not sure who that was staking out my house" read="1" status="-1" locked="0" date_sent="1772005546000" readable_date="2026-02-25 07:45:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGYyeW9EU3ZQUk42M1ZRZ2tiYzR0a3cqEI" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1772005463000" type="1" body="Cool brother everything ok" read="1" status="-1" locked="0" date_sent="1772005463000" readable_date="2026-02-25 07:44:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeElwT09XbDVmUVlHPWRNc3U4WlY0OEEqEC" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1772005446000" type="2" body="I just got my phone back" read="1" status="-1" locked="0" date_sent="1772005446000" readable_date="2026-02-25 07:44:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGpEPXdyRFZLUkFxNXltNW5GU296cUEqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1771997192000" type="1" body="Hey man what's up" read="1" status="-1" locked="0" date_sent="1771997192000" readable_date="2026-02-25 05:26:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE1RWlkxS2VYVHhTZVcxc3BlYzBzTGcqEI" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771988707000" type="2" body="I have not" read="1" status="-1" locked="0" date_sent="1771988707000" readable_date="2026-02-25 03:05:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDVkRjg3dHRYVHFPZkxIcVJ1bmhUbncqEJ" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771988601000" type="1" body="Cool. I just stopped at Walmart on 164th. I can grab a ps controller. Have you played the new Commandos yet?" read="1" status="-1" locked="0" date_sent="1771988601000" readable_date="2026-02-25 03:03:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE9hU2tSR0hlVC1pTDZqb0trRlVJN3cqEN" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771987946000" type="2" body="Oh if you haven't left yet, bring a Xbox or PS controller" read="1" status="-1" locked="0" date_sent="1771987946000" readable_date="2026-02-25 02:52:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEMyTm5RVlVUUmk2cU5VYzYxMHprVFEqEK" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1771987841000" type="2" body="Maybe sooner depending on traffic, just gotta get gas" read="1" status="-1" locked="0" date_sent="1771987841000" readable_date="2026-02-25 02:50:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5XeFZ6THRxUWplUWM0TFI3VWw4OWcqEC" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1771987786000" type="2" body="Just packing up, should be home in 30-45 mins" read="1" status="-1" locked="0" date_sent="1771987786000" readable_date="2026-02-25 02:49:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEd4RHZTaWtVUlgyZ0w3eGZON3FuQVEqEL" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771985802000" type="1" body="Cool. I'll start slowly making my way" read="1" status="-1" locked="0" date_sent="1771985802000" readable_date="2026-02-25 02:16:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGRCNW5laHdWU0wtSk1qZVN3ZVJRb0EqEA" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771985705000" type="2" body="Gonna be heading out soon. I'll let you know when I'm on my way home" read="1" status="-1" locked="0" date_sent="1771985705000" readable_date="2026-02-25 02:15:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEVzSDdIVHdjU2dDME14dGlQQTVaemcqEA" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771972720000" type="1" body="Cool" read="1" status="-1" locked="0" date_sent="1771972720000" readable_date="2026-02-24 22:38:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDVacUV6ajlkUkJDS2xGYXRIM0kyVHcqEI" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771972358000" type="2" body="Not much, gotta run into work till about 630, then I'll be free" read="1" status="-1" locked="0" date_sent="1771972358000" readable_date="2026-02-24 22:32:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG5VM3pOcmdFVGZHODJDOFhTTTdnbVEqEH" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771972294000" type="1" body="Sup" read="1" status="-1" locked="0" date_sent="1771972294000" readable_date="2026-02-24 22:31:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFhabzJSOUdSU0tlN21SejhTYVJRR3cqEE" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+14253193719" date="1771959715000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1771959715000" readable_date="2026-02-24 19:01:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE9FT2VMWTJXUXVxVz05eW04VEV2bmcqEJ" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771958136000" type="2" body="Will be mine again when I'm back, I'm waiting for all move notices become null before returning from loa" read="1" status="-1" locked="0" date_sent="1771958136000" readable_date="2026-02-24 18:35:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771957974000" type="2" body="Let tom know if I can know if I'll be selected for move crew or not will help my recovery. I know he's still a weekout at the earliest form his surgery, but ifvyou talk to him" read="1" status="-1" locked="0" date_sent="1771957974000" readable_date="2026-02-24 18:32:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGRiOTFRbk8tUThhem92dDlFMTZFVWcqEP" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+19168990823" date="1771957867000" type="1" body="No, in dieshay's area. The position ahead of mine" read="1" status="-1" locked="0" date_sent="1771957867000" readable_date="2026-02-24 18:31:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1771957830000" type="1" body="Ohh good. I’m guessing that lady isn’t in your area then" read="1" status="-1" locked="0" date_sent="1771957830000" readable_date="2026-02-24 18:30:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771957611000" type="1" body="I have great leads" read="1" status="-1" locked="0" date_sent="1771957611000" readable_date="2026-02-24 18:26:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771956814000" type="2" body="They think he is cocky. I know a few leads like that, but they all work aft bodies" read="1" status="-1" locked="0" date_sent="1771956814000" readable_date="2026-02-24 18:13:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1771956768000" type="1" body="Sorry fellas. My lead is honestly the best lead I’ve had my entire life. Dude works on the airplane can get almost every IP done in half the time and is always helping. You’ll never find him at his desk. Wish you guys had someone like that aswell." read="1" status="-1" locked="0" date_sent="1771956768000" readable_date="2026-02-24 18:12:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771956740000" type="2" body="They don't like him" read="1" status="-1" locked="0" date_sent="1771956740000" readable_date="2026-02-24 18:12:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771956707000" type="2" body="Agreed" read="1" status="-1" locked="0" date_sent="1771956707000" readable_date="2026-02-24 18:11:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771956694000" type="1" body="I have 0 faith in people from CI&amp;R, they're lazy and they make things worse because they dont know the build or the jobs" read="1" status="-1" locked="0" date_sent="1771956694000" readable_date="2026-02-24 18:11:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771956606000" type="1" body="Dieshay says he want to go to erd shift now, because he wanted the spot but they didn't even ask" read="1" status="-1" locked="0" date_sent="1771956606000" readable_date="2026-02-24 18:10:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771956593000" type="2" body="She was lead out in CI&amp;R, she does know a few thing's" read="1" status="-1" locked="0" date_sent="1771956593000" readable_date="2026-02-24 18:09:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1771956526000" type="1" body="Dang that’s sad. The lead should be the best guy on the team" read="1" status="-1" locked="0" date_sent="1771956526000" readable_date="2026-02-24 18:08:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771956521000" type="1" body="She knows whats shes doing" read="1" status="-1" locked="0" date_sent="1771956521000" readable_date="2026-02-24 18:08:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771956499000" type="2" body="I mean doubt she is*" read="1" status="-1" locked="0" date_sent="1771956499000" readable_date="2026-02-24 18:08:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771956453000" type="2" body="She had flick the qa manager wrapped around her finger" read="1" status="-1" locked="0" date_sent="1771956453000" readable_date="2026-02-24 18:07:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771956422000" type="2" body="Yeah, and she blamed me for a lot of it on one of the planes. No doubt, and no doubt she isn't playing nick" read="1" status="-1" locked="0" date_sent="1771956422000" readable_date="2026-02-24 18:07:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771956358000" type="1" body="I think its because nick has a crush on her" read="1" status="-1" locked="0" date_sent="1771956358000" readable_date="2026-02-24 18:05:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1771956322000" type="1" body="A&amp;P and still effs stuff up and is lead? Love this company" read="1" status="-1" locked="0" date_sent="1771956322000" readable_date="2026-02-24 18:05:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771956305000" type="1" body="I wouldn't let her touch me" read="1" status="-1" locked="0" date_sent="1771956305000" readable_date="2026-02-24 18:05:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771956287000" type="1" body="All I know is, she fucks a lot of things up, especially the lap" read="1" status="-1" locked="0" date_sent="1771956287000" readable_date="2026-02-24 18:04:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771956181000" type="2" body="Just like our jakie poo...if she wasnt a lezbo I'd say they should hookup" read="1" status="-1" locked="0" date_sent="1771956181000" readable_date="2026-02-24 18:03:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066057095" date="1771949395000" type="1" body="Ill send it." read="1" status="-1" locked="0" date_sent="1771949395000" readable_date="2026-02-24 16:09:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+14259234274" date="1771949256000" type="1" body="Sounds like a badass" read="1" status="-1" locked="0" date_sent="1771949256000" readable_date="2026-02-24 16:07:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771949068000" type="2" body="Caitlin, and she's an A&amp;P" read="1" status="-1" locked="0" date_sent="1771949068000" readable_date="2026-02-24 16:04:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771948965000" type="2" body="Lol" read="1" status="-1" locked="0" date_sent="1771948965000" readable_date="2026-02-24 16:02:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771948959000" type="2" body="Oh, I'm tired af" read="1" status="-1" locked="0" date_sent="1771948959000" readable_date="2026-02-24 16:02:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1771948928000" type="1" body="He said white girl not girl with white hair lol" read="1" status="-1" locked="0" date_sent="1771948928000" readable_date="2026-02-24 16:02:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771948580000" type="2" body="Can dad send me $200? I overestimated on what I could send back, but now I know what I can afford to cut going forward. I'll have a new budget for you today or tomorrow, and I should be be able to wind down a little more every month." read="1" status="-1" locked="0" date_sent="1771948580000" readable_date="2026-02-24 15:56:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1771948321000" type="2" body="What girl with white hair?" read="1" status="-1" locked="0" date_sent="1771948321000" readable_date="2026-02-24 15:52:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771891934000" type="1" body="Lead" read="1" status="-1" locked="0" date_sent="1771891934000" readable_date="2026-02-24 00:12:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771891928000" type="1" body="Bald nick is a temp manager now and he made that fat white girl that does nothing a temp team leas" read="1" status="-1" locked="0" date_sent="1771891928000" readable_date="2026-02-24 00:12:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14253277371" date="1771889205000" type="1" body="Ok sounds good" read="1" status="-1" locked="0" date_sent="1771889205000" readable_date="2026-02-23 23:26:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDg1N0dDYWZ3U1U2MUNTZHY1QXg4RncqEF" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+14253277371" date="1771889194000" type="1" body="I'd be leaving from uberbeatz" read="1" status="-1" locked="0" date_sent="1771889194000" readable_date="2026-02-23 23:26:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG02VzlWdDEyUWxtMXNJTmtCaE81cncqED" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771889193000" type="2" body="Let's do tomorrow, give me some time to clean after work, and I might just take the day off" read="1" status="-1" locked="0" date_sent="1771889193000" readable_date="2026-02-23 23:26:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEE9eWp1OWw1UUlxdGlSdmZhU296blEqEJ" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771889113000" type="1" body="I'm meeting Will at 7 and would be done by 930 tonight. I can head down there afterwards unless tomorrow is better for you" read="1" status="-1" locked="0" date_sent="1771889113000" readable_date="2026-02-23 23:25:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHF5WHRYZEFhUWphUDA3WWxFY013OVEqEO" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771889027000" type="2" body="I can leave work around 6:30 or 7 either night." read="1" status="-1" locked="0" date_sent="1771889027000" readable_date="2026-02-23 23:23:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGlrSUdXZGlOUTNDT1lsSlM1NGZVTkEqEC" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771888957000" type="1" body="No plans. Meeting up with Will at 7 to jam for a bit. Then nothing. Nothing tomorrow either" read="1" status="-1" locked="0" date_sent="1771888957000" readable_date="2026-02-23 23:22:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHFJVHJBdTI2UndTM1VpNHM4d3IzemcqEH" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771888898000" type="2" body="What are you doing tonight or tomorrow?" read="1" status="-1" locked="0" date_sent="1771888898000" readable_date="2026-02-23 23:21:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGR5NzNwWFZaVFp1Q0xzWkl0NVZDVUEqED" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1771811318000" type="2" body="OK, I'm down. I'll figure out when I can get out of work early and I'll hit you up. And Happy Birthday" read="1" status="-1" locked="0" date_sent="1771811318000" readable_date="2026-02-23 01:48:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDhmcGRpTXhZUUZhNktqcXF3WnFoY1EqEA" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1771807284000" type="1" body="I'm off this week for my bday and going to Canada Thursday if you want to kick it at all this week" read="1" status="-1" locked="0" date_sent="1771807284000" readable_date="2026-02-23 00:41:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHloTHQ5WUpaVEdtZUhCLW10d3ZabmcqEF" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771794829000" type="2" body="Sure" read="1" status="-1" locked="0" date_sent="1771794829000" readable_date="2026-02-22 21:13:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE1zWi03R096VEJPcGJpeVRIcGJYa3cqEJ" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14258706692" date="1771792795000" type="1" body="Next Sunday? I'm not feeling too hot" read="1" status="-1" locked="0" date_sent="1771792795000" readable_date="2026-02-22 20:39:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZoMDg9VFY5VC1tWVZkYVU0d3A9eVEqEB" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771788758000" type="2" body="I can meet up with you any day before work. I start at 2:30, so any morning would work" read="1" status="-1" locked="0" date_sent="1771788758000" readable_date="2026-02-22 19:32:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEdTLWNkU1p3UjIyNDlIbUswRm1YSlEqEE" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1771788687000" type="2" body="No worries" read="1" status="-1" locked="0" date_sent="1771788687000" readable_date="2026-02-22 19:31:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFdaN2lzN2JhU1Utb2s9ejZoVS1GV0EqEJ" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14258706692" date="1771787660000" type="1" body="Just waking up sorry" read="1" status="-1" locked="0" date_sent="1771787660000" readable_date="2026-02-22 19:14:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGhLWFRENkJyUi0yd3BwRkZJQ05WSUEqEJ" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771786657000" type="2" body="What's up?" read="1" status="-1" locked="0" date_sent="1771786657000" readable_date="2026-02-22 18:57:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEdMRnV6TnVuU04yd1lMUUJrSkFIVkEqEI" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14253266955" date="1771736244000" type="1" body="I would love to, but tomorrow I’m helping my friend clear out her Dad’s stuff his house who passed away recently. If another day works for you let me know please" read="1" status="-1" locked="0" date_sent="1771736244000" readable_date="2026-02-22 04:57:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQwQkMxNkYxOS0xODhGLTQ4NUQtQkYzMS1DRU" group_addresses="+14253266955,+14253266955,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771734085000" type="1" body="3 round TKO" read="1" status="-1" locked="0" date_sent="1771734085000" readable_date="2026-02-22 04:21:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1771733892000" type="1" body="Dang I totally missed it" read="1" status="-1" locked="0" date_sent="1771733892000" readable_date="2026-02-22 04:18:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771733800000" type="1" body="What a fight" read="1" status="-1" locked="0" date_sent="1771733800000" readable_date="2026-02-22 04:16:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771717455000" type="2" body="I feel it, I do that with sears lol. But if you want, I'm helping a buddy get a job there also, we are meeting at noon tomorrow at the ed-lynn fellowship hall to go over their interview style and stuff if you wanna join us. We are gonna do the noon meeting then get some grub and go over everything" read="1" status="-1" locked="0" date_sent="1771717455000" readable_date="2026-02-21 23:44:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGYydk1peDlsVDdhM2w3RTRwQVYxd2cqEL" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+18165072664" date="1771717272000" type="1" body="perfect … sorry about today" read="1" status="-1" locked="0" date_sent="1771717272000" readable_date="2026-02-21 23:41:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQxODM2OTZFRC1BNUQxLTRBOEQtQTkyQS01RU" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771715626000" type="2" body="Lets schedule for the Sunday at 3 my time" read="1" status="-1" locked="0" date_sent="1771715626000" readable_date="2026-02-21 23:13:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE80aT1jdUJiUVVtd0s5Y1Vtek9zMVEqEP" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1771715212000" type="1" body="Ill tell you what I have so maybe we can fit something together….i have friday 2/27 at 4:00 (2:00 your time)Sunday 3/1 at 5:00 (3:00 your time) or 3/7 Saturday at 11:00 or 12:00 (9:00 or 10:00 am your time) .. any of those work?" read="1" status="-1" locked="0" date_sent="1771715212000" readable_date="2026-02-21 23:06:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQyNDcyRDM1Ri1FQTEyLTQzRUYtQTYxRC1DOT" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771714811000" type="2" body="During the week is extremely hard due to my work schedule, but I can probably make something work, can I let you know Monday after I see what my load looks like" read="1" status="-1" locked="0" date_sent="1771714811000" readable_date="2026-02-21 23:00:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG11ZTF1bm9FVEMteW15RGdjTFVIdWcqEI" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1771714694000" type="1" body="Hi matt Im sorry about today I had an emergency today and couldnt connect with you.. I apologize…. can we meet sometime this week?" read="1" status="-1" locked="0" date_sent="1771714694000" readable_date="2026-02-21 22:58:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRDMEJGMkNFMy02RjRGLTQyRjMtODVEMS0wRk" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771714412000" type="2" body="This is Matthew Davison, Ashley's client. Not sure if there's tech issues but I havnt gotten a meet notification and nothing seems to be happening on my end, or if we got time zones screwed up...but since for whatever reason we were unable to connect, should we reschedule for an earlier time next weekend? I can use a day of sick time to take next Saturday off, and if we schedule earlier give us time to work out kinks, since I know your 2 hours ahead, so almost 5pm there for you" read="1" status="-1" locked="0" date_sent="1771714412000" readable_date="2026-02-21 22:53:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG9vaHNPVlAwVHNPNkJvPUpXa1dJZFEqEL" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066125383" date="1771709395000" type="1" body="Nope she still doesnt care" read="1" status="-1" locked="0" date_sent="1771709395000" readable_date="2026-02-21 21:29:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771709367000" type="2" body="Stopped for an energy drink. Your landlord isn't going to care I parked in the post office garage right? She still doesn't care?" read="1" status="-1" locked="0" date_sent="1771709367000" readable_date="2026-02-21 21:29:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771709143000" type="1" body="You must have got in" read="1" status="-1" locked="0" date_sent="1771709143000" readable_date="2026-02-21 21:25:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1771694177000" type="1" body="Ok" read="1" status="-1" locked="0" date_sent="1771694177000" readable_date="2026-02-21 17:16:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771694083000" type="2" body="Okay thanks. I'll call in a bit" read="1" status="-1" locked="0" date_sent="1771694083000" readable_date="2026-02-21 17:14:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771694050000" type="1" body="Next door neighbor is opening" read="1" status="-1" locked="0" date_sent="1771694050000" readable_date="2026-02-21 17:14:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1771689891000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1771689891000" readable_date="2026-02-21 16:04:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771689877000" type="2" body="This is for something else Ashley wants me to do" read="1" status="-1" locked="0" date_sent="1771689877000" readable_date="2026-02-21 16:04:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771689828000" type="1" body="Okay so he's testing again?" read="1" status="-1" locked="0" date_sent="1771689828000" readable_date="2026-02-21 16:03:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771687528000" type="2" body="I need the apartment again at about 2" read="1" status="-1" locked="0" date_sent="1771687528000" readable_date="2026-02-21 15:25:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+14253193719" date="1771658102000" type="1" body="Great point. Thanks" read="1" status="-1" locked="0" date_sent="1771658102000" readable_date="2026-02-21 07:15:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFp0RXdZWHJBUVMycVhBTDZlend2dkEqEM" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771658060000" type="2" body="I don't blame you, it will be faster, but I'd be careful they don't say oh, used your own insurance? You have to pay us back. I'd talk to our insurance guy before doing that." read="1" status="-1" locked="0" date_sent="1771658060000" readable_date="2026-02-21 07:14:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHFCOVdtVmVUUnZXNXplekxBNVB2PVEqEB" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1771657963000" type="1" body="Yeah I'm probably going to end up just using my insurance instead of fighting them." read="1" status="-1" locked="0" date_sent="1771657963000" readable_date="2026-02-21 07:12:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGlwc3VFWjRBVDNLdW5FdXU4LWZwZEEqEH" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771657905000" type="2" body="No worries, that sucks the IME was fucked. Was hoping you would get some good news" read="1" status="-1" locked="0" date_sent="1771657905000" readable_date="2026-02-21 07:11:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFltbzRmT2VpUjZLTnZ6bVVrc0pQb3cqEP" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1771657847000" type="1" body="I was thinking about it. But I have to get up early. I'm just going to grab something to eat and go to Kristinas. Sorry man. IME was fucked as expected." read="1" status="-1" locked="0" date_sent="1771657847000" readable_date="2026-02-21 07:10:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEFEa2h4VjRLUUNhTmlyY1A5VkUxQ1EqEP" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771657755000" type="2" body="You coming by the hall?" read="1" status="-1" locked="0" date_sent="1771657755000" readable_date="2026-02-21 07:09:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDZCQlRrbmNMUjJlV0ptTHhaRVpEZ1EqEN" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+12066990041" date="1771652121000" type="2" body="Ah that's right" read="1" status="-1" locked="0" date_sent="1771652121000" readable_date="2026-02-21 05:35:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFpEUEJpSU42UlFPdm5tTHZnbWdMZ2cqEM" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1771652104000" type="1" body="Working till 6 am" read="1" status="-1" locked="0" date_sent="1771652104000" readable_date="2026-02-21 05:35:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGQyUXVZR1VtUUdTMy1NTng1NGVhNWcqEC" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771652011000" type="2" body="You should come to the 10 if you can" read="1" status="-1" locked="0" date_sent="1771652011000" readable_date="2026-02-21 05:33:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFIzdlpUY3hZUkRtaHpmVzRnYjRrU1EqEB" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+14253266955" date="1771624128000" type="1" body="I sent you my resume and cover letter. I smudged it saying I worked at group health longer than I did, but they can’t really check that because the company no longer exists haha. Thanks" read="1" status="-1" locked="0" date_sent="1771624128000" readable_date="2026-02-20 21:48:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ0ODI4NkM3QS03MTU5LTQ5QjYtQTYwMi1DOT" group_addresses="+14253266955,+14253266955,+12066990041" />
<sms protocol="0" address="+12066571038" date="1771576176000" type="1" body="Ok cool been missing ya" read="1" status="-1" locked="0" date_sent="1771576176000" readable_date="2026-02-20 08:29:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFF2RzduV0tQUnJ1WTlxRm5KUU50b1EqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771575967000" type="2" body="Let me check how much money I have" read="1" status="-1" locked="0" date_sent="1771575967000" readable_date="2026-02-20 08:26:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEZQZTlLdTZrVEhhak5Nam8zVmd1TkEqEL" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1771575926000" type="1" body="Dennys afterwords I can pay you back for mine and Ashley's tomorrow as soon as my check clear" read="1" status="-1" locked="0" date_sent="1771575926000" readable_date="2026-02-20 08:25:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDE2V1dUaz15VDJPU2dJM005RmEzV1EqEF" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1771572741000" type="1" body="Hey man haven't heard from you in a while you ok" read="1" status="-1" locked="0" date_sent="1771572741000" readable_date="2026-02-20 07:32:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD1ZTGYweTNvU3ZTNmd3SDhuU1VHV0EqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+14253193719" date="1771572715000" type="1" body="What's going on Matt?" read="1" status="-1" locked="0" date_sent="1771572715000" readable_date="2026-02-20 07:31:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHlHOTFudkJ6UkhLNnNqST1jQVVJWFEqEJ" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+14253266955" date="1771542423000" type="1" body="Thanks, I’ll look that up and email you tonight!" read="1" status="-1" locked="0" date_sent="1771542423000" readable_date="2026-02-19 23:07:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRGMDEzNDdEQy1ENTkxLTRBODItODNGOC1DRE" group_addresses="+14253266955,+14253266955,+12066990041" />
<sms protocol="0" address="+14258706692" date="1771536821000" type="1" body="" read="1" status="-1" locked="0" date_sent="1771536821000" readable_date="2026-02-19 21:33:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFlwWWpaSTZaUmJPNnVCeWFzZ3NIVUEqEA" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771536817000" type="2" body="Will do" read="1" status="-1" locked="0" date_sent="1771536817000" readable_date="2026-02-19 21:33:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeC1OYS1LM0hIVGktaXlkQnpuTUN4blEqEB" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14258706692" date="1771536799000" type="1" body="It's cool call me back if ya got a min" read="1" status="-1" locked="0" date_sent="1771536799000" readable_date="2026-02-19 21:33:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFUxMXloTUtKUy11ZEkwcVYwQjZ5YVEqEI" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771536777000" type="2" body="Sorry got a new phone" read="1" status="-1" locked="0" date_sent="1771536777000" readable_date="2026-02-19 21:32:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDN1VlNmZjdxUWxxNFJEM1k4Ri1aWFEqEC" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14258706692" date="1771536777000" type="1" body="Not much trying to talk to you lol" read="1" status="-1" locked="0" date_sent="1771536777000" readable_date="2026-02-19 21:32:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGV3RnlLUDZkUWhxVWs2QkVOMHFXVHcqEC" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771536756000" type="2" body="Oh shit sup" read="1" status="-1" locked="0" date_sent="1771536756000" readable_date="2026-02-19 21:32:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHdXT1FlYkNXUTFTTDJyN1k1ZDZ1aHcqEI" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+14258706692" date="1771536745000" type="1" body="It's Brandon you jackwagon" read="1" status="-1" locked="0" date_sent="1771536745000" readable_date="2026-02-19 21:32:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFB0RUpIQnctU1VLdDFPSkZaYVlnV2cqEO" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+14258706692" date="1771536712000" type="1" body="Who did ?" read="1" status="-1" locked="0" date_sent="1771536712000" readable_date="2026-02-19 21:31:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFozbUJFcE45U3VTTFBEMUktbFhESkEqEK" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771536673000" type="2" body="Who did?" read="1" status="-1" locked="0" date_sent="1771536673000" readable_date="2026-02-19 21:31:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG96ME84SmRGUkhXcHFkYkdSOFVCN2cqEA" group_addresses="+12066990041,+14258706692" />
<sms protocol="0" address="+12066990041" date="1771529712000" type="2" body="Also if you havent looked up the STAR method, I would. Its how Boeing does their hiring process" read="1" status="-1" locked="0" date_sent="1771529712000" readable_date="2026-02-19 19:35:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHhHVElDazlYUjNlSHktc2VNQUFMdmcqEO" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+12066990041" date="1771529640000" type="2" body="Things been OK. Yeah you can email me resume and cover letter, mbdavison8@gmail.com, I put my references on my resume its self." read="1" status="-1" locked="0" date_sent="1771529640000" readable_date="2026-02-19 19:34:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeERtTmJSeUl4VE91bk85NzFtUDE9R3cqEH" group_addresses="+12066990041,+14253266955" />
<sms protocol="0" address="+14258706692" date="1771523918000" type="1" body="Yo" read="1" status="-1" locked="0" date_sent="1771523918000" readable_date="2026-02-19 17:58:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="54" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHZPSzRiSUVlVEMtbXh2Tk9MMFVvRlEqEJ" group_addresses="+14258706692,+14258706692,+12066990041" />
<sms protocol="0" address="+14253266955" date="1771475220000" type="1" body="Things are okay with me. How is life treating you? Yes, I can definitely use some help ahaha. I’ve been applying for jobs like crazy for the last few months! Do you want me to send you my cover letter and resume? Every time I’ve filled out a job application for Boeing they haven’t asked for references" read="1" status="-1" locked="0" date_sent="1771475220000" readable_date="2026-02-19 04:27:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRBNTI0NDdGNy1GNTdCLTQyQkItQTIxQS0zQT" group_addresses="+14253266955,+14253266955,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771473549000" type="2" body="Hey Brady, its your cousin Matt. I've been meaning to touch base with you, how's life been? Also, I hear you have been applying at Boeing. If you want I can help you with your resume and see if the referral program is active right now or not. If it is, the person applying almost always get the job you apply for if its on the list. Just let me know!" read="1" status="-1" locked="0" date_sent="1771473549000" readable_date="2026-02-19 03:59:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="53" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHJZUzJTOWZYUVM2STlkTG1YWEE3dWcqEH" group_addresses="+12066990041,4253266955" />
<sms protocol="0" address="+12063210430" date="1771462580000" type="1" body="Hope you ate" read="1" status="-1" locked="0" date_sent="1771462580000" readable_date="2026-02-19 00:56:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRENjkzM0FCNy1DMjI0LTRDQkYtQjRDQS04Rj" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771393373000" type="1" body="Thats the rumor" read="1" status="-1" locked="0" date_sent="1771393373000" readable_date="2026-02-18 05:42:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="52" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDgtZlIyNDV3UXpTb1NyOFJoS0plM2cqEJ" group_addresses="+19168990823,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771393352000" type="2" body="Idk why,?" read="1" status="-1" locked="0" date_sent="1771393352000" readable_date="2026-02-18 05:42:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="52" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDN1UEdkPU04UVJ5MC10YTczaDRmS1EqEC" group_addresses="+12066990041,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771393318000" type="1" body="Are you transferring to my shop?" read="1" status="-1" locked="0" date_sent="1771393318000" readable_date="2026-02-18 05:41:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="52" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD16WVVvMkhnVHktN1lrSk5oYkc5V2cqEO" group_addresses="+19168990823,+19168990823,+12066990041" />
<sms protocol="0" address="+12066125383" date="1771390946000" type="1" body="Fingers crossed" read="1" status="-1" locked="0" date_sent="1771390946000" readable_date="2026-02-18 05:02:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771390919000" type="2" body="OK, I never heard back after the test. The guy said he would send the results at the end of the day. With the time difference might not hear until tomorrow. Only thing the guy said after the test was I hope this helps your case, but idk if that means anything" read="1" status="-1" locked="0" date_sent="1771390919000" readable_date="2026-02-18 05:01:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771360167000" type="1" body="Brady is 425 326 6955 He said he has no message from Matt so they both need wrong numbers" read="1" status="-1" locked="0" date_sent="1771360167000" readable_date="2026-02-17 20:29:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771299681000" type="2" body="I rescheduled for tomorrow, and it looks like I will need that $900 until at least Thursday. I have a flat tire, I'll have to see if my tire will hold air long enough after work to drive or not, but I will have to drop my car off at les Schwab before my appointment or on my way home and uber, and yes I still have the $750. But I will pay the cost of the tires back if its non-repairable, and if it is I'll send it back. My only other option is to try and use one of the high cost credit/payment plans they offer. I know you guys are mad at me for missing Sunday, but I don't want to fight right now and I don't want you giving up on me. I already feel like a loser for asking for help" read="1" status="-1" locked="0" date_sent="1771299681000" readable_date="2026-02-17 03:41:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1771282855000" type="2" body="Ill be on a LOA for a few weeks, but id stay away from the waveyness tag, I filed a speak up about that and a few other issues, namely that every request Ive made since my restrictions came off has gone unawnsered, and a few other issue. Also a peice of advice, always have your outlook set to send read and delivery recipts. One can be rejected by the sender, the other cant, and it can cover your butt." read="1" status="-1" locked="0" date_sent="1771282855000" readable_date="2026-02-16 23:00:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDFXZEF5blhqUzdhZGdFaXYzbFZyYUEqED" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+12066990041" date="1771282321000" type="2" body="You might want to let QA know that we reported that fod in STA 888 to our lead as soon as it was found. That we did not delay in reporting or try to hide it, just tell them the truth. That we just didn't see it when we put it together since the job was picked up from first shift and you were only helping out, the job was mine. I'd also grab a Union steward. Can't say more" read="1" status="-1" locked="0" date_sent="1771282321000" readable_date="2026-02-16 22:52:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEYxdVQ9dURmUk1TYll1Y012Ym12OXcqEO" group_addresses="+12066990041,+14254052681" />
<sms protocol="0" address="+12063210430" date="1771172059000" type="1" body="We were thinking mid day or later. If you can’t we understand" read="1" status="-1" locked="0" date_sent="1771172059000" readable_date="2026-02-15 16:14:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRFODU1MDUzRC0wNDJELTQzMzItOTc0RC1CND" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+12066125383" date="1771139112000" type="1" body="Give him another day." read="1" status="-1" locked="0" date_sent="1771139112000" readable_date="2026-02-15 07:05:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771138557000" type="2" body="I txted Brady yesterday morning and never head back" read="1" status="-1" locked="0" date_sent="1771138557000" readable_date="2026-02-15 06:55:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1771138513000" type="2" body="Not even close, but what's new" read="1" status="-1" locked="0" date_sent="1771138513000" readable_date="2026-02-15 06:55:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFFtSFZ0V2Z3UVFHMk9HUHVDY2lOSUEqEG" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1771134530000" type="1" body="You good bro?" read="1" status="-1" locked="0" date_sent="1771134530000" readable_date="2026-02-15 05:48:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFd2VjYtWmdjVHAtOVd6azBCRXM4NFEqEA" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771125895000" type="2" body="OK thank you and sorry" read="1" status="-1" locked="0" date_sent="1771125895000" readable_date="2026-02-15 03:24:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1771125874000" type="1" body="OK. I'll send 150." read="1" status="-1" locked="0" date_sent="1771125874000" readable_date="2026-02-15 03:24:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771125851000" type="2" body="Everything is good down here, he's working on the blend right now. As soon as he is done he is gonna put it up for u/s and reconvene" read="1" status="-1" locked="0" date_sent="1771125851000" readable_date="2026-02-15 03:24:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="50" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEMyUzNKOXAyVFdTPUdHNk4wM1ZtLUEqEE" group_addresses="+12066990041,+14254041957" />
<sms protocol="0" address="+12066990041" date="1771124925000" type="2" body="But that $150 should the last I'll need, that's $600 less then before" read="1" status="-1" locked="0" date_sent="1771124925000" readable_date="2026-02-15 03:08:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1771124876000" type="2" body="Can dad send me another $150? Sorry as I am adjusting to try and figure out the new budget I realized I forgot to include my car tabs this month. My next check later this week should have my bonus. Not sure how much it will be though" read="1" status="-1" locked="0" date_sent="1771124876000" readable_date="2026-02-15 03:07:56 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+14253277371" date="1771116925000" type="1" body="Woah, that's crazy" read="1" status="-1" locked="0" date_sent="1771116925000" readable_date="2026-02-15 00:55:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG1YQW56THE9UUJpb2w4c3NEYWdBb3cqEM" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771116644000" type="2" body="Wtf...ai take over has begun lol" read="1" status="-1" locked="0" date_sent="1771116644000" readable_date="2026-02-15 00:50:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGhpdktPc0M4UUhhVmg0NFhVQnc3YncqED" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1771116629000" type="2" body="Dude...https://theshamblog.com/an-ai-agent-published-a-hit-piece-on-me/" read="1" status="-1" locked="0" date_sent="1771116629000" readable_date="2026-02-15 00:50:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeElHdVJXdFhDUjdPQ1dyVzJLTWNERkEqEJ" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1771108327000" type="2" body="You coming in today?" read="1" status="-1" locked="0" date_sent="1771108327000" readable_date="2026-02-14 22:32:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEU2ZEZmQU0tUXpLR215U0NXUHpVZVEqEC" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+14253193719" date="1771054687000" type="1" body="You working late?" read="1" status="-1" locked="0" date_sent="1771054687000" readable_date="2026-02-14 07:38:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHkxMGl0Vk96UWRXZG51NT1GOVVUQXcqEP" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771049236000" type="2" body="Trying to get my job done before 11" read="1" status="-1" locked="0" date_sent="1771049236000" readable_date="2026-02-14 06:07:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE9CQ2pjQXBsVDNLc2lTaTlhSzU0SkEqEA" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066990041" date="1771049200000" type="2" body="I'm getting buy. Hoping ibcan make tonight. Fucking mandatory 10's are killing me." read="1" status="-1" locked="0" date_sent="1771049200000" readable_date="2026-02-14 06:06:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDc4V2lObm16VGxpZlpnZVAwWk1taXcqEI" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+19168990823" date="1771049083000" type="1" body="Crack me over the head a few times some I can forget this week" read="1" status="-1" locked="0" date_sent="1771049083000" readable_date="2026-02-14 06:04:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771049039000" type="2" body="That thing you get hit with to make you forget things" read="1" status="-1" locked="0" date_sent="1771049039000" readable_date="2026-02-14 06:03:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771049019000" type="1" body="Its been a tough week" read="1" status="-1" locked="0" date_sent="1771049019000" readable_date="2026-02-14 06:03:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771049002000" type="2" body="Retard only stuff" read="1" status="-1" locked="0" date_sent="1771049002000" readable_date="2026-02-14 06:03:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771048998000" type="1" body="Phone book?" read="1" status="-1" locked="0" date_sent="1771048998000" readable_date="2026-02-14 06:03:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1771048965000" type="1" body="What are you retards talking about" read="1" status="-1" locked="0" date_sent="1771048965000" readable_date="2026-02-14 06:02:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771048941000" type="2" body="I clicked your number while trying to scroll in my phone book and the screen stuck. Called you instead of scrolling. Prolly happened cu I'm fat and white" read="1" status="-1" locked="0" date_sent="1771048941000" readable_date="2026-02-14 06:02:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771048815000" type="2" body="It means didn't mean to" read="1" status="-1" locked="0" date_sent="1771048815000" readable_date="2026-02-14 06:00:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771048814000" type="1" body="There are no accidents" read="1" status="-1" locked="0" date_sent="1771048814000" readable_date="2026-02-14 06:00:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771048797000" type="2" body="I think the key keyword there is accidentally" read="1" status="-1" locked="0" date_sent="1771048797000" readable_date="2026-02-14 05:59:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771048767000" type="1" body="Why" read="1" status="-1" locked="0" date_sent="1771048767000" readable_date="2026-02-14 05:59:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771048755000" type="2" body="Lol, guess it didn't. I accidentally started calling you" read="1" status="-1" locked="0" date_sent="1771048755000" readable_date="2026-02-14 05:59:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1771048754000" type="1" body="" read="1" status="-1" locked="0" date_sent="1771048754000" readable_date="2026-02-14 05:59:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1771048718000" type="1" body="Huh" read="1" status="-1" locked="0" date_sent="1771048718000" readable_date="2026-02-14 05:58:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771048700000" type="2" body="Idk if that call dialed if it did sorry" read="1" status="-1" locked="0" date_sent="1771048700000" readable_date="2026-02-14 05:58:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771048659000" type="2" body="Doh" read="1" status="-1" locked="0" date_sent="1771048659000" readable_date="2026-02-14 05:57:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1771042833000" type="2" body="Sorry I didn't get back you sooner, been crazy at work. I'll have to see if I can make Sunday morning work, what time would you be in the area?" read="1" status="-1" locked="0" date_sent="1771042833000" readable_date="2026-02-14 04:20:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE4weGluSm0xUnB1WkRyd3hzUGZ5WHcqEM" group_addresses="+12066990041,+12063210430" />
<sms protocol="0" address="+12066990041" date="1771041901000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1771041901000" readable_date="2026-02-14 04:05:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771041895000" type="1" body="Here's Bradys info. Dad will send in a bit" read="1" status="-1" locked="0" date_sent="1771041895000" readable_date="2026-02-14 04:04:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1771041867000" type="1" body="" read="1" status="-1" locked="0" date_sent="1771041867000" readable_date="2026-02-14 04:04:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771041784000" type="2" body="Oh have dad send the money. $750" read="1" status="-1" locked="0" date_sent="1771041784000" readable_date="2026-02-14 04:03:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1771037868000" type="1" body="No problem. Im in no hurry" read="1" status="-1" locked="0" date_sent="1771037868000" readable_date="2026-02-14 02:57:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771037842000" type="2" body="Just on my way home. Traffic is pretty bad due to rain." read="1" status="-1" locked="0" date_sent="1771037842000" readable_date="2026-02-14 02:57:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1771036008000" type="2" body="I am gonna take off" read="1" status="-1" locked="0" date_sent="1771036008000" readable_date="2026-02-14 02:26:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="48" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeER1UVZLMUJpU3lDQlZGYzdWQkxBb0EqEH" group_addresses="+12066990041,+14252127645" />
<sms protocol="0" address="+12065320025" date="1771035991000" type="1" body="Okay" read="1" status="-1" locked="0" date_sent="1771035991000" readable_date="2026-02-14 02:26:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQwRkRBQjdFNC0xQjBDLTRCMUMtOTExMi02QU" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771035977000" type="2" body="OK I will call in a second, in the factory." read="1" status="-1" locked="0" date_sent="1771035977000" readable_date="2026-02-14 02:26:17 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEZtODE9cUs0VEpHeGFBZ0N1YUVxZXcqEM" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12065320025" date="1771035955000" type="1" body="Yes, Sunday at 3:00. Call me right now. I forgot whether I gave you last minute instructions" read="1" status="-1" locked="0" date_sent="1771035955000" readable_date="2026-02-14 02:25:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ5QUQzNDI1Qi00RThGLTRDNTUtQjNFQS1BOD" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771035811000" type="2" body="I called and left a message, we are still good for Sunday? You got Ashley to review the questions?" read="1" status="-1" locked="0" date_sent="1771035811000" readable_date="2026-02-14 02:23:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFB0bVRpZXJ3UUNLOFlNZ3FwVTNQMFEqEH" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12066990041" date="1771035759000" type="2" body="Its Matt, I might take off at lunch as, dates changed for the the time off I needed next week. We we rescheduled for a weekend" read="1" status="-1" locked="0" date_sent="1771035759000" readable_date="2026-02-14 02:22:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="48" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFJFRktIVk85UjAySWYxNGRueEdpbFEqEH" group_addresses="+12066990041,+14252127645" />
<sms protocol="0" address="+14257377604" date="1771030176000" type="1" body="" read="1" status="-1" locked="0" date_sent="1771030176000" readable_date="2026-02-14 00:49:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="8" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE9FYjZBejBzU0wtemdIamZ1bHZFcGcqEI" group_addresses="+14257377604,+14257377604,+12066990041" />
<sms protocol="0" address="+12066990041" date="1771027190000" type="2" body="I don't see anything at the tube, I just stopped by after talking to that LE about a tag" read="1" status="-1" locked="0" date_sent="1771027190000" readable_date="2026-02-13 23:59:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDBQWFVJempGUUtLUm0zVFlrWHhETFEqEI" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+12066571038" date="1771023048000" type="1" body="How ya holding" read="1" status="-1" locked="0" date_sent="1771023048000" readable_date="2026-02-13 22:50:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDE9ZDN5cm9NVFpLbTU1Y0xiMUFpTUEqEO" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066571038" date="1771023041000" type="1" body="Hey brotha" read="1" status="-1" locked="0" date_sent="1771023041000" readable_date="2026-02-13 22:50:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDVUOUlDb0V6U04tb2xiTUoyNU9YVFEqEM" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12063210430" date="1771010684000" type="1" body="Let me know if we could come up maybe to your moms and you could" read="1" status="-1" locked="0" date_sent="1771010684000" readable_date="2026-02-13 19:24:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiREN0U1RTAwRi0zRTVGLTRGNzItQTgwQy0yRj" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+12065320025" date="1771005820000" type="1" body="Sunday at 3:00 pm is a go. Call me sometime today and confirm" read="1" status="-1" locked="0" date_sent="1771005820000" readable_date="2026-02-13 18:03:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQzMkNFREFFNS05QzM4LTQyNTQtQkZEQy0xRT" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12065320025" date="1770952709000" type="1" body="I meant to include that you can call tonight up to 1:00 am." read="1" status="-1" locked="0" date_sent="1770952709000" readable_date="2026-02-13 03:18:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRDRjA0OUQxRS1EMDQ3LTRGQkMtOEIxRi05OD" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12065320025" date="1770951877000" type="1" body="No problems! Call me any time tonight when you’ve got time. Ian usually up till 1:00 or 2:00. I left a voicemail on your phone" read="1" status="-1" locked="0" date_sent="1770951877000" readable_date="2026-02-13 03:04:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQwQ0E5NDk3MC00OTlDLTQ3QzktQkE1Qy1GMz" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770951738000" type="2" body="In after lunch meeting" read="1" status="-1" locked="0" date_sent="1770951738000" readable_date="2026-02-13 03:02:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGpmVUZQVnAzUT1tN0Z1SzhGcXNKZEEqEA" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+18165072664" date="1770933902000" type="1" body="perfect…. ill see you on 2/21!" read="1" status="-1" locked="0" date_sent="1770933902000" readable_date="2026-02-12 22:05:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQxMjNBRjQ2Qy03Mzc4LTRCRkMtQjk4NS1GQ0" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770933840000" type="2" body="Brady is my middle name in case you are wondering on the name difference" read="1" status="-1" locked="0" date_sent="1770933840000" readable_date="2026-02-12 22:04:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGc3Nz0yRU5BUUQyeTJvVnFpWXA5MHcqEJ" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1770933801000" type="2" body="Bradytheking17@gmail.com" read="1" status="-1" locked="0" date_sent="1770933801000" readable_date="2026-02-12 22:03:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEtZcUhiY3RxUk1TSzlRNmhWcGc9WHcqEE" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1770933763000" type="2" body="Yes I do have google Meet" read="1" status="-1" locked="0" date_sent="1770933763000" readable_date="2026-02-12 22:02:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGtLNTA5U2tjUmdpYzAyLU0zMGFTR0EqEP" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1770933744000" type="1" body="do you have google meet?" read="1" status="-1" locked="0" date_sent="1770933744000" readable_date="2026-02-12 22:02:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ2OTBBODkxNC00QjQzLTQ0RDItOTA5Ri04OD" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770933709000" type="2" body="I don't have apple so I can only do Face time via a weblink" read="1" status="-1" locked="0" date_sent="1770933709000" readable_date="2026-02-12 22:01:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD0zTW5UcDVjUTVLNj1BU2wyQVJHd2cqEO" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1770933566000" type="1" body="perfect… face time?" read="1" status="-1" locked="0" date_sent="1770933566000" readable_date="2026-02-12 21:59:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQxNkFCMjI3Qi1GRDM0LTREODctQjFBMC1FQz" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770933508000" type="2" body="That's works for me" read="1" status="-1" locked="0" date_sent="1770933508000" readable_date="2026-02-12 21:58:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHU9SzBPZTlrUTk2a0puZE5EUlMtRVEqEI" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+18165072664" date="1770933449000" type="1" body="i can do 2:30 your time on 2/21 Saturday" read="1" status="-1" locked="0" date_sent="1770933449000" readable_date="2026-02-12 21:57:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ5RDU2NUZCMi1ENUZBLTRBRUMtQUEzMC1GRT" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770933283000" type="2" body="Next Saturday will be my off weekend" read="1" status="-1" locked="0" date_sent="1770933283000" readable_date="2026-02-12 21:54:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeD1OSXNYQXBvUVpLS3gxNjZHOEtzUkEqEP" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1770933205000" type="2" body="This is Matthew Davison, my Attorney is Ashley Repp, sorry it took me a bit to get back to you. I had to talk with my attorney about a few things and then figure out my OT schedule at work. (I am 2 hours behind you guys by the way). It has made scheduling things a bit complicated. I work 2nd shift at Boeing so my hours are 2:30pm PT to 11:00PM PT Mon - Fri, but recently we have been on mandatory weekends. I don't know how long each appointment is, but because of the OT, either early morning (for me) Saturdays or if your available, Sundays would work." read="1" status="-1" locked="0" date_sent="1770933205000" readable_date="2026-02-12 21:53:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHM4QlVUQTM0VC1hOXluZjhDSHRSNFEqEN" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1770927989000" type="2" body="I get that." read="1" status="-1" locked="0" date_sent="1770927989000" readable_date="2026-02-12 20:26:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770927922000" type="1" body="Yeah we used it last year when we went to a few games, it’s so much better I’d just need my car just in case anything happens" read="1" status="-1" locked="0" date_sent="1770927922000" readable_date="2026-02-12 20:25:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927854000" type="2" body="Well, not really free. But yeah. If you have never taken the train, you should some time. Its fun. Picks up in everett, mukilteo and Edmonds." read="1" status="-1" locked="0" date_sent="1770927854000" readable_date="2026-02-12 20:24:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927756000" type="1" body="Go for free" read="1" status="-1" locked="0" date_sent="1770927756000" readable_date="2026-02-12 20:22:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927750000" type="1" body="Yeah, I have an orca card" read="1" status="-1" locked="0" date_sent="1770927750000" readable_date="2026-02-12 20:22:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927725000" type="2" body="Yeah I wouldn't drink, I'd also probably take the train or light rail. They will have the sounder running for opening weekend, its like 6 bucks" read="1" status="-1" locked="0" date_sent="1770927725000" readable_date="2026-02-12 20:22:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927678000" type="1" body="No, ill check" read="1" status="-1" locked="0" date_sent="1770927678000" readable_date="2026-02-12 20:21:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927654000" type="2" body="Did you check on like Craig's list for people selling their season tickets? There are a lot of people who buy the home game full season, and then keep one game per series and sell the rest. Used to be able to get &quot;seasons pass&quot; for pretty cheap" read="1" status="-1" locked="0" date_sent="1770927654000" readable_date="2026-02-12 20:20:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927520000" type="1" body="Ok, I dont think any of us really drink anymore" read="1" status="-1" locked="0" date_sent="1770927520000" readable_date="2026-02-12 20:18:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927513000" type="2" body="I remember when M's tickets were $15 dollars for the 300 level just above the 3rd base line in the kingdom. Best seats for catching foul balls and great view" read="1" status="-1" locked="0" date_sent="1770927513000" readable_date="2026-02-12 20:18:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927486000" type="1" body="Id buy season tickets next year" read="1" status="-1" locked="0" date_sent="1770927486000" readable_date="2026-02-12 20:18:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770927479000" type="1" body="If I go I won’t be able to drink and I’ll probably have to drive separately because the wife is getting close to giving birth around that time" read="1" status="-1" locked="0" date_sent="1770927479000" readable_date="2026-02-12 20:17:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927464000" type="1" body="" read="1" status="-1" locked="0" date_sent="1770927464000" readable_date="2026-02-12 20:17:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927435000" type="1" body="Took my dad there" read="1" status="-1" locked="0" date_sent="1770927435000" readable_date="2026-02-12 20:17:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927427000" type="1" body="I looked because thats where I sat at a game last year and didnt see any. If you find some its 100% worth it" read="1" status="-1" locked="0" date_sent="1770927427000" readable_date="2026-02-12 20:17:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927378000" type="2" body="Section 243's not bad. Any club level tickets available? Just wondering" read="1" status="-1" locked="0" date_sent="1770927378000" readable_date="2026-02-12 20:16:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927353000" type="1" body="I look at getting season tickets but its too much and too late" read="1" status="-1" locked="0" date_sent="1770927353000" readable_date="2026-02-12 20:15:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927322000" type="1" body="Cleveland" read="1" status="-1" locked="0" date_sent="1770927322000" readable_date="2026-02-12 20:15:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770927283000" type="2" body="Who are we playing against?" read="1" status="-1" locked="0" date_sent="1770927283000" readable_date="2026-02-12 20:14:43 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770927256000" type="2" body="I'm down" read="1" status="-1" locked="0" date_sent="1770927256000" readable_date="2026-02-12 20:14:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770927233000" type="1" body="Opening day weekend, yall wanna go" read="1" status="-1" locked="0" date_sent="1770927233000" readable_date="2026-02-12 20:13:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770927220000" type="1" body="" read="1" status="-1" locked="0" date_sent="1770927220000" readable_date="2026-02-12 20:13:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12063210430" date="1770857915000" type="1" body="Thanks Matt" read="1" status="-1" locked="0" date_sent="1770857915000" readable_date="2026-02-12 00:58:35 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQwNzY2NDZDNi1EQzgxLTQyOUItODMxRC02Nz" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+12063210430" date="1770857911000" type="1" body="He will follow your instructions and see if it helps. He is getting a new phones tomorrow and will go to AT&amp;T and have them take care of his phone" read="1" status="-1" locked="0" date_sent="1770857911000" readable_date="2026-02-12 00:58:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQyQ0UyODg3Qy1CM0VCLTREMTAtQUM4MS1EMT" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770856818000" type="2" body="If you do go to best buy, tell them you need a system restore because he may have been infected with a virus, or see if one of the grandkids can help him do that. System restore is pretty simple" read="1" status="-1" locked="0" date_sent="1770856818000" readable_date="2026-02-12 00:40:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZpN2pkYWEyU2F1S2wxU1AzRnIzSmcqEG" group_addresses="+12066990041,+12063210430" />
<sms protocol="0" address="+12066990041" date="1770856536000" type="2" body="Yes, I can help, but the earliest would be this weekend because of my work schedule. Best thing to do if he hasn't already is change all his passwords, uninstall any app on his phone that isn't necessary and if he still has access to his email, see if he had clicked a link in his email that looked like it came from his bank . Right now there are a lot of people getting emails that look completely official, even have an official bank email that are fake and that's how people are getting access. It might be worth going to best buys geek squad counter and have them take a look since I might not be free until Sunday depending on if we have to work Saturday or not. On another note for his pictures, I have not forgotten, I just need to find my hard drive adapter to hook his old drive up to my computer." read="1" status="-1" locked="0" date_sent="1770856536000" readable_date="2026-02-12 00:35:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGs9VnUzdk1nU2V5MUxxT0JiZ1UxZFEqEC" group_addresses="+12066990041,+12063210430" />
<sms protocol="0" address="+12063210430" date="1770852792000" type="1" body="Matt Mikes been hacked and we think it might be his computer. Any chance you could by tomorrow sometime and help us or we could meet at your folks apt with his computer" read="1" status="-1" locked="0" date_sent="1770852792000" readable_date="2026-02-11 23:33:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="46" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRGQTA1REE1Mi1FMUU2LTRBOEUtODNGOS1DMT" group_addresses="+12063210430,+12063210430,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770840133000" type="2" body="I bet, the news said to stay away from seatac to Seattle today" read="1" status="-1" locked="0" date_sent="1770840133000" readable_date="2026-02-11 20:02:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770710339000" type="2" body="That's what she said" read="1" status="-1" locked="0" date_sent="1770710339000" readable_date="2026-02-10 07:58:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770710329000" type="2" body="How you been?" read="1" status="-1" locked="0" date_sent="1770710329000" readable_date="2026-02-10 07:58:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEZqU2xTVDJGVDJLTD0zYkZCMHhOc3cqEE" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+12066990041" date="1770710321000" type="2" body="Their going. Mostly just a waiting game now until the polygraph. Other than that just been working. Lots of OT, this week is the first week we havnt been designated, and I thinks that's in part because we had a death last week in my work area. The guy collapsed, hit his head hard. CPR was performed, but we found out as we were searching, they took all the AEDs out of the part of the factory we work in" read="1" status="-1" locked="0" date_sent="1770710321000" readable_date="2026-02-10 07:58:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEZtPU9JQjk4UTBLOGFTeUc9a1QzdlEqEG" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1770703667000" type="1" body="Hows things going?" read="1" status="-1" locked="0" date_sent="1770703667000" readable_date="2026-02-10 06:07:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDNUeGtWaDByUjFLR1R0RHZENUpFLVEqEJ" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770686353000" type="1" body="Good times are coming" read="1" status="-1" locked="0" date_sent="1770686353000" readable_date="2026-02-10 01:19:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770686154000" type="2" body="Hell yeah" read="1" status="-1" locked="0" date_sent="1770686154000" readable_date="2026-02-10 01:15:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770686080000" type="1" body="Geoff Moody is back as my manager" read="1" status="-1" locked="0" date_sent="1770686080000" readable_date="2026-02-10 01:14:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770682237000" type="2" body="Better make it $50" read="1" status="-1" locked="0" date_sent="1770682237000" readable_date="2026-02-10 00:10:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770681929000" type="2" body="Can dad send me $30, I'm still adjusting to the new budget. I get the 1100 tomorrow night, just will need to get gas tonight. I'm still bellow the 2800 I still won't need the extra $900. (Excluding court stuff)" read="1" status="-1" locked="0" date_sent="1770681929000" readable_date="2026-02-10 00:05:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770669953000" type="2" body="I'll do that right now" read="1" status="-1" locked="0" date_sent="1770669953000" readable_date="2026-02-09 20:45:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlLUlkySzAtU3otTXlKb0lTSGhnOVEqED" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12065320025" date="1770669936000" type="1" body="Good idea. Now, you need to ask your attorney to call me tomorrow.." read="1" status="-1" locked="0" date_sent="1770669936000" readable_date="2026-02-09 20:45:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ2NjlFNjU1NS00MUY4LTQ3MzEtQjAwOS0yRE" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770669910000" type="2" body="That would also make sure you and Ashley connect" read="1" status="-1" locked="0" date_sent="1770669910000" readable_date="2026-02-09 20:45:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlqaXU0cUdLUkRHTDVDZlNsWWN5LVEqEP" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12065320025" date="1770669873000" type="1" body="Ppl" read="1" status="-1" locked="0" date_sent="1770669873000" readable_date="2026-02-09 20:44:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRFMjkyRDhDMS1GRDI4LTQ5M0QtODVFMi05OD" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770669871000" type="2" body="We can do that. Let's do next Tuesday so we don't have guess and rush this." read="1" status="-1" locked="0" date_sent="1770669871000" readable_date="2026-02-09 20:44:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFZVMFZZMjFyUVVHd1FuRVVjZDQyamcqEF" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12065320025" date="1770669842000" type="1" body="$750." read="1" status="-1" locked="0" date_sent="1770669842000" readable_date="2026-02-09 20:44:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ2NzU5MzgxRi01QThELTQyQUEtQTFCNy00M0" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770669814000" type="2" body="We can plan for tomorrow, and if some reason my work won't let me out, I can let you know this evening. How much cash should I bring?" read="1" status="-1" locked="0" date_sent="1770669814000" readable_date="2026-02-09 20:43:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHY3S25RcEJCUmNxZ29rPXRZWmZqcUEqEK" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12066990041" date="1770669554000" type="2" body="I would know by about 6pm, I would have to talk with my manager and then get my sr manager to OK me missing time" read="1" status="-1" locked="0" date_sent="1770669554000" readable_date="2026-02-09 20:39:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHI0MVl0MGwyU2RlY0J3QU1WWmE9dGcqEP" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12066990041" date="1770669477000" type="2" body="How long are the appointments? I was hoping to schedule something for a weekend, during the week is pretty difficult for me unless its early in the morning or if I a have a few more days notice. I can try and make tomorrow work, but it will depend on whether I can get the day off approved." read="1" status="-1" locked="0" date_sent="1770669477000" readable_date="2026-02-09 20:37:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDNEUGI3SlpMUkV5NGlpZWZsZTNITXcqEB" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+12065320025" date="1770669287000" type="1" body="Do you want to schedule an appt for tomorrow at 12:30pm?" read="1" status="-1" locked="0" date_sent="1770669287000" readable_date="2026-02-09 20:34:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQyNkFBNDM0RC0xNzk4LTREREItQjUxRC00RD" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770669161000" type="2" body="Hey Terry, Its Matthew Davison, My Attorney gave you call last Weds. and we are anxious to get a polygraph scheduled. My attorney Ashley Repp can be reached at 913-308-4716. Thanks, we look forward to hearing from you" read="1" status="-1" locked="0" date_sent="1770669161000" readable_date="2026-02-09 20:32:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGc2WGxRNmxSUzhpUnBmcDFCTjRrZkEqEK" group_addresses="+12066990041,+12065320025" />
<sms protocol="0" address="+14259234274" date="1770608368000" type="1" body="Amen bro!" read="1" status="-1" locked="0" date_sent="1770608368000" readable_date="2026-02-09 03:39:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066057095" date="1770608045000" type="1" body="They earned it for sure!" read="1" status="-1" locked="0" date_sent="1770608045000" readable_date="2026-02-09 03:34:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770607987000" type="2" body="Good game, we deserved that win." read="1" status="-1" locked="0" date_sent="1770607987000" readable_date="2026-02-09 03:33:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770607833000" type="2" body="We deserved it. Fuck New England. Last time we played them, our win was stolen." read="1" status="-1" locked="0" date_sent="1770607833000" readable_date="2026-02-09 03:30:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770607831000" type="1" body="Tastes so good" read="1" status="-1" locked="0" date_sent="1770607831000" readable_date="2026-02-09 03:30:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770607617000" type="1" body="Good game" read="1" status="-1" locked="0" date_sent="1770607617000" readable_date="2026-02-09 03:26:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770607599000" type="2" body="Fuck yeah" read="1" status="-1" locked="0" date_sent="1770607599000" readable_date="2026-02-09 03:26:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770597510000" type="1" body="Amen brother!" read="1" status="-1" locked="0" date_sent="1770597510000" readable_date="2026-02-09 00:38:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770597505000" type="1" body="" read="1" status="-1" locked="0" date_sent="1770597505000" readable_date="2026-02-09 00:38:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770597464000" type="2" body="Fuck yeah. We gonna own this shit. Its our time this year" read="1" status="-1" locked="0" date_sent="1770597464000" readable_date="2026-02-09 00:37:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770597432000" type="1" body="Nice buddy! Let’s go hawks!" read="1" status="-1" locked="0" date_sent="1770597432000" readable_date="2026-02-09 00:37:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770597410000" type="1" body="" read="1" status="-1" locked="0" date_sent="1770597410000" readable_date="2026-02-09 00:36:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770597378000" type="2" body="I'm just hanging out with my partner, watching the game" read="1" status="-1" locked="0" date_sent="1770597378000" readable_date="2026-02-09 00:36:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770592805000" type="1" body="Good stuff I’m glad your with fam! Let’s go HAWKS!" read="1" status="-1" locked="0" date_sent="1770592805000" readable_date="2026-02-08 23:20:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770592006000" type="1" body="Im watching the Super Bowl at my cousins house" read="1" status="-1" locked="0" date_sent="1770592006000" readable_date="2026-02-08 23:06:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770591799000" type="1" body="Hope you guys been good since that happened, I’m praying for both of you all the time." read="1" status="-1" locked="0" date_sent="1770591799000" readable_date="2026-02-08 23:03:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770591658000" type="2" body="Wouldn't surprise me. He was there for a long time" read="1" status="-1" locked="0" date_sent="1770591658000" readable_date="2026-02-08 23:00:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770591625000" type="1" body="He thinks he knows him" read="1" status="-1" locked="0" date_sent="1770591625000" readable_date="2026-02-08 23:00:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770591514000" type="1" body="What was Len's last name?" read="1" status="-1" locked="0" date_sent="1770591514000" readable_date="2026-02-08 22:58:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="7178076886" date="1770513273000" type="1" body="Your order was dropped off. Please refer to this photo your Dasher provided to see where it was left." read="1" status="-1" locked="0" date_sent="1770513273000" readable_date="2026-02-08 01:14:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="42" rcs_tr_id="proto:CkQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SHhocChowOTcwNzkzMzE3N0MwMDAwRjg4MDAyMDEwMQ" group_addresses="7178076886,2066990041" />
<sms protocol="0" address="+12066057095" date="1770495685000" type="1" body="Good. You had me worried" read="1" status="-1" locked="0" date_sent="1770495685000" readable_date="2026-02-07 20:21:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770495648000" type="2" body="It connected, I must have just fat fingered something" read="1" status="-1" locked="0" date_sent="1770495648000" readable_date="2026-02-07 20:20:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1770495574000" type="1" body="It is gorbash342." read="1" status="-1" locked="0" date_sent="1770495574000" readable_date="2026-02-07 20:19:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770495498000" type="2" body="The guy is running a few minutes late due to traffic, no not a huge rush" read="1" status="-1" locked="0" date_sent="1770495498000" readable_date="2026-02-07 20:18:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770495394000" type="2" body="OK, no problem" read="1" status="-1" locked="0" date_sent="1770495394000" readable_date="2026-02-07 20:16:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1770495363000" type="1" body="Dad will answer in a sec" read="1" status="-1" locked="0" date_sent="1770495363000" readable_date="2026-02-07 20:16:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770495284000" type="2" body="What the WiFi password up here, I can't remember. I thought it was gorbash342. I just need to print something" read="1" status="-1" locked="0" date_sent="1770495284000" readable_date="2026-02-07 20:14:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770493544000" type="2" body="Send me a text when you arrive and I will come let you in the building" read="1" status="-1" locked="0" date_sent="1770493544000" readable_date="2026-02-07 19:45:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="41" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHZxSkt2eEV2UnpPT3Z0Q1BSbD1idWcqEG" group_addresses="+12066990041,+12063350631" />
<sms protocol="0" address="+12066057095" date="1770490321000" type="1" body="Great" read="1" status="-1" locked="0" date_sent="1770490321000" readable_date="2026-02-07 18:52:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770489725000" type="2" body="Its all good" read="1" status="-1" locked="0" date_sent="1770489725000" readable_date="2026-02-07 18:42:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGVVbmdyQUV2VDlXdml2WTFaSVZuS0EqEM" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+12066990041" date="1770489712000" type="2" body="Just getting ready to go" read="1" status="-1" locked="0" date_sent="1770489712000" readable_date="2026-02-07 18:41:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770489693000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1770489693000" readable_date="2026-02-07 18:41:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+14253193719" date="1770487454000" type="1" body="Sorry. I slept all frickin day yesterday" read="1" status="-1" locked="0" date_sent="1770487454000" readable_date="2026-02-07 18:04:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDNYcEpSWU1yUkVXaGMwdW5SQjZuM3cqEK" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066057095" date="1770479273000" type="1" body="All cameras are deactivated" read="1" status="-1" locked="0" date_sent="1770479273000" readable_date="2026-02-07 15:47:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066125383" date="1770478990000" type="1" body="Apts open" read="1" status="-1" locked="0" date_sent="1770478990000" readable_date="2026-02-07 15:43:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066571038" date="1770450933000" type="1" body="Oh man alright sounds like you can use some extra prayers" read="1" status="-1" locked="0" date_sent="1770450933000" readable_date="2026-02-07 07:55:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE5kNVV2dmpoU1dxa2JHajhDUEZlZncqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770450884000" type="2" body="I can't tonight. I texted Ray hoping he would cover. But I have a super early court thing tomorrow I have to do and today we had a guy drop dead. While we weren't really friend-friends, just work friends, I still can't get the image out of my head of seeing him down" read="1" status="-1" locked="0" date_sent="1770450884000" readable_date="2026-02-07 07:54:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDVJVXNKTXhLU09HWnZmT1FVRTQ3PWcqEB" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1770450629000" type="1" body="You coming to chair the meeting" read="1" status="-1" locked="0" date_sent="1770450629000" readable_date="2026-02-07 07:50:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEVtRzhwY0gwUXlDcUNlcjA1TXBSVkEqEL" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770445784000" type="2" body="Hey, if your at the hall can you run my midnight tonight? I got an appointment in the early am tomorrow and after the day today I just wanna crash out" read="1" status="-1" locked="0" date_sent="1770445784000" readable_date="2026-02-07 06:29:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFVQTWUyZDNrVGRDPW00VVo5WkhnZEEqEA" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+19168990823" date="1770434559000" type="1" body="Yeah" read="1" status="-1" locked="0" date_sent="1770434559000" readable_date="2026-02-07 03:22:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770433935000" type="2" body="Yeah, death sucks that way. Is anyone actually working on fwd bodies" read="1" status="-1" locked="0" date_sent="1770433935000" readable_date="2026-02-07 03:12:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770433861000" type="1" body="Its pretty somber here" read="1" status="-1" locked="0" date_sent="1770433861000" readable_date="2026-02-07 03:11:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770433466000" type="2" body="He wasn't like a friend-friend, more of a work friend. Thanks though." read="1" status="-1" locked="0" date_sent="1770433466000" readable_date="2026-02-07 03:04:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770432533000" type="1" body="What a great man, sorry you lost a friend buddy. I’m here if you need anything." read="1" status="-1" locked="0" date_sent="1770432533000" readable_date="2026-02-07 02:48:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770431884000" type="2" body="He was also in the middle of renovation at his home to have things he always wanted installed. Before joining Boeing, he served in the US Navy working Machine shop, was in operation desert storm and began his career at Boeing in 1990's on 747. He had a great sense of humor, even if it was a bit maudlin at times. And even though he could be grouchy, when you needed help, he was always there to lend a hand." read="1" status="-1" locked="0" date_sent="1770431884000" readable_date="2026-02-07 02:38:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770430154000" type="1" body="Len, been with the company for a long time. Had a wife and 2 daughters. He was gonna retire at the end of the year too" read="1" status="-1" locked="0" date_sent="1770430154000" readable_date="2026-02-07 02:09:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770429165000" type="1" body="That’s so dangerous what the heck" read="1" status="-1" locked="0" date_sent="1770429165000" readable_date="2026-02-07 01:52:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770428168000" type="2" body="We told ED our union steward that we are gonna &quot;pull the pol-19 red card&quot; and stop production if they don't take our concerns serious over safety equipment" read="1" status="-1" locked="0" date_sent="1770428168000" readable_date="2026-02-07 01:36:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770428087000" type="2" body="Like with how no one could find the emergency number for Arnie yesterday, we couldn't find a single fucking AED. They took them out of the shop." read="1" status="-1" locked="0" date_sent="1770428087000" readable_date="2026-02-07 01:34:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770428058000" type="1" body="Gosh dang man im praying for his family and you guys. Sorry that happened matt." read="1" status="-1" locked="0" date_sent="1770428058000" readable_date="2026-02-07 01:34:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770428001000" type="1" body="Fuck" read="1" status="-1" locked="0" date_sent="1770428001000" readable_date="2026-02-07 01:33:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770427982000" type="2" body="Yeah. He was a mess" read="1" status="-1" locked="0" date_sent="1770427982000" readable_date="2026-02-07 01:33:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770427971000" type="1" body="I feel bad for Taylor" read="1" status="-1" locked="0" date_sent="1770427971000" readable_date="2026-02-07 01:32:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427954000" type="1" body="All the Pre auto guys left our building" read="1" status="-1" locked="0" date_sent="1770427954000" readable_date="2026-02-07 01:32:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770427948000" type="2" body="Our floor coordinator. And, he dropped, heart attack probably, and as he was collapsing hit his head." read="1" status="-1" locked="0" date_sent="1770427948000" readable_date="2026-02-07 01:32:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770427910000" type="1" body="Holy crap no way" read="1" status="-1" locked="0" date_sent="1770427910000" readable_date="2026-02-07 01:31:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427908000" type="1" body="Heard*" read="1" status="-1" locked="0" date_sent="1770427908000" readable_date="2026-02-07 01:31:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427902000" type="1" body="I hear he fell" read="1" status="-1" locked="0" date_sent="1770427902000" readable_date="2026-02-07 01:31:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427892000" type="1" body="Yeah" read="1" status="-1" locked="0" date_sent="1770427892000" readable_date="2026-02-07 01:31:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770427887000" type="1" body="Someone die????" read="1" status="-1" locked="0" date_sent="1770427887000" readable_date="2026-02-07 01:31:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770427881000" type="2" body="I'm taking the rest of the day off" read="1" status="-1" locked="0" date_sent="1770427881000" readable_date="2026-02-07 01:31:21 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770427861000" type="2" body="It sucks. Len was pretty cool guy. I feel bad for Taylor, he did the CPR until the medics arrived" read="1" status="-1" locked="0" date_sent="1770427861000" readable_date="2026-02-07 01:31:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770427818000" type="1" body="Everything all good?" read="1" status="-1" locked="0" date_sent="1770427818000" readable_date="2026-02-07 01:30:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427806000" type="1" body="I heard what happened" read="1" status="-1" locked="0" date_sent="1770427806000" readable_date="2026-02-07 01:30:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770427800000" type="1" body="How you doing Matt?" read="1" status="-1" locked="0" date_sent="1770427800000" readable_date="2026-02-07 01:30:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770424463000" type="2" body="He walked up to the barge and just collapsed right where AJ sits. Taylor did CPR until medics arrived. We ran around looking for an AED, and they got ride of all the ones that were on the shop floor" read="1" status="-1" locked="0" date_sent="1770424463000" readable_date="2026-02-07 00:34:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGlRYXZOT3VMU3pTME9RaDVDaTVQUmcqEN" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14253193719" date="1770424351000" type="1" body="Holy fuck. No I did not hear. He did tell me that he was going through something bad." read="1" status="-1" locked="0" date_sent="1770424351000" readable_date="2026-02-07 00:32:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE5PSkYxV1RLU2tPeEJNVzdNZUtSQVEqEE" group_addresses="+14253193719,+14253193719,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770424260000" type="2" body="I dunno if anyone texted you, and I didn't see you at work, Len died at the crew meeting today. They did CPR for an hour and couldnt bring him back" read="1" status="-1" locked="0" date_sent="1770424260000" readable_date="2026-02-07 00:31:00 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFlPM0t2dnMxUWlpNU10VlRpeUtjeFEqEB" group_addresses="+12066990041,+14253193719" />
<sms protocol="0" address="+14254052681" date="1770423577000" type="1" body="Which spec was it that said you could glue two shims together" read="1" status="-1" locked="0" date_sent="1770423577000" readable_date="2026-02-07 00:19:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRFMEM4MTJCQS04MDc5LTRGMTEtODVFMC00Qk" group_addresses="+14254052681,+14254052681,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770362618000" type="2" body="Crap, helping nick get his kit I forgot to update my stamp on the wavey cro" read="1" status="-1" locked="0" date_sent="1770362618000" readable_date="2026-02-06 07:23:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDdmZmJwZ2VtUlhhb0dxVWVDOGpsN0EqEL" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+14259234274" date="1770332402000" type="1" body="Yeah my retirement is gone because of my kids so it kinda evens out 😂" read="1" status="-1" locked="0" date_sent="1770332402000" readable_date="2026-02-05 23:00:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770332401000" type="1" body="" read="1" status="-1" locked="0" date_sent="1770332401000" readable_date="2026-02-05 23:00:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770331678000" type="2" body="Wish I didn't have to sell a quarter of my retirement to pay all these legal fees and shit." read="1" status="-1" locked="0" date_sent="1770331678000" readable_date="2026-02-05 22:47:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770331597000" type="2" body="Kids*" read="1" status="-1" locked="0" date_sent="1770331597000" readable_date="2026-02-05 22:46:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770331593000" type="2" body="That's cuz you got kida" read="1" status="-1" locked="0" date_sent="1770331593000" readable_date="2026-02-05 22:46:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770330307000" type="1" body="Heck yeah bro I made 80k with 4k in OT! I’m getting 9 grand back lol" read="1" status="-1" locked="0" date_sent="1770330307000" readable_date="2026-02-05 22:25:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770330303000" type="1" body="Damn" read="1" status="-1" locked="0" date_sent="1770330303000" readable_date="2026-02-05 22:25:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770330256000" type="2" body="I had a taxable income of 218k last year, taxed at 26â„…, I only paid 22% at time of sale" read="1" status="-1" locked="0" date_sent="1770330256000" readable_date="2026-02-05 22:24:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770329774000" type="2" body="I'm being killed by capital gains taxes, the stocks I sold had a cost base of near 0 so I'm being taxed up the pooper" read="1" status="-1" locked="0" date_sent="1770329774000" readable_date="2026-02-05 22:16:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770329600000" type="2" body="Very nice" read="1" status="-1" locked="0" date_sent="1770329600000" readable_date="2026-02-05 22:13:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1770329586000" type="1" body="I made $87k last year with $6K in overtime" read="1" status="-1" locked="0" date_sent="1770329586000" readable_date="2026-02-05 22:13:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770319213000" type="1" body="Ahh yeah that makes sense, sorry bro" read="1" status="-1" locked="0" date_sent="1770319213000" readable_date="2026-02-05 19:20:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770319155000" type="2" body="I'm gonna end up oweing this year so imma do mine towards April and file for an extension. My taxes are gonna suck balls this year" read="1" status="-1" locked="0" date_sent="1770319155000" readable_date="2026-02-05 19:19:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+14259234274" date="1770319068000" type="1" body="Good boy" read="1" status="-1" locked="0" date_sent="1770319068000" readable_date="2026-02-05 19:17:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770313677000" type="1" body="Im doing mine this weekend" read="1" status="-1" locked="0" date_sent="1770313677000" readable_date="2026-02-05 17:47:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770311550000" type="1" body="lol just looking out for my boys" read="1" status="-1" locked="0" date_sent="1770311550000" readable_date="2026-02-05 17:12:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770311536000" type="1" body="What are you, the Fed's? Pay taxes, pay registration" read="1" status="-1" locked="0" date_sent="1770311536000" readable_date="2026-02-05 17:12:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770296131000" type="1" body="Don’t forget to do your taxes pussies" read="1" status="-1" locked="0" date_sent="1770296131000" readable_date="2026-02-05 12:55:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770270336000" type="1" body="Go for you giys" read="1" status="-1" locked="0" date_sent="1770270336000" readable_date="2026-02-05 05:45:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770259124000" type="2" body="I don't either" read="1" status="-1" locked="0" date_sent="1770259124000" readable_date="2026-02-05 02:38:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1770259107000" type="2" body="Ok" read="1" status="-1" locked="0" date_sent="1770259107000" readable_date="2026-02-05 02:38:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+19168990823" date="1770257787000" type="1" body="That suck" read="1" status="-1" locked="0" date_sent="1770257787000" readable_date="2026-02-05 02:16:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1770257775000" type="1" body="I don’t have IG anymore so I can’t watch that lol" read="1" status="-1" locked="0" date_sent="1770257775000" readable_date="2026-02-05 02:16:15 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+14259234274,+12066990041" />
<sms protocol="0" address="+19168990823" date="1770257740000" type="1" body="https://www.instagram.com/reel/DTbe3jbkcyS/?igsh=MTF1aXpmMW9oMjc1ag==" read="1" status="-1" locked="0" date_sent="1770257740000" readable_date="2026-02-05 02:15:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066125383" date="1770246671000" type="1" body="Eric will open before then" read="1" status="-1" locked="0" date_sent="1770246671000" readable_date="2026-02-04 23:11:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066125383" date="1770241425000" type="1" body="K" read="1" status="-1" locked="0" date_sent="1770241425000" readable_date="2026-02-04 21:43:45 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770241306000" type="2" body="I will need access to your apartment at noon on Saturday" read="1" status="-1" locked="0" date_sent="1770241306000" readable_date="2026-02-04 21:41:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1770177459000" type="2" body="You can also use two solid shims, glue them together with bms 5-36 per bac" read="1" status="-1" locked="0" date_sent="1770177459000" readable_date="2026-02-04 03:57:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHVYSG5ubHVIU05lbUw5c0NFR1BSM1EqEO" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+12066990041" date="1770177186000" type="2" body="The Bac1534-62F laminated shim is allowed for 487-634 per 141w0120#DL and sh 86. So laminated shims are fine" read="1" status="-1" locked="0" date_sent="1770177186000" readable_date="2026-02-04 03:53:06 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDJBazdoVW5ZUVNhTjRLZWtuSmpVS0EqEN" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+12066990041" date="1770165253000" type="2" body="After hooking up those stands I realized I forgot some of my stuff in the drawer, and the fasteners Kevin asked me to grab, his ME8's and the collars. Anything you need before I head back down? Anything from the MIC? Fasteners from the drawer, countersinks? Let me know before break ends and I'll grab them" read="1" status="-1" locked="0" date_sent="1770165253000" readable_date="2026-02-04 00:34:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="38" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHpJRjViSXg2US1XU1BWREthakNwbncqEK" group_addresses="+12066990041,+14258798117" />
<sms protocol="0" address="+12066990041" date="1770165067000" type="2" body="Cool thank you sir" read="1" status="-1" locked="0" date_sent="1770165067000" readable_date="2026-02-04 00:31:07 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGRDY1l1d0lGU0F5eGVyVy1FRVFFRVEqEI" group_addresses="+12066990041,+14254052681" />
<sms protocol="0" address="+14254052681" date="1770165051000" type="1" body="Aaron’s number +1 (425) 879-8117" read="1" status="-1" locked="0" date_sent="1770165051000" readable_date="2026-02-04 00:30:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQyMjE2NEI0Ri1GODU1LTQ4NTQtODk2NC1FQ0" group_addresses="+14254052681,+14254052681,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770164604000" type="2" body="Some reason Aaron and nicks numbers didn't transfer to my new phone, can you let them know I came back to the 27 to grab the fasteners I ordered and Kevin's Fasteners (The ME8's and the collars). Can you ask them if they need anything from the drawer while I'm down here" read="1" status="-1" locked="0" date_sent="1770164604000" readable_date="2026-02-04 00:23:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDlkSlBKZWl0UWtTQlMxUW43YkUyM2cqEK" group_addresses="+12066990041,+14254052681" />
<sms protocol="0" address="+12066125383" date="1770063433000" type="1" body="K" read="1" status="-1" locked="0" date_sent="1770063433000" readable_date="2026-02-02 20:17:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066990041" date="1770063409000" type="2" body="Yeah, I call in a few, just finishing getting ready. Its just a small strategy change" read="1" status="-1" locked="0" date_sent="1770063409000" readable_date="2026-02-02 20:16:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066125383" date="1770063332000" type="1" body="Shani's gone. Do you still need to talk?" read="1" status="-1" locked="0" date_sent="1770063332000" readable_date="2026-02-02 20:15:32 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+12066057095" date="1769996410000" type="1" body="The funding is there. You can fill us in in the morning. We don't want o slow the process." read="1" status="-1" locked="0" date_sent="1769996410000" readable_date="2026-02-02 01:40:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769996285000" type="2" body="I'll call around 9 tomorrow to go over everything." read="1" status="-1" locked="0" date_sent="1769996285000" readable_date="2026-02-02 01:38:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1769996157000" type="2" body="Yes, but the prosecutor has put us in a bit of a time crunch, basically instead of doing one polygraph we want to do 2 with different providers. We have 2 who have availability this and next week, and she wants to call them tomorrow to get things moving. Just want to make sure the funding is there for two. Roughly about $700 each, will have exact costs after she talks to them" read="1" status="-1" locked="0" date_sent="1769996157000" readable_date="2026-02-02 01:35:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1769995476000" type="1" body="We will. We're just going to dinner with Shani. Can we talk in the morning? If so just call in the morning when you get up." read="1" status="-1" locked="0" date_sent="1769995476000" readable_date="2026-02-02 01:24:36 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769995262000" type="2" body="I talked with Ashley a bit more, nothing has really changed except strategy slightly. When you have some time give me a call and we can go over what we are thinking." read="1" status="-1" locked="0" date_sent="1769995262000" readable_date="2026-02-02 01:21:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066990041" date="1769972551000" type="2" body="Thanks" read="1" status="-1" locked="0" date_sent="1769972551000" readable_date="2026-02-01 19:02:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+12066057095" date="1769972007000" type="1" body="Ill send it now" read="1" status="-1" locked="0" date_sent="1769972007000" readable_date="2026-02-01 18:53:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066057095,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769969860000" type="2" body="Can dad $125? I was off just a little bit on what I would need. I still shouldn't need the $900" read="1" status="-1" locked="0" date_sent="1769969860000" readable_date="2026-02-01 18:17:40 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066990041,+12066057095,+12066125383" />
<sms protocol="0" address="+19168990823" date="1769922203000" type="1" body="No" read="1" status="-1" locked="0" date_sent="1769922203000" readable_date="2026-02-01 05:03:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+19168990823,+14255353504,+14259234274,+14253145582,+19168990823,+12066990041" />
<sms protocol="0" address="+14259234274" date="1769922196000" type="1" body="Worst state in the country" read="1" status="-1" locked="0" date_sent="1769922196000" readable_date="2026-02-01 05:03:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+19168990823,+14255353504,+14259234274,+14253145582,+14259234274,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769922159000" type="2" body="Jake...you better get your tabs if you haven't. I think you said you did but I can't remember https://mynorthwest.com/kiro-opinion/harger-wa-expired-car-tab/4195970" read="1" status="-1" locked="0" date_sent="1769922159000" readable_date="2026-02-01 05:02:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+18165072664" date="1769912452000" type="1" body="no problem… just let me know!" read="1" status="-1" locked="0" date_sent="1769912452000" readable_date="2026-02-01 02:20:52 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRBQTQyQjM3OC0wN0RCLTQ1ODItQTNEMy1CNU" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769912254000" type="2" body="My attorney and I have a few things to review and discuss next week, for now we are putting the assessment on hold. I apologize for jumping the gun" read="1" status="-1" locked="0" date_sent="1769912254000" readable_date="2026-02-01 02:17:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGZacTc5NU5xUlBDdjB5SWxsVUc4MmcqEI" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066990041" date="1769911831000" type="2" body="On my way" read="1" status="-1" locked="0" date_sent="1769911831000" readable_date="2026-02-01 02:10:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="35" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGlxV3FuajdVUjBDRmt5azhaZWVmZEEqEE" group_addresses="+12066990041,+14253085560" />
<sms protocol="0" address="+14253085560" date="1769909937000" type="1" body="And now you have some washers here as well" read="1" status="-1" locked="0" date_sent="1769909937000" readable_date="2026-02-01 01:38:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="35" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQyMEY4QjgzOC0yOUZELTQ5NTAtOEQ1QS1GQ0" group_addresses="+14253085560,+14253085560,+12066990041" />
<sms protocol="0" address="+14253085560" date="1769904637000" type="1" body="Your rivets just arrived. Where you hiding at today?" read="1" status="-1" locked="0" date_sent="1769904637000" readable_date="2026-02-01 00:10:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="35" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRGRDk1QTc3MS1ERUY1LTQzNzAtQjc5OS1DOE" group_addresses="+14253085560,+14253085560,+12066990041" />
<sms protocol="0" address="+18165072664" date="1769898790000" type="1" body="Just let me know and we’ll get an appt ASAP" read="1" status="-1" locked="0" date_sent="1769898790000" readable_date="2026-01-31 22:33:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ1QTg1OUY0Ny04M0RFLTQ4MEYtQkU4MS1EQ0" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+18165072664" date="1769897125000" type="1" body="hi matt what is agood time for you?" read="1" status="-1" locked="0" date_sent="1769897125000" readable_date="2026-01-31 22:05:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRBNTMwNUFGMi1BNzhCLTRGQ0ItQjAyMy1BN0" group_addresses="+18165072664,+18165072664,+12066990041" />
<sms protocol="0" address="+14254052681" date="1769844124000" type="1" body="Maybe Aron can relay the message" read="1" status="-1" locked="0" date_sent="1769844124000" readable_date="2026-01-31 07:22:04 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRGNjFEQTUxNC02RkY1LTQyNDktQTZGNS1DRU" group_addresses="+14254052681,+14254052681,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769844090000" type="2" body="I don't have his number. Just team lead nick. Oh well." read="1" status="-1" locked="0" date_sent="1769844090000" readable_date="2026-01-31 07:21:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeE81bzZ2OHN3Ujg2TkJaakFDYWFjTmcqEP" group_addresses="+12066990041,+14254052681" />
<sms protocol="0" address="+14254052681" date="1769844044000" type="1" body="Or if you just have Nick's numbers" read="1" status="-1" locked="0" date_sent="1769844044000" readable_date="2026-01-31 07:20:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ1RDBGNjMyNC01MjFELTQzNkEtOEQ5RS01Mj" group_addresses="+14254052681,+14254052681,+12066990041" />
<sms protocol="0" address="+14254052681" date="1769844028000" type="1" body="Tristan might still be there he was finishing up putting his things away when I was heading out" read="1" status="-1" locked="0" date_sent="1769844028000" readable_date="2026-01-31 07:20:28 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ1MUIxQjg3Qy1FMjAxLTQzMTMtOTA2Qi0yMj" group_addresses="+14254052681,+14254052681,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769843969000" type="2" body="I was gonna ask you to tell him to double check them. Oh well" read="1" status="-1" locked="0" date_sent="1769843969000" readable_date="2026-01-31 07:19:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="36" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHptRnA5WkhWU29lSVE9LThTYzVYZ0EqEG" group_addresses="+12066990041,+14254052681" />
<sms protocol="0" address="+12066990041" date="1769838029000" type="2" body="They should be delivering some YK 2nd overs shortly, I'm at EO getting some freeze plugs, if you could drop those off at his desk when they show up also that would be awesome. If you get caught up in something, just let me know and I'll swing by when I get back" read="1" status="-1" locked="0" date_sent="1769838029000" readable_date="2026-01-31 05:40:29 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="35" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGYwcnNwMHc1VEVTZlZuaVBZN3p2UFEqEJ" group_addresses="+12066990041,+14253085560" />
<sms protocol="0" address="+12066990041" date="1769837867000" type="2" body="OK thank you" read="1" status="-1" locked="0" date_sent="1769837867000" readable_date="2026-01-31 05:37:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="35" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFRzZkJzTUxVUTU2MXQ5anZNU1pmb3cqEH" group_addresses="+12066990041,+14253085560" />
<sms protocol="0" address="+19168990823" date="1769819952000" type="1" body="This place needs a reality check" read="1" status="-1" locked="0" date_sent="1769819952000" readable_date="2026-01-31 00:39:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+19168990823,+14255353504,+14259234274,+14253145582,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769819930000" type="2" body="Time to call the BR" read="1" status="-1" locked="0" date_sent="1769819930000" readable_date="2026-01-31 00:38:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1769819891000" type="2" body="Good luck. IG Tyler cloud is stopping transfers, Orion told one of our guys that they had it all set up, but then Tyler said nope at the last minute" read="1" status="-1" locked="0" date_sent="1769819891000" readable_date="2026-01-31 00:38:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1769819808000" type="1" body="I reminded my manager that I was serious about my transfer" read="1" status="-1" locked="0" date_sent="1769819808000" readable_date="2026-01-31 00:36:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+19168990823,+14255353504,+14259234274,+14253145582,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769819771000" type="2" body="They were in the FAC at a both the last couple of days" read="1" status="-1" locked="0" date_sent="1769819771000" readable_date="2026-01-31 00:36:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1769819742000" type="1" body="This place is a joke, HR stopped responding to my emails" read="1" status="-1" locked="0" date_sent="1769819742000" readable_date="2026-01-31 00:35:42 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+19168990823,+14255353504,+14259234274,+14253145582,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769819711000" type="2" body="I feel you Jake. The frustration is real" read="1" status="-1" locked="0" date_sent="1769819711000" readable_date="2026-01-31 00:35:11 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1769819670000" type="2" body="I'm getting sick of this bs. Them not assigning me any work, saying they can't find anything for me to do, then on the 28th I had to go redo my seal recert because it mysteriously went away last week, then after I did it, I get an email at 11:05 saying I have been decertified again. I asked my manager why he decerited me, he said he did it earlier in the day. Why the fuck is he decerting me in the first place in a cert we use everyday" read="1" status="-1" locked="0" date_sent="1769819670000" readable_date="2026-01-31 00:34:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" group_addresses="+12066990041,+14253145582,+14255353504,+14259234274,+19168990823" />
<sms protocol="0" address="+12065320025" date="1769809774000" type="1" body="Terry Ball here. Returning you text msg earlier today. Please call me at 206-532-0025." read="1" status="-1" locked="0" date_sent="1769809774000" readable_date="2026-01-30 21:49:34 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="34" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiRGMTBCMkVFOS1DNkZGLTRCQzgtQUE2Ri1GNj" group_addresses="+12065320025,+12065320025,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769798557000" type="2" body="Hi Mike, my name is Matthew Davison, and my Attorney Ashley Repp told me to give you a text to schedule a evaluation" read="1" status="-1" locked="0" date_sent="1769798557000" readable_date="2026-01-30 18:42:37 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="33" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeHpGNjd5WVlCUTNLZ0FVUm00cVJDdEEqEM" group_addresses="+12066990041,+18165072664" />
<sms protocol="0" address="+12066571038" date="1769757054000" type="1" body="Doing alright just got out of the 10" read="1" status="-1" locked="0" date_sent="1769757054000" readable_date="2026-01-30 07:10:54 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeG9qdDZCcU5XUTEtaE1NSURmcGJSd3cqEB" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769756650000" type="2" body="Just tired AF from work. How about you?" read="1" status="-1" locked="0" date_sent="1769756650000" readable_date="2026-01-30 07:04:10 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGUtSkJFRTFyUzFTVmdxdXdRNFc4Q3cqEJ" group_addresses="+12066990041,+12066571038" />
<sms protocol="0" address="+12066571038" date="1769750155000" type="1" body="Hey man what's up just checking in with you" read="1" status="-1" locked="0" date_sent="1769750155000" readable_date="2026-01-30 05:15:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="7" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDQ1TEJ1OTdlUU02TGtiSHRQc3JqWGcqEK" group_addresses="+12066571038,+12066571038,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769740342000" type="2" body="Sick" read="1" status="-1" locked="0" date_sent="1769740342000" readable_date="2026-01-30 02:32:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeEZmSVJOa1lEUm9peEZmaGlGTTVTNGcqEL" group_addresses="+12066990041,+14253277371" />
<sms protocol="0" address="+14253277371" date="1769739951000" type="1" body="https://www.gateworld.net/news/2026/01/stargate-film-new-series-london-this-year/" read="1" status="-1" locked="0" date_sent="1769739951000" readable_date="2026-01-30 02:25:51 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="28" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeDV0cThTQ0ZSUTZHQkI0N1FOY096d0EqEG" group_addresses="+14253277371,+14253277371,+12066990041" />
<sms protocol="0" address="+14055515649" date="1769694858000" type="1" body="Thank you sir. I will call you at 11:45am PST." read="1" status="-1" locked="0" date_sent="1769694858000" readable_date="2026-01-29 13:54:18 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="24" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQzMURGNDZCRS1DMzM0LTRBREItQkIzMi1DQT" group_addresses="+14055515649,+14055515649,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769631417000" type="2" body="I can make myself available at 11:45am tomorrow if that works for you" read="1" status="-1" locked="0" date_sent="1769631417000" readable_date="2026-01-28 20:16:57 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="24" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeGJDcVBzU0xsVDBLeGJoTm52T1NZbFEqEM" group_addresses="+12066990041,+14055515649" />
<sms protocol="0" address="+14055515649" date="1769620754000" type="1" body="Mr Davison, sir my name is Mickey Banks, I am with the FAA. I am trying to contact you regarding the hotline report you filed. I would like to set up an interview with you tomorrow January 29, 2026, between 10am and 12 pm PST. Please give me a call or respond to this text if that works for you. Thank you." read="1" status="-1" locked="0" date_sent="1769620754000" readable_date="2026-01-28 17:19:14 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="24" rcs_tr_id="proto:CmAKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SOhImKiQ5MDI2OEZERC1CNEVELTQxNjAtQkU2RC0wOD" group_addresses="+14055515649,+14055515649,+12066990041" />
<sms protocol="0" address="+12066125383" date="1769586038000" type="1" body="Just going to sleep. Will watch in morning. Night" read="1" status="-1" locked="0" date_sent="1769586038000" readable_date="2026-01-28 07:40:38 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" group_addresses="+12066057095,+12066125383,+12066125383,+12066990041" />
<sms protocol="0" address="+19168990823" date="1769581735000" type="1" body="There's nothing they can do. The parts do get here in time and the come loaded with NCR's and its not perfect they dont buy it" read="1" status="-1" locked="0" date_sent="1769581735000" readable_date="2026-01-28 06:28:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769581724000" type="2" body="I screwed up one frame splice since I've been back last October, and they won't let me work on the plane. So fuck em" read="1" status="-1" locked="0" date_sent="1769581724000" readable_date="2026-01-28 06:28:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+12066990041" date="1769581661000" type="2" body="I know what your saying, but if your managers and leads won't help, there's really nothing else you can do except document your work and say fuck it. Get what pay you can and go home at the end if the day. Trust me, I'm pretty &quot;done&quot; also with the BS they are pulling with me" read="1" status="-1" locked="0" date_sent="1769581661000" readable_date="2026-01-28 06:27:41 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="+19168990823" date="1769581452000" type="1" body="Its not about getting paid" read="1" status="-1" locked="0" date_sent="1769581452000" readable_date="2026-01-28 06:24:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+19168990823" date="1769581419000" type="1" body="Im done" read="1" status="-1" locked="0" date_sent="1769581419000" readable_date="2026-01-28 06:23:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+19168990823,+14259234274,+19168990823,+12066990041" />
<sms protocol="0" address="+12066990041" date="1769580450000" type="2" body="Just remember, your paid by the hour. If some fucktards want to slow your shit down and your boss won't help, not your problem, you still getting paid" read="1" status="-1" locked="0" date_sent="1769580450000" readable_date="2026-01-28 06:07:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" group_addresses="+12066990041,+14259234274,+19168990823" />
<sms protocol="0" address="" date="1769580380000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769580380000" readable_date="2026-01-28 06:06:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580365000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769580365000" readable_date="2026-01-28 06:06:05 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580333000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769580333000" readable_date="2026-01-28 06:05:33 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580265000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769580265000" readable_date="2026-01-28 06:04:25 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580249000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769580249000" readable_date="2026-01-28 06:04:09 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580233000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769580233000" readable_date="2026-01-28 06:03:53 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769580199000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769580199000" readable_date="2026-01-28 06:03:19 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769578523000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769578523000" readable_date="2026-01-28 05:35:23 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769565432000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769565432000" readable_date="2026-01-28 01:57:12 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769565415000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769565415000" readable_date="2026-01-28 01:56:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769565407000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769565407000" readable_date="2026-01-28 01:56:47 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769565355000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769565355000" readable_date="2026-01-28 01:55:55 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769545858000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769545858000" readable_date="2026-01-27 20:30:58 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769545764000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769545764000" readable_date="2026-01-27 20:29:24 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769545595000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769545595000" readable_date="2026-01-27 20:26:35 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769545489000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769545489000" readable_date="2026-01-27 20:24:49 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769545323000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769545323000" readable_date="2026-01-27 20:22:03 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769503502000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769503502000" readable_date="2026-01-27 08:45:02 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769503499000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769503499000" readable_date="2026-01-27 08:44:59 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769503347000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769503347000" readable_date="2026-01-27 08:42:27 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769502860000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769502860000" readable_date="2026-01-27 08:34:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769483311000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769483311000" readable_date="2026-01-27 03:08:31 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="10" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeERFVXllVEJwUU55enM2ZGdPc2ZWOVEqEI" />
<sms protocol="0" address="" date="1769450581000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769450581000" readable_date="2026-01-26 18:03:01 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="17" rcs_tr_id="proto:ClQKImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLm1lc3NhZ2luZy4SLhIaKhhNeFRxTElpWjdkUVUySjBHcFNqNmpHa0EqEB" />
<sms protocol="0" address="" date="1769397870000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769397870000" readable_date="2026-01-26 03:24:30 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769397866000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769397866000" readable_date="2026-01-26 03:24:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769397230000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769397230000" readable_date="2026-01-26 03:13:50 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769396084000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769396084000" readable_date="2026-01-26 02:54:44 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769396026000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769396026000" readable_date="2026-01-26 02:53:46 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769392226000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769392226000" readable_date="2026-01-26 01:50:26 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769376468000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769376468000" readable_date="2026-01-25 21:27:48 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769374599000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769374599000" readable_date="2026-01-25 20:56:39 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CoMCCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtwBEscBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769374400000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769374400000" readable_date="2026-01-25 20:53:20 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769372962000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769372962000" readable_date="2026-01-25 20:29:22 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="15" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBiZTA0N2FjMmJjMDI0ZjcyYjc2NT" />
<sms protocol="0" address="" date="1769307916000" type="2" body="" read="1" status="-1" locked="0" date_sent="1769307916000" readable_date="2026-01-25 02:25:16 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769298193000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769298193000" readable_date="2026-01-24 23:43:13 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="13" rcs_tr_id="proto:CvEBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEsoBErUBEpgBCiA3NGEwMzlhYmIxNjk0YmEyYWI5MG" />
<sms protocol="0" address="" date="1769298188000" type="1" body="" read="1" status="-1" locked="0" date_sent="1769298188000" readable_date="2026-01-24 23:43:08 UTC" contact_name="(Unknown)" msg_protocol="RCS" thread_id="12" rcs_tr_id="proto:CvcBCiJjb20uZ29vZ2xlLmFuZHJvaWQuYXBwcy5tZXNzYWdpbmcuEtABErsBEp4BCiBhZmU2ZWQyNzUxZTA0NDMxOWVmNT" />
</smses>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""AUTARCH LoRA Training Script (Transformers + PEFT)"""
import json
import torch
from datasets import Dataset
from transformers import (
AutoModelForCausalLM, AutoTokenizer, TrainingArguments,
BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
# Quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
) if True else None
print("Loading base model: models/Hal_v2.gguf")
model = AutoModelForCausalLM.from_pretrained(
"models/Hal_v2.gguf",
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=False,
)
tokenizer = AutoTokenizer.from_pretrained("models/Hal_v2.gguf", trust_remote_code=False)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
if True:
model = prepare_model_for_kbit_training(model)
# LoRA config
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Load dataset
samples = []
with open("C:\she\autarch\data\training\autarch_dataset_20260302_202634.jsonl", "r") as f:
for line in f:
samples.append(json.loads(line))
def format_sample(sample):
if "conversations" in sample:
msgs = sample["conversations"]
text = ""
for msg in msgs:
role = "user" if msg["from"] == "human" else "assistant"
text += f"<|im_start|>{role}\n{msg['value']}<|im_end|>\n"
return {"text": text}
else:
return {"text": f"<|im_start|>user\n{sample['instruction']}\n{sample.get('input','')}<|im_end|>\n<|im_start|>assistant\n{sample['output']}<|im_end|>\n"}
dataset = Dataset.from_list([format_sample(s) for s in samples])
print(f"Dataset: {len(dataset)} samples")
# Train
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
args=TrainingArguments(
output_dir="C:\she\autarch\data\training\output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=0.0002,
warmup_ratio=0.03,
save_steps=50,
logging_steps=10,
fp16=True,
optim="adamw_8bit",
report_to="none",
),
)
print("Starting training...")
trainer.train()
print("Training complete!")
# Save
model.save_pretrained("C:\she\autarch\data\training\output/lora_adapter")
tokenizer.save_pretrained("C:\she\autarch\data\training\output/lora_adapter")
print(f"LoRA adapter saved to C:\she\autarch\data\training\output/lora_adapter")

View File

@ -1,14 +0,0 @@
C:\she\autarch\data\training\train_lora.py:50: SyntaxWarning: invalid escape sequence '\s'
with open("C:\she\autarch\data\training\autarch_dataset_20260302_202634.jsonl", "r") as f:
C:\she\autarch\data\training\train_lora.py:76: SyntaxWarning: invalid escape sequence '\s'
output_dir="C:\she\autarch\data\training\output",
C:\she\autarch\data\training\train_lora.py:95: SyntaxWarning: invalid escape sequence '\s'
model.save_pretrained("C:\she\autarch\data\training\output/lora_adapter")
C:\she\autarch\data\training\train_lora.py:96: SyntaxWarning: invalid escape sequence '\s'
tokenizer.save_pretrained("C:\she\autarch\data\training\output/lora_adapter")
C:\she\autarch\data\training\train_lora.py:97: SyntaxWarning: invalid escape sequence '\s'
print(f"LoRA adapter saved to C:\she\autarch\data\training\output/lora_adapter")
Traceback (most recent call last):
File "C:\she\autarch\data\training\train_lora.py", line 5, in <module>
from datasets import Dataset
ModuleNotFoundError: No module named 'datasets'

View File

@ -1,5 +0,0 @@
{
"username": "admin",
"password": "admin",
"force_change": true
}

586
docs/install.sh Normal file
View File

@ -0,0 +1,586 @@
#!/bin/bash
# ╔══════════════════════════════════════════════════════════════════╗
# ║ AUTARCH Installer ║
# ║ Autonomous Tactical Agent for Reconnaissance, ║
# ║ Counterintelligence, and Hacking ║
# ║ By darkHal Security Group & Setec Security Labs ║
# ╚══════════════════════════════════════════════════════════════════╝
set -e
# ── Colors & Symbols ─────────────────────────────────────────────────
R='\033[91m'; G='\033[92m'; Y='\033[93m'; B='\033[94m'; M='\033[95m'
C='\033[96m'; W='\033[97m'; D='\033[2m'; BLD='\033[1m'; RST='\033[0m'
CHK="${G}${RST}"; CROSS="${R}${RST}"; DOT="${C}${RST}"; ARROW="${M}${RST}"
WARN="${Y}${RST}"
# ── Paths ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$SCRIPT_DIR/venv"
REQ_FILE="$SCRIPT_DIR/requirements.txt"
# ── State ────────────────────────────────────────────────────────────
INSTALL_LLM_LOCAL=false
INSTALL_LLM_CLOUD=false
INSTALL_LLM_HF=false
INSTALL_SYSTEM_TOOLS=false
INSTALL_NODE_HW=false
GPU_TYPE="none"
TOTAL_STEPS=0
CURRENT_STEP=0
# ── Helper Functions ─────────────────────────────────────────────────
clear_screen() { printf '\033[2J\033[H'; }
# Draw a horizontal rule
hr() {
local char="${1:-}"
printf "${D}"
printf '%*s' 66 '' | tr ' ' "$char"
printf "${RST}\n"
}
# Print a styled header
header() {
printf "\n${BLD}${C} $1${RST}\n"
hr
}
# Print a status line
status() { printf " ${DOT} $1\n"; }
ok() { printf " ${CHK} $1\n"; }
fail() { printf " ${CROSS} $1\n"; }
warn() { printf " ${WARN} $1\n"; }
info() { printf " ${ARROW} $1\n"; }
# Progress bar
progress_bar() {
local pct=$1
local width=40
local filled=$(( pct * width / 100 ))
local empty=$(( width - filled ))
printf "\r ${D}[${RST}${G}"
printf '%*s' "$filled" '' | tr ' ' '█'
printf "${D}"
printf '%*s' "$empty" '' | tr ' ' '░'
printf "${RST}${D}]${RST} ${W}%3d%%${RST}" "$pct"
}
step_progress() {
CURRENT_STEP=$((CURRENT_STEP + 1))
local pct=$((CURRENT_STEP * 100 / TOTAL_STEPS))
progress_bar "$pct"
printf " ${D}$1${RST}\n"
}
# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) OS="linux" ;;
Darwin*) OS="macos" ;;
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
*) OS="unknown" ;;
esac
}
# Check if command exists
has() { command -v "$1" &>/dev/null; }
# ── Banner ───────────────────────────────────────────────────────────
show_banner() {
clear_screen
printf "${R}${BLD}"
cat << 'BANNER'
▄▄▄ █ ██ ▄▄▄█████▓ ▄▄▄ ██▀███ ▄████▄ ██░ ██
▒████▄ ██ ▓██▒▓ ██▒ ▓▒▒████▄ ▓██ ▒ ██▒▒██▀ ▀█ ▓██░ ██▒
▒██ ▀█▄ ▓██ ▒██░▒ ▓██░ ▒░▒██ ▀█▄ ▓██ ░▄█ ▒▒▓█ ▄ ▒██▀▀██░
░██▄▄▄▄██ ▓▓█ ░██░░ ▓██▓ ░ ░██▄▄▄▄██ ▒██▀▀█▄ ▒▓▓▄ ▄██▒░▓█ ░██
▓█ ▓██▒▒▒█████▓ ▒██▒ ░ ▓█ ▓██▒░██▓ ▒██▒▒ ▓███▀ ░░▓█▒░██▓
▒▒ ▓▒█░░▒▓▒ ▒ ▒ ▒ ░░ ▒▒ ▓▒█░░ ▒▓ ░▒▓░░ ░▒ ▒ ░ ▒ ░░▒░▒
▒ ▒▒ ░░░▒░ ░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░▒░ ░
░ ▒ ░░░ ░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
BANNER
printf "${RST}"
printf "${C}${BLD} ╔══════════════════════════════════╗${RST}\n"
printf "${C}${BLD} ║ I N S T A L L E R v1.0 ║${RST}\n"
printf "${C}${BLD} ╚══════════════════════════════════╝${RST}\n"
printf "${D} By darkHal Security Group & Setec Security Labs${RST}\n\n"
}
# ── System Check ─────────────────────────────────────────────────────
show_system_check() {
header "SYSTEM CHECK"
detect_os
case "$OS" in
linux) ok "OS: Linux ($(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -r))" ;;
macos) ok "OS: macOS $(sw_vers -productVersion 2>/dev/null)" ;;
windows) ok "OS: Windows (MSYS2/Git Bash)" ;;
*) warn "OS: Unknown ($(uname -s))" ;;
esac
# Python
if has python3; then
local pyver=$(python3 --version 2>&1 | awk '{print $2}')
local pymajor=$(echo "$pyver" | cut -d. -f1)
local pyminor=$(echo "$pyver" | cut -d. -f2)
if [ "$pymajor" -ge 3 ] && [ "$pyminor" -ge 10 ]; then
ok "Python $pyver"
else
warn "Python $pyver ${D}(3.10+ recommended)${RST}"
fi
elif has python; then
local pyver=$(python --version 2>&1 | awk '{print $2}')
ok "Python $pyver ${D}(using 'python' command)${RST}"
else
fail "Python not found — install Python 3.10+"
exit 1
fi
# pip
if has pip3 || has pip; then
ok "pip available"
else
fail "pip not found"
exit 1
fi
# Git
if has git; then
ok "Git $(git --version | awk '{print $3}')"
else
warn "Git not found ${D}(optional)${RST}"
fi
# Node/npm
if has node && has npm; then
ok "Node $(node --version) / npm $(npm --version 2>/dev/null)"
else
warn "Node.js not found ${D}(needed for hardware WebUSB libs)${RST}"
fi
# System tools
local tools=("nmap" "tshark" "openssl" "adb" "fastboot" "wg" "upnpc")
local found=()
local missing=()
for t in "${tools[@]}"; do
if has "$t"; then
found+=("$t")
else
missing+=("$t")
fi
done
if [ ${#found[@]} -gt 0 ]; then
ok "System tools: ${G}${found[*]}${RST}"
fi
if [ ${#missing[@]} -gt 0 ]; then
info "Not found: ${D}${missing[*]}${RST} ${D}(optional)${RST}"
fi
# GPU detection
if has nvidia-smi; then
local gpu_name=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1)
ok "GPU: ${G}$gpu_name${RST} (CUDA)"
GPU_TYPE="cuda"
elif has rocm-smi; then
ok "GPU: AMD ROCm detected"
GPU_TYPE="rocm"
elif [ -d "/opt/intel" ] || has xpu-smi; then
ok "GPU: Intel XPU detected"
GPU_TYPE="intel"
elif [ "$OS" = "macos" ]; then
ok "GPU: Apple Metal (auto via llama-cpp)"
GPU_TYPE="metal"
else
info "No GPU detected ${D}(CPU-only mode)${RST}"
fi
echo
}
# ── Interactive Menu ─────────────────────────────────────────────────
show_menu() {
header "INSTALL OPTIONS"
echo
printf " ${BLD}${W}What would you like to install?${RST}\n\n"
printf " ${BLD}${C}[1]${RST} ${W}Core only${RST} ${D}Flask, OSINT, networking, analysis${RST}\n"
printf " ${BLD}${C}[2]${RST} ${W}Core + Local LLM${RST} ${D}+ llama-cpp-python (GGUF models)${RST}\n"
printf " ${BLD}${C}[3]${RST} ${W}Core + Cloud LLM${RST} ${D}+ anthropic SDK (Claude API)${RST}\n"
printf " ${BLD}${C}[4]${RST} ${W}Core + HuggingFace${RST} ${D}+ transformers, torch, accelerate${RST}\n"
printf " ${BLD}${C}[5]${RST} ${W}Full install${RST} ${D}All of the above${RST}\n"
echo
printf " ${BLD}${Y}[S]${RST} ${W}System tools${RST} ${D}nmap, tshark, openssl, adb (Linux only)${RST}\n"
printf " ${BLD}${Y}[H]${RST} ${W}Hardware libs${RST} ${D}Build WebUSB/Serial JS bundles (needs npm)${RST}\n"
echo
printf " ${BLD}${R}[Q]${RST} ${W}Quit${RST}\n"
echo
hr
printf " ${BLD}Choice: ${RST}"
read -r choice
case "$choice" in
1) ;;
2) INSTALL_LLM_LOCAL=true ;;
3) INSTALL_LLM_CLOUD=true ;;
4) INSTALL_LLM_HF=true ;;
5) INSTALL_LLM_LOCAL=true; INSTALL_LLM_CLOUD=true; INSTALL_LLM_HF=true ;;
s|S) INSTALL_SYSTEM_TOOLS=true ;;
h|H) INSTALL_NODE_HW=true ;;
q|Q) printf "\n ${D}Bye.${RST}\n\n"; exit 0 ;;
*) warn "Invalid choice"; show_menu; return ;;
esac
# Extras prompt (only for options 1-5)
if [[ "$choice" =~ ^[1-5]$ ]]; then
echo
printf " ${D}Also install system tools? (nmap, tshark, etc.) [y/N]:${RST} "
read -r yn
[[ "$yn" =~ ^[Yy] ]] && INSTALL_SYSTEM_TOOLS=true
printf " ${D}Also build hardware JS bundles? (needs npm) [y/N]:${RST} "
read -r yn
[[ "$yn" =~ ^[Yy] ]] && INSTALL_NODE_HW=true
fi
}
# ── Install Functions ────────────────────────────────────────────────
get_pip() {
if has pip3; then echo "pip3"
elif has pip; then echo "pip"
fi
}
get_python() {
if has python3; then echo "python3"
elif has python; then echo "python"
fi
}
create_venv() {
header "VIRTUAL ENVIRONMENT"
if [ -d "$VENV_DIR" ]; then
ok "venv already exists at ${D}$VENV_DIR${RST}"
else
status "Creating virtual environment..."
$(get_python) -m venv "$VENV_DIR"
ok "Created venv at ${D}$VENV_DIR${RST}"
fi
# Activate
if [ "$OS" = "windows" ]; then
source "$VENV_DIR/Scripts/activate" 2>/dev/null || source "$VENV_DIR/bin/activate"
else
source "$VENV_DIR/bin/activate"
fi
ok "Activated venv ${D}($(which python))${RST}"
echo
}
install_core() {
header "CORE DEPENDENCIES"
step_progress "Upgrading pip..."
$(get_python) -m pip install --upgrade pip setuptools wheel -q 2>&1 | tail -1
step_progress "Installing core packages..."
# Install from requirements.txt but skip optional LLM lines
# Core packages: flask, bcrypt, requests, msgpack, pyserial, esptool, pyshark, qrcode, Pillow
local core_pkgs=(
"flask>=3.0"
"bcrypt>=4.0"
"requests>=2.31"
"msgpack>=1.0"
"pyserial>=3.5"
"esptool>=4.0"
"pyshark>=0.6"
"qrcode>=7.0"
"Pillow>=10.0"
)
for pkg in "${core_pkgs[@]}"; do
local name=$(echo "$pkg" | sed 's/[>=<].*//')
step_progress "$name"
pip install "$pkg" -q 2>&1 | tail -1
done
ok "Core dependencies installed"
echo
}
install_llm_local() {
header "LOCAL LLM (llama-cpp-python)"
if [ "$GPU_TYPE" = "cuda" ]; then
info "CUDA detected — building with GPU acceleration"
step_progress "llama-cpp-python (CUDA)..."
CMAKE_ARGS="-DGGML_CUDA=on" pip install llama-cpp-python>=0.3.16 --force-reinstall --no-cache-dir -q 2>&1 | tail -1
elif [ "$GPU_TYPE" = "rocm" ]; then
info "ROCm detected — building with AMD GPU acceleration"
step_progress "llama-cpp-python (ROCm)..."
CMAKE_ARGS="-DGGML_HIPBLAS=on" pip install llama-cpp-python>=0.3.16 --force-reinstall --no-cache-dir -q 2>&1 | tail -1
elif [ "$GPU_TYPE" = "metal" ]; then
info "Apple Metal — auto-enabled in llama-cpp"
step_progress "llama-cpp-python (Metal)..."
pip install llama-cpp-python>=0.3.16 -q 2>&1 | tail -1
else
info "CPU-only mode"
step_progress "llama-cpp-python (CPU)..."
pip install llama-cpp-python>=0.3.16 -q 2>&1 | tail -1
fi
ok "llama-cpp-python installed"
echo
}
install_llm_cloud() {
header "CLOUD LLM (Anthropic Claude API)"
step_progress "anthropic SDK..."
pip install "anthropic>=0.40" -q 2>&1 | tail -1
ok "Anthropic SDK installed"
info "Set your API key in autarch_settings.conf [claude] section"
echo
}
install_llm_hf() {
header "HUGGINGFACE (transformers + torch)"
step_progress "transformers..."
pip install "transformers>=4.35" -q 2>&1 | tail -1
step_progress "accelerate..."
pip install "accelerate>=0.25" -q 2>&1 | tail -1
# PyTorch — pick the right variant
step_progress "PyTorch..."
if [ "$GPU_TYPE" = "cuda" ]; then
info "Installing PyTorch with CUDA support..."
pip install torch --index-url https://download.pytorch.org/whl/cu121 -q 2>&1 | tail -1
elif [ "$GPU_TYPE" = "rocm" ]; then
info "Installing PyTorch with ROCm support..."
pip install torch --index-url https://download.pytorch.org/whl/rocm6.0 -q 2>&1 | tail -1
elif [ "$GPU_TYPE" = "intel" ]; then
info "Installing PyTorch with Intel XPU support..."
pip install torch intel-extension-for-pytorch -q 2>&1 | tail -1
else
pip install torch -q 2>&1 | tail -1
fi
# bitsandbytes (Linux/CUDA only)
if [ "$OS" = "linux" ] && [ "$GPU_TYPE" = "cuda" ]; then
step_progress "bitsandbytes (quantization)..."
pip install "bitsandbytes>=0.41" -q 2>&1 | tail -1
else
info "Skipping bitsandbytes ${D}(Linux + CUDA only)${RST}"
fi
ok "HuggingFace stack installed"
echo
}
install_system_tools() {
header "SYSTEM TOOLS"
if [ "$OS" != "linux" ]; then
warn "System tool install is only automated on Linux (apt/dnf/pacman)"
info "On $OS, install these manually: nmap, wireshark-cli, openssl, android-tools"
echo
return
fi
# Detect package manager
local PM=""
local INSTALL=""
if has apt-get; then
PM="apt"
INSTALL="sudo apt-get install -y"
elif has dnf; then
PM="dnf"
INSTALL="sudo dnf install -y"
elif has pacman; then
PM="pacman"
INSTALL="sudo pacman -S --noconfirm"
else
warn "No supported package manager found (apt/dnf/pacman)"
echo
return
fi
ok "Package manager: ${G}$PM${RST}"
local packages=()
# nmap
if ! has nmap; then
packages+=("nmap")
status "Will install: nmap"
else
ok "nmap already installed"
fi
# tshark
if ! has tshark; then
case "$PM" in
apt) packages+=("tshark") ;;
dnf) packages+=("wireshark-cli") ;;
pacman) packages+=("wireshark-cli") ;;
esac
status "Will install: tshark/wireshark-cli"
else
ok "tshark already installed"
fi
# openssl
if ! has openssl; then
packages+=("openssl")
status "Will install: openssl"
else
ok "openssl already installed"
fi
# adb/fastboot
if ! has adb; then
case "$PM" in
apt) packages+=("android-tools-adb android-tools-fastboot") ;;
dnf) packages+=("android-tools") ;;
pacman) packages+=("android-tools") ;;
esac
status "Will install: adb + fastboot"
else
ok "adb already installed"
fi
# wireguard
if ! has wg; then
case "$PM" in
apt) packages+=("wireguard wireguard-tools") ;;
dnf) packages+=("wireguard-tools") ;;
pacman) packages+=("wireguard-tools") ;;
esac
status "Will install: wireguard-tools"
else
ok "wireguard already installed"
fi
# miniupnpc
if ! has upnpc; then
packages+=("miniupnpc")
status "Will install: miniupnpc"
else
ok "miniupnpc already installed"
fi
if [ ${#packages[@]} -gt 0 ]; then
echo
info "Installing with: $PM"
if [ "$PM" = "apt" ]; then
sudo apt-get update -qq 2>&1 | tail -1
fi
$INSTALL ${packages[@]} 2>&1 | tail -5
ok "System tools installed"
else
ok "All system tools already present"
fi
echo
}
install_node_hw() {
header "HARDWARE JS BUNDLES (WebUSB / Web Serial)"
if ! has npm; then
fail "npm not found — install Node.js first"
info "https://nodejs.org or: apt install nodejs npm"
echo
return
fi
step_progress "npm install..."
(cd "$SCRIPT_DIR" && npm install --silent 2>&1 | tail -3)
step_progress "Building bundles..."
if [ -f "$SCRIPT_DIR/scripts/build-hw-libs.sh" ]; then
(cd "$SCRIPT_DIR" && bash scripts/build-hw-libs.sh 2>&1 | tail -5)
ok "Hardware bundles built"
else
warn "scripts/build-hw-libs.sh not found"
fi
echo
}
# ── Summary ──────────────────────────────────────────────────────────
show_summary() {
hr "═"
printf "\n${BLD}${G} INSTALLATION COMPLETE${RST}\n\n"
printf " ${BLD}${W}Quick Start:${RST}\n"
echo
if [ "$OS" = "windows" ]; then
printf " ${D}# Activate the virtual environment${RST}\n"
printf " ${C}source venv/Scripts/activate${RST}\n\n"
else
printf " ${D}# Activate the virtual environment${RST}\n"
printf " ${C}source venv/bin/activate${RST}\n\n"
fi
printf " ${D}# Launch the CLI${RST}\n"
printf " ${C}python autarch.py${RST}\n\n"
printf " ${D}# Launch the web dashboard${RST}\n"
printf " ${C}python autarch_web.py${RST}\n\n"
printf " ${D}# Open in browser${RST}\n"
printf " ${C}https://localhost:8181${RST}\n"
echo
if $INSTALL_LLM_LOCAL; then
printf " ${ARROW} Local LLM: place a .gguf model in ${D}models/${RST}\n"
printf " ${D}and set model_path in autarch_settings.conf [llama]${RST}\n"
fi
if $INSTALL_LLM_CLOUD; then
printf " ${ARROW} Claude API: set api_key in ${D}autarch_settings.conf [claude]${RST}\n"
fi
echo
hr "═"
echo
}
# ── Main ─────────────────────────────────────────────────────────────
main() {
show_banner
show_system_check
show_menu
# Calculate total steps for progress
TOTAL_STEPS=11 # pip upgrade + 9 core packages + 1 finish
$INSTALL_LLM_LOCAL && TOTAL_STEPS=$((TOTAL_STEPS + 1))
$INSTALL_LLM_CLOUD && TOTAL_STEPS=$((TOTAL_STEPS + 1))
$INSTALL_LLM_HF && TOTAL_STEPS=$((TOTAL_STEPS + 4))
$INSTALL_NODE_HW && TOTAL_STEPS=$((TOTAL_STEPS + 2))
echo
create_venv
install_core
$INSTALL_LLM_LOCAL && install_llm_local
$INSTALL_LLM_CLOUD && install_llm_cloud
$INSTALL_LLM_HF && install_llm_hf
$INSTALL_SYSTEM_TOOLS && install_system_tools
$INSTALL_NODE_HW && install_node_hw
show_summary
}
main "$@"

669
docs/setec_manager_plan.md Normal file
View File

@ -0,0 +1,669 @@
# Setec App Manager — Architecture Plan
**A lightweight Plesk/cPanel replacement built in Go, designed to work with AUTARCH**
By darkHal Security Group & Setec Security Labs
---
## 1. What Is This?
Setec App Manager is a standalone Go application that turns a bare Debian 13 VPS into a fully managed web hosting platform. It provides:
- A **web dashboard** (its own HTTP server on port 9090) for managing the VPS
- **Multi-domain hosting** with Nginx reverse proxy management
- **Git-based deployment** (clone, pull, restart)
- **SSL/TLS automation** via Let's Encrypt (ACME)
- **AUTARCH-native integration** — first-class support for deploying and managing AUTARCH instances
- **System administration** — users, firewall, packages, monitoring, backups
- **Float Mode backend** — WebSocket bridge for AUTARCH Cloud Edition USB passthrough
It is NOT a general-purpose hosting panel. It is purpose-built for running AUTARCH and supporting web applications on a single VPS, with the lightest possible footprint.
---
## 2. Technology Stack
| Component | Choice | Rationale |
|-----------|--------|-----------|
| Language | Go 1.22+ | Single binary, no runtime deps, fast |
| Web framework | `net/http` + `chi` router | Lightweight, stdlib-based |
| Templates | Go `html/template` | Built-in, secure, fast |
| Database | SQLite (via `modernc.org/sqlite`) | Zero-config, embedded, pure Go |
| Reverse proxy | Nginx (managed configs) | Battle-tested, performant |
| SSL | certbot / ACME (`golang.org/x/crypto/acme`) | Let's Encrypt automation |
| Auth | bcrypt + JWT sessions | Compatible with AUTARCH's credential format |
| Firewall | ufw / iptables (via exec) | Standard Debian tooling |
| Process mgmt | systemd (unit generation) | Native Debian service management |
| WebSocket | `gorilla/websocket` | For Float Mode USB bridge + live logs |
---
## 3. Directory Structure
```
/opt/setec-manager/
├── setec-manager # Single Go binary
├── config.yaml # Manager configuration
├── data/
│ ├── setec.db # SQLite database (sites, users, logs, jobs)
│ ├── credentials.json # Admin credentials (bcrypt)
│ └── acme/ # Let's Encrypt account + certs
├── templates/ # Embedded HTML templates (via embed.FS)
├── static/ # Embedded CSS/JS assets
└── nginx/
├── sites-available/ # Generated per-domain configs
└── snippets/ # Shared SSL/proxy snippets
```
**Managed directories on the VPS:**
```
/var/www/ # Web applications root
├── autarch/ # AUTARCH instance (cloned from git)
├── example.com/ # Static site or app
└── api.example.com/ # Another app
/etc/nginx/
├── sites-available/ # Setec-generated Nginx configs
├── sites-enabled/ # Symlinks to active sites
└── snippets/
├── ssl-params.conf # Shared SSL settings
└── proxy-params.conf # Shared proxy headers
/etc/systemd/system/
├── setec-manager.service # Manager itself
├── autarch-web.service # AUTARCH web service
├── autarch-dns.service # AUTARCH DNS service
└── app-*.service # Per-app service units
```
---
## 4. Database Schema
```sql
-- Sites / domains managed by the panel
CREATE TABLE sites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
aliases TEXT DEFAULT '', -- comma-separated alt domains
app_type TEXT NOT NULL DEFAULT 'static', -- 'static', 'reverse_proxy', 'autarch', 'python', 'node'
app_root TEXT NOT NULL, -- /var/www/domain.com
app_port INTEGER DEFAULT 0, -- backend port (for reverse proxy)
app_entry TEXT DEFAULT '', -- entry point (e.g., autarch_web.py, server.js)
git_repo TEXT DEFAULT '', -- git clone URL
git_branch TEXT DEFAULT 'main',
ssl_enabled BOOLEAN DEFAULT FALSE,
ssl_cert_path TEXT DEFAULT '',
ssl_key_path TEXT DEFAULT '',
ssl_auto BOOLEAN DEFAULT TRUE, -- auto Let's Encrypt
enabled BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- System users (for SSH/SFTP access)
CREATE TABLE system_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
uid INTEGER,
home_dir TEXT,
shell TEXT DEFAULT '/bin/bash',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Manager users (web panel login)
CREATE TABLE manager_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'admin', -- 'admin', 'viewer'
force_change BOOLEAN DEFAULT FALSE,
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Deployment history
CREATE TABLE deployments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id),
action TEXT NOT NULL, -- 'clone', 'pull', 'restart', 'ssl_renew'
status TEXT DEFAULT 'pending', -- 'pending', 'running', 'success', 'failed'
output TEXT DEFAULT '',
started_at DATETIME,
finished_at DATETIME
);
-- Scheduled jobs (SSL renewal, backups, git pull)
CREATE TABLE cron_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id), -- NULL for system jobs
job_type TEXT NOT NULL, -- 'ssl_renew', 'backup', 'git_pull', 'restart'
schedule TEXT NOT NULL, -- cron expression
enabled BOOLEAN DEFAULT TRUE,
last_run DATETIME,
next_run DATETIME
);
-- Firewall rules
CREATE TABLE firewall_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
direction TEXT DEFAULT 'in', -- 'in', 'out'
protocol TEXT DEFAULT 'tcp',
port TEXT NOT NULL, -- '80', '443', '8181', '80,443', '1000:2000'
source TEXT DEFAULT 'any',
action TEXT DEFAULT 'allow', -- 'allow', 'deny'
comment TEXT DEFAULT '',
enabled BOOLEAN DEFAULT TRUE
);
-- Float Mode sessions (AUTARCH Cloud Edition)
CREATE TABLE float_sessions (
id TEXT PRIMARY KEY, -- UUID session token
user_id INTEGER REFERENCES manager_users(id),
client_ip TEXT,
client_agent TEXT, -- browser user-agent
usb_bridge BOOLEAN DEFAULT FALSE, -- USB passthrough active
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_ping DATETIME,
expires_at DATETIME
);
-- Backups
CREATE TABLE backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id), -- NULL for full system backup
backup_type TEXT DEFAULT 'site', -- 'site', 'database', 'full'
file_path TEXT NOT NULL,
size_bytes INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
---
## 5. Web Dashboard Routes
### 5.1 Authentication
```
GET /login → Login page
POST /login → Authenticate (returns JWT cookie)
POST /logout → Clear session
GET /api/auth/status → Current user info
```
### 5.2 Dashboard
```
GET / → Dashboard overview (system stats, sites, services)
GET /api/system/info → CPU, RAM, disk, uptime, load
GET /api/system/processes → Top processes by resource usage
```
### 5.3 Site Management
```
GET /sites → Site list page
GET /sites/new → New site form
POST /sites → Create site (clone repo, generate nginx config, enable)
GET /sites/:id → Site detail / edit
PUT /sites/:id → Update site config
DELETE /sites/:id → Remove site (disable nginx, optionally delete files)
POST /sites/:id/deploy → Git pull + restart
POST /sites/:id/restart → Restart app service
POST /sites/:id/stop → Stop app service
POST /sites/:id/start → Start app service
GET /sites/:id/logs → View app logs (journalctl stream)
GET /sites/:id/logs/stream → SSE live log stream
```
### 5.4 AUTARCH Management
```
POST /autarch/install → Clone from git, setup venv, install deps
POST /autarch/update → Git pull + pip install + restart
GET /autarch/status → Service status, version, config
POST /autarch/start → Start AUTARCH web + DNS
POST /autarch/stop → Stop all AUTARCH services
POST /autarch/restart → Restart all AUTARCH services
GET /autarch/config → Read autarch_settings.conf
PUT /autarch/config → Update autarch_settings.conf
POST /autarch/dns/build → Build DNS server from source
```
### 5.5 SSL / Certificates
```
GET /ssl → Certificate overview
POST /ssl/:domain/issue → Issue Let's Encrypt cert (ACME)
POST /ssl/:domain/renew → Renew cert
POST /ssl/:domain/upload → Upload custom cert + key
DELETE /ssl/:domain → Remove cert
GET /api/ssl/status → All cert statuses + expiry dates
```
### 5.6 Nginx Management
```
GET /nginx/status → Nginx service status + config test
POST /nginx/reload → Reload nginx (graceful)
POST /nginx/restart → Restart nginx
GET /nginx/config/:domain → View generated config
PUT /nginx/config/:domain → Edit config (with validation)
POST /nginx/test → nginx -t (config syntax check)
```
### 5.7 Firewall
```
GET /firewall → Rule list + status
POST /firewall/rules → Add rule
DELETE /firewall/rules/:id → Remove rule
POST /firewall/enable → Enable firewall (ufw enable)
POST /firewall/disable → Disable firewall
GET /api/firewall/status → Current rules + status JSON
```
### 5.8 System Users
```
GET /users → System user list
POST /users → Create system user (useradd)
DELETE /users/:id → Remove system user
POST /users/:id/password → Reset password
POST /users/:id/ssh-key → Add SSH public key
```
### 5.9 Panel Users
```
GET /panel/users → Manager user list
POST /panel/users → Create panel user
PUT /panel/users/:id → Update (role, password)
DELETE /panel/users/:id → Remove
```
### 5.10 Backups
```
GET /backups → Backup list
POST /backups/site/:id → Backup specific site (tar.gz)
POST /backups/full → Full system backup
POST /backups/:id/restore → Restore from backup
DELETE /backups/:id → Delete backup file
GET /backups/:id/download → Download backup archive
```
### 5.11 Monitoring
```
GET /monitor → System monitoring page
GET /api/monitor/cpu → CPU usage (1s sample)
GET /api/monitor/memory → Memory usage
GET /api/monitor/disk → Disk usage per mount
GET /api/monitor/network → Network I/O stats
GET /api/monitor/services → Service status list
WS /api/monitor/live → WebSocket live stats stream (1s interval)
```
### 5.12 Float Mode Backend
```
POST /float/register → Register Float client (returns session token)
WS /float/bridge/:session → WebSocket USB bridge (binary frames)
GET /float/sessions → Active Float sessions
DELETE /float/sessions/:id → Disconnect Float session
POST /float/usb/enumerate → List USB devices on connected client
POST /float/usb/open → Open USB device on client
POST /float/usb/close → Close USB device on client
POST /float/usb/transfer → USB bulk/interrupt transfer via bridge
```
### 5.13 Logs
```
GET /logs → Log viewer page
GET /api/logs/system → System logs (journalctl)
GET /api/logs/nginx → Nginx access + error logs
GET /api/logs/setec → Manager logs
GET /api/logs/stream → SSE live log stream (filterable)
```
---
## 6. Core Features Detail
### 6.1 Site Deployment Flow
When creating a new site:
```
1. User submits: domain, git_repo (optional), app_type, app_port
2. Manager:
a. Creates /var/www/<domain>/
b. If git_repo: git clone <repo> /var/www/<domain>
c. If python app: creates venv, pip install -r requirements.txt
d. If node app: npm install
e. Generates Nginx config from template
f. Writes to /etc/nginx/sites-available/<domain>
g. Symlinks to sites-enabled/
h. If ssl_auto: runs ACME cert issuance
i. Generates systemd unit for the app
j. Starts the app service
k. Reloads nginx
l. Records deployment in database
```
### 6.2 AUTARCH Install Flow
```
1. git clone https://github.com/DigijEth/autarch.git /var/www/autarch
2. chown -R autarch:autarch /var/www/autarch
3. python3 -m venv /var/www/autarch/venv
4. /var/www/autarch/venv/bin/pip install -r /var/www/autarch/requirements.txt
5. npm install (in /var/www/autarch for hardware JS bundles)
6. bash /var/www/autarch/scripts/build-hw-libs.sh
7. Copy default autarch_settings.conf → update web.host/port, web.secret_key
8. Generate systemd units (autarch-web, autarch-dns)
9. Generate Nginx reverse proxy config (domain → localhost:8181)
10. Issue SSL cert
11. Enable + start services
12. Record deployment
```
### 6.3 Nginx Config Templates
**Reverse Proxy (AUTARCH / Python / Node):**
```nginx
server {
listen 80;
server_name {{.Domain}} {{.Aliases}};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name {{.Domain}} {{.Aliases}};
ssl_certificate {{.SSLCertPath}};
ssl_certificate_key {{.SSLKeyPath}};
include snippets/ssl-params.conf;
location / {
proxy_pass https://127.0.0.1:{{.AppPort}};
include snippets/proxy-params.conf;
}
# WebSocket support (for AUTARCH SSE/WebSocket)
location /api/ {
proxy_pass https://127.0.0.1:{{.AppPort}};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
include snippets/proxy-params.conf;
}
}
```
**Static Site:**
```nginx
server {
listen 443 ssl http2;
server_name {{.Domain}};
root {{.AppRoot}};
index index.html;
ssl_certificate {{.SSLCertPath}};
ssl_certificate_key {{.SSLKeyPath}};
include snippets/ssl-params.conf;
location / {
try_files $uri $uri/ =404;
}
}
```
### 6.4 Firewall Default Rules
On first setup, Setec Manager installs these ufw rules:
```
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment "SSH"
ufw allow 80/tcp comment "HTTP"
ufw allow 443/tcp comment "HTTPS"
ufw allow 9090/tcp comment "Setec Manager"
ufw allow 8181/tcp comment "AUTARCH Web"
ufw allow 53 comment "AUTARCH DNS"
ufw enable
```
### 6.5 Float Mode USB Bridge
The Float Mode bridge is the backend half of AUTARCH Cloud Edition's USB passthrough. It works as follows:
```
┌──────────────────┐ WebSocket ┌──────────────────┐
│ User's Browser │◄──────────────────►│ Setec Manager │
│ (AUTARCH CE) │ │ (VPS) │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ Float Applet │ USB Commands
│ (runs on user's PC) │ forwarded to
│ │ AUTARCH modules
┌────────▼─────────┐ ┌────────▼─────────┐
│ WebSocket Client │ │ AUTARCH Backend │
│ + USB Access │ │ (hardware.py │
│ (native app) │ │ equivalent) │
└────────┬─────────┘ └──────────────────┘
┌────▼────┐
│ USB Hub │ ← Physical devices (phones, ESP32, etc.)
└─────────┘
```
**Protocol:**
1. Float applet on user's PC opens WebSocket to `wss://domain/float/bridge/<session>`
2. Manager authenticates session token
3. Binary WebSocket frames carry USB commands and data:
- Frame type byte: `0x01` = enumerate, `0x02` = open, `0x03` = close, `0x04` = transfer
- Payload: device descriptor, endpoint, data
4. Manager translates USB operations into AUTARCH hardware module calls
5. Results flow back over the same WebSocket
This is the **server side only**. The client applet is designed in `autarch_float.md`.
---
## 7. Go Package Structure
```
services/setec-manager/
├── cmd/
│ └── main.go # Entry point, flag parsing
├── internal/
│ ├── server/
│ │ ├── server.go # HTTP server setup, middleware, router
│ │ ├── auth.go # JWT auth, login/logout handlers
│ │ └── middleware.go # Logging, auth check, CORS
│ ├── handlers/
│ │ ├── dashboard.go # Dashboard + system info
│ │ ├── sites.go # Site CRUD + deployment
│ │ ├── autarch.go # AUTARCH-specific management
│ │ ├── ssl.go # Certificate management
│ │ ├── nginx.go # Nginx config + control
│ │ ├── firewall.go # ufw rule management
│ │ ├── users.go # System + panel user management
│ │ ├── backups.go # Backup/restore operations
│ │ ├── monitor.go # System monitoring + WebSocket stream
│ │ ├── logs.go # Log viewer + SSE stream
│ │ └── float.go # Float Mode WebSocket bridge
│ ├── nginx/
│ │ ├── config.go # Nginx config generation
│ │ ├── templates.go # Go templates for nginx configs
│ │ └── control.go # nginx reload/restart/test
│ ├── acme/
│ │ └── acme.go # Let's Encrypt ACME client
│ ├── deploy/
│ │ ├── git.go # Git clone/pull operations
│ │ ├── python.go # Python venv + pip setup
│ │ ├── node.go # npm install
│ │ └── systemd.go # Service unit generation + control
│ ├── system/
│ │ ├── info.go # CPU, RAM, disk, network stats
│ │ ├── firewall.go # ufw wrapper
│ │ ├── users.go # useradd/userdel/passwd wrappers
│ │ └── packages.go # apt wrapper
│ ├── db/
│ │ ├── db.go # SQLite connection + migrations
│ │ ├── sites.go # Site queries
│ │ ├── users.go # User queries
│ │ ├── deployments.go # Deployment history queries
│ │ ├── backups.go # Backup queries
│ │ └── float.go # Float session queries
│ ├── float/
│ │ ├── bridge.go # WebSocket USB bridge protocol
│ │ ├── session.go # Session management
│ │ └── protocol.go # Binary frame protocol definitions
│ └── config/
│ └── config.go # YAML config loader
├── web/
│ ├── templates/ # HTML templates (embedded)
│ │ ├── base.html
│ │ ├── login.html
│ │ ├── dashboard.html
│ │ ├── sites.html
│ │ ├── site_detail.html
│ │ ├── site_new.html
│ │ ├── autarch.html
│ │ ├── ssl.html
│ │ ├── nginx.html
│ │ ├── firewall.html
│ │ ├── users.html
│ │ ├── backups.html
│ │ ├── monitor.html
│ │ ├── logs.html
│ │ └── float.html
│ └── static/ # CSS/JS assets (embedded)
│ ├── css/style.css
│ └── js/app.js
├── build.sh # Build script
├── go.mod
├── config.yaml # Default config
└── README.md
```
---
## 8. Configuration (config.yaml)
```yaml
server:
host: "0.0.0.0"
port: 9090
tls: true
cert: "/opt/setec-manager/data/acme/manager.crt"
key: "/opt/setec-manager/data/acme/manager.key"
database:
path: "/opt/setec-manager/data/setec.db"
nginx:
sites_available: "/etc/nginx/sites-available"
sites_enabled: "/etc/nginx/sites-enabled"
snippets: "/etc/nginx/snippets"
webroot: "/var/www"
certbot_webroot: "/var/www/certbot"
acme:
email: "" # Let's Encrypt registration email
staging: false # Use LE staging for testing
account_dir: "/opt/setec-manager/data/acme"
autarch:
install_dir: "/var/www/autarch"
git_repo: "https://github.com/DigijEth/autarch.git"
git_branch: "main"
web_port: 8181
dns_port: 53
float:
enabled: false
max_sessions: 10
session_ttl: "24h"
backups:
dir: "/opt/setec-manager/data/backups"
max_age_days: 30
max_count: 50
logging:
level: "info"
file: "/var/log/setec-manager.log"
max_size_mb: 100
max_backups: 3
```
---
## 9. Build Targets
```
Part 1: Core server, auth, dashboard, site CRUD, Nginx config gen,
AUTARCH install/deploy, systemd management
(~4,000 lines)
Part 2: SSL/ACME automation, firewall management, system users,
backup/restore, system monitoring
(~3,500 lines)
Part 3: Float Mode WebSocket bridge, live log streaming,
deployment history, scheduled jobs (cron), web UI polish
(~3,500 lines)
Part 4: Web UI templates + CSS + JS, full frontend for all features
(~3,000 lines Go templates + 2,000 lines CSS/JS)
Total estimated: ~16,000 lines
```
---
## 10. Security Considerations
- Manager runs as root (required for nginx, systemd, useradd)
- Web panel protected by bcrypt + JWT with short-lived tokens
- All subprocess calls use `exec.Command()` with argument arrays (no shell injection)
- Nginx configs validated with `nginx -t` before reload
- ACME challenges served from dedicated webroot (no app interference)
- Float Mode sessions require authentication + have TTL
- USB bridge frames validated for protocol compliance
- SQLite database file permissions: 0600
- Credentials file permissions: 0600
- All user-supplied domains validated against DNS before cert issuance
- Rate limiting on login attempts (5 per minute per IP)
---
## 11. First-Run Bootstrap
When `setec-manager` is run for the first time on a fresh Debian 13 VPS:
```
1. Detect if first run (no config.yaml or empty database)
2. Interactive TUI setup:
a. Set admin username + password
b. Set manager domain (or IP)
c. Set email for Let's Encrypt
d. Configure AUTARCH auto-install (y/n)
3. System setup:
a. apt update && apt install -y nginx certbot python3 python3-venv git ufw
b. Generate Nginx base config + snippets
c. Configure ufw default rules
d. Enable ufw
4. If AUTARCH auto-install:
a. Clone from git
b. Full AUTARCH setup (venv, pip, npm, build)
c. Generate + install systemd units
d. Generate Nginx reverse proxy
e. Issue SSL cert
f. Start AUTARCH
5. Start Setec Manager web dashboard
6. Print access URL
```

Binary file not shown.

View File

@ -0,0 +1,51 @@
#!/bin/bash
# Build Autarch Server Manager
# Usage: bash build.sh
#
# Targets: Linux AMD64 (Debian 13 server)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo "══════════════════════════════════════════════════════"
echo " Building Autarch Server Manager"
echo "══════════════════════════════════════════════════════"
echo
# Resolve dependencies
echo "[1/3] Resolving Go dependencies..."
go mod tidy
echo " ✔ Dependencies resolved"
echo
# Build for Linux AMD64 (Debian 13 target)
echo "[2/3] Building linux/amd64..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
-o autarch-server-manager \
./cmd/
echo " ✔ autarch-server-manager ($(ls -lh autarch-server-manager | awk '{print $5}'))"
echo
# Also build for current platform if different
if [ "$(go env GOOS)" != "linux" ] || [ "$(go env GOARCH)" != "amd64" ]; then
echo "[3/3] Building for current platform ($(go env GOOS)/$(go env GOARCH))..."
go build \
-ldflags="-s -w" \
-o autarch-server-manager-local \
./cmd/
echo " ✔ autarch-server-manager-local"
else
echo "[3/3] Current platform is linux/amd64 — skipping duplicate build"
fi
echo
echo "══════════════════════════════════════════════════════"
echo " Build complete!"
echo ""
echo " Deploy to server:"
echo " scp autarch-server-manager root@server:/opt/autarch/"
echo " ssh root@server /opt/autarch/autarch-server-manager"
echo "══════════════════════════════════════════════════════"

Binary file not shown.

View File

@ -0,0 +1,25 @@
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/darkhal/autarch-server-manager/internal/tui"
)
const version = "1.0.0"
func main() {
if os.Geteuid() != 0 {
fmt.Println("\033[91m[!] Autarch Server Manager requires root privileges.\033[0m")
fmt.Println(" Run with: sudo ./autarch-server-manager")
os.Exit(1)
}
p := tea.NewProgram(tui.NewApp(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,32 @@
module github.com/darkhal/autarch-server-manager
go 1.23.0
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.4
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/crypto v0.32.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

View File

@ -0,0 +1,51 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=

View File

@ -0,0 +1,132 @@
// Package config provides INI-style configuration file parsing and editing
// for autarch_settings.conf. It preserves comments and formatting.
package config
import (
"fmt"
"os"
"strings"
)
// ListSections returns all [section] names from an INI file.
func ListSections(path string) ([]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var sections []string
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
sec := line[1 : len(line)-1]
sections = append(sections, sec)
}
}
return sections, nil
}
// GetSection returns all key-value pairs from a specific section.
func GetSection(path, section string) (keys []string, vals []string, err error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, fmt.Errorf("read config: %w", err)
}
inSection := false
target := "[" + section + "]"
for _, line := range strings.Split(string(data), "\n") {
trimmed := strings.TrimSpace(line)
// Check for section headers
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inSection = (trimmed == target)
continue
}
if !inSection {
continue
}
// Skip comments and empty lines
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
continue
}
// Parse key = value
eqIdx := strings.Index(trimmed, "=")
if eqIdx < 0 {
continue
}
key := strings.TrimSpace(trimmed[:eqIdx])
val := strings.TrimSpace(trimmed[eqIdx+1:])
keys = append(keys, key)
vals = append(vals, val)
}
return keys, vals, nil
}
// SetValue updates a single key in a section within the INI content string.
// Returns the modified content. If the key doesn't exist, it's appended to the section.
func SetValue(content, section, key, value string) string {
lines := strings.Split(content, "\n")
target := "[" + section + "]"
inSection := false
found := false
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
// If we were in our target section and didn't find the key, insert before this line
if inSection && !found {
lines[i] = key + " = " + value + "\n" + line
found = true
}
inSection = (trimmed == target)
continue
}
if !inSection {
continue
}
// Check if this line matches our key
eqIdx := strings.Index(trimmed, "=")
if eqIdx < 0 {
continue
}
lineKey := strings.TrimSpace(trimmed[:eqIdx])
if lineKey == key {
lines[i] = key + " = " + value
found = true
}
}
// If key wasn't found and we're still in section (or section was last), append
if !found {
if inSection {
lines = append(lines, key+" = "+value)
}
}
return strings.Join(lines, "\n")
}
// GetValue reads a single value from a section.
func GetValue(path, section, key string) (string, error) {
keys, vals, err := GetSection(path, section)
if err != nil {
return "", err
}
for i, k := range keys {
if k == key {
return vals[i], nil
}
}
return "", fmt.Errorf("key %q not found in [%s]", key, section)
}

View File

@ -0,0 +1,826 @@
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ── View IDs ────────────────────────────────────────────────────────
type ViewID int
const (
ViewMain ViewID = iota
ViewDeps
ViewDepsInstall
ViewModules
ViewModuleToggle
ViewSettings
ViewSettingsSection
ViewSettingsEdit
ViewUsers
ViewUsersCreate
ViewUsersReset
ViewService
ViewDNS
ViewDNSBuild
ViewDNSManage
ViewDNSZones
ViewDNSZoneEdit
ViewDeploy
ViewConfirm
ViewResult
)
// ── Styles ──────────────────────────────────────────────────────────
var (
colorRed = lipgloss.Color("#ef4444")
colorGreen = lipgloss.Color("#22c55e")
colorYellow = lipgloss.Color("#eab308")
colorBlue = lipgloss.Color("#6366f1")
colorCyan = lipgloss.Color("#06b6d4")
colorMagenta = lipgloss.Color("#a855f7")
colorDim = lipgloss.Color("#6b7280")
colorWhite = lipgloss.Color("#f9fafb")
colorSurface = lipgloss.Color("#1e1e2e")
colorBorder = lipgloss.Color("#3b3b5c")
styleBanner = lipgloss.NewStyle().
Foreground(colorRed).
Bold(true)
styleTitle = lipgloss.NewStyle().
Foreground(colorCyan).
Bold(true).
PaddingLeft(2)
styleSubtitle = lipgloss.NewStyle().
Foreground(colorDim).
PaddingLeft(2)
styleMenuItem = lipgloss.NewStyle().
PaddingLeft(4)
styleSelected = lipgloss.NewStyle().
Foreground(colorBlue).
Bold(true).
PaddingLeft(2)
styleNormal = lipgloss.NewStyle().
Foreground(colorWhite).
PaddingLeft(4)
styleKey = lipgloss.NewStyle().
Foreground(colorCyan).
Bold(true)
styleSuccess = lipgloss.NewStyle().
Foreground(colorGreen)
styleError = lipgloss.NewStyle().
Foreground(colorRed)
styleWarning = lipgloss.NewStyle().
Foreground(colorYellow)
styleDim = lipgloss.NewStyle().
Foreground(colorDim)
styleStatusOK = lipgloss.NewStyle().
Foreground(colorGreen).
Bold(true)
styleStatusBad = lipgloss.NewStyle().
Foreground(colorRed).
Bold(true)
styleBox = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorBorder).
Padding(1, 2)
styleHR = lipgloss.NewStyle().
Foreground(colorDim)
)
// ── Menu Item ───────────────────────────────────────────────────────
type MenuItem struct {
Key string
Label string
Desc string
View ViewID
}
// ── Messages ────────────────────────────────────────────────────────
type ResultMsg struct {
Title string
Lines []string
IsError bool
}
type ConfirmMsg struct {
Prompt string
OnConfirm func() tea.Cmd
}
type OutputLineMsg string
type DoneMsg struct{ Err error }
// ── App Model ───────────────────────────────────────────────────────
type App struct {
width, height int
// Navigation
view ViewID
viewStack []ViewID
cursor int
// Main menu
mainMenu []MenuItem
// Dynamic content
listItems []ListItem
listTitle string
sectionKeys []string
// Settings
settingsSections []string
settingsSection string
settingsKeys []string
settingsVals []string
// Text input
textInput textinput.Model
inputLabel string
inputField string
inputs []textinput.Model
labels []string
focusIdx int
// Result / output
resultTitle string
resultLines []string
resultIsErr bool
outputLines []string
outputDone bool
outputCh chan tea.Msg
progressStep int
progressTotal int
progressLabel string
// Confirm
confirmPrompt string
confirmAction func() tea.Cmd
// Config path
autarchDir string
}
type ListItem struct {
Name string
Status string
Enabled bool
Extra string
}
func NewApp() App {
ti := textinput.New()
ti.CharLimit = 256
app := App{
view: ViewMain,
autarchDir: findAutarchDir(),
textInput: ti,
mainMenu: []MenuItem{
{Key: "1", Label: "Deploy AUTARCH", Desc: "Clone from GitHub, setup dirs, venv, deps, permissions, systemd", View: ViewDeploy},
{Key: "2", Label: "Dependencies", Desc: "Install & manage system packages, Python venv, pip, npm", View: ViewDeps},
{Key: "3", Label: "Modules", Desc: "List, enable, or disable AUTARCH Python modules", View: ViewModules},
{Key: "4", Label: "Settings", Desc: "Edit autarch_settings.conf (all 14+ sections)", View: ViewSettings},
{Key: "5", Label: "Users", Desc: "Create users, reset passwords, manage web credentials", View: ViewUsers},
{Key: "6", Label: "Services", Desc: "Start, stop, restart AUTARCH web & background daemons", View: ViewService},
{Key: "7", Label: "DNS Server", Desc: "Build, configure, and manage the AUTARCH DNS server", View: ViewDNS},
{Key: "q", Label: "Quit", Desc: "Exit the server manager", View: ViewMain},
},
}
return app
}
func (a App) Init() tea.Cmd {
return nil
}
// waitForOutput returns a Cmd that reads the next message from the output channel.
// This creates the streaming chain: OutputLineMsg → waitForOutput → OutputLineMsg → ...
func (a App) waitForOutput() tea.Cmd {
ch := a.outputCh
if ch == nil {
return nil
}
return func() tea.Msg {
msg, ok := <-ch
if !ok {
return DoneMsg{}
}
return msg
}
}
// ── Update ──────────────────────────────────────────────────────────
func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
a.width = msg.Width
a.height = msg.Height
return a, nil
case ResultMsg:
a.pushView(ViewResult)
a.resultTitle = msg.Title
a.resultLines = msg.Lines
a.resultIsErr = msg.IsError
return a, nil
case OutputLineMsg:
a.outputLines = append(a.outputLines, string(msg))
return a, a.waitForOutput()
case ProgressMsg:
a.progressStep = msg.Step
a.progressTotal = msg.Total
a.progressLabel = msg.Label
return a, a.waitForOutput()
case DoneMsg:
a.outputDone = true
a.outputCh = nil
if msg.Err != nil {
a.outputLines = append(a.outputLines, "", styleError.Render("Error: "+msg.Err.Error()))
}
a.outputLines = append(a.outputLines, "", styleDim.Render("Press any key to continue..."))
return a, nil
case depsLoadedMsg:
a.listItems = msg.items
return a, nil
case modulesLoadedMsg:
a.listItems = msg.items
return a, nil
case settingsLoadedMsg:
a.settingsSections = msg.sections
return a, nil
case dnsZonesMsg:
a.listItems = msg.items
return a, nil
case tea.KeyMsg:
return a.handleKey(msg)
}
// Update text inputs if active
if a.isInputView() {
return a.updateInputs(msg)
}
return a, nil
}
func (a App) isInputView() bool {
return a.view == ViewUsersCreate || a.view == ViewUsersReset ||
a.view == ViewSettingsEdit || a.view == ViewDNSZoneEdit
}
func (a App) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
// Global keys
switch key {
case "ctrl+c":
return a, tea.Quit
}
// Input views get special handling
if a.isInputView() {
return a.handleInputKey(msg)
}
// Output view (streaming)
if a.view == ViewDepsInstall || a.view == ViewDNSBuild {
if a.outputDone {
a.popView()
a.progressStep = 0
a.progressTotal = 0
a.progressLabel = ""
// Reload the parent view's data
switch a.view {
case ViewDeps:
return a, a.loadDepsStatus()
case ViewDNS:
return a, nil
case ViewDeploy:
return a, nil
}
}
return a, nil
}
// Result view
if a.view == ViewResult {
a.popView()
return a, nil
}
// Confirm view
if a.view == ViewConfirm {
switch key {
case "y", "Y":
if a.confirmAction != nil {
cmd := a.confirmAction()
a.popView()
return a, cmd
}
a.popView()
case "n", "N", "esc":
a.popView()
}
return a, nil
}
// List navigation
switch key {
case "up", "k":
if a.cursor > 0 {
a.cursor--
}
return a, nil
case "down", "j":
max := a.maxCursor()
if a.cursor < max {
a.cursor++
}
return a, nil
case "esc":
if len(a.viewStack) > 0 {
a.popView()
}
return a, nil
case "q":
if a.view == ViewMain {
return a, tea.Quit
}
if len(a.viewStack) > 0 {
a.popView()
return a, nil
}
return a, tea.Quit
}
// View-specific handling
switch a.view {
case ViewMain:
return a.handleMainMenu(key)
case ViewDeps:
return a.handleDepsMenu(key)
case ViewModules:
return a.handleModulesMenu(key)
case ViewModuleToggle:
return a.handleModuleToggle(key)
case ViewSettings:
return a.handleSettingsMenu(key)
case ViewSettingsSection:
return a.handleSettingsSection(key)
case ViewUsers:
return a.handleUsersMenu(key)
case ViewService:
return a.handleServiceMenu(key)
case ViewDeploy:
return a.handleDeployMenu(key)
case ViewDNS:
return a.handleDNSMenu(key)
case ViewDNSManage:
return a.handleDNSManageMenu(key)
case ViewDNSZones:
return a.handleDNSZonesMenu(key)
}
return a, nil
}
func (a App) maxCursor() int {
switch a.view {
case ViewMain:
return len(a.mainMenu) - 1
case ViewModules, ViewModuleToggle:
return len(a.listItems) - 1
case ViewSettings:
return len(a.settingsSections) - 1
case ViewSettingsSection:
return len(a.settingsKeys) - 1
case ViewDNSZones:
return len(a.listItems) - 1
}
return 0
}
// ── Navigation ──────────────────────────────────────────────────────
func (a *App) pushView(v ViewID) {
a.viewStack = append(a.viewStack, a.view)
a.view = v
a.cursor = 0
}
func (a *App) popView() {
if len(a.viewStack) > 0 {
a.view = a.viewStack[len(a.viewStack)-1]
a.viewStack = a.viewStack[:len(a.viewStack)-1]
a.cursor = 0
}
}
// ── View Rendering ──────────────────────────────────────────────────
func (a App) View() string {
var b strings.Builder
b.WriteString(a.renderBanner())
b.WriteString("\n")
switch a.view {
case ViewMain:
b.WriteString(a.renderMainMenu())
case ViewDeploy:
b.WriteString(a.renderDeployMenu())
case ViewDeps:
b.WriteString(a.renderDepsMenu())
case ViewDepsInstall:
b.WriteString(a.renderOutput("Installing Dependencies"))
case ViewModules:
b.WriteString(a.renderModulesList())
case ViewModuleToggle:
b.WriteString(a.renderModulesList())
case ViewSettings:
b.WriteString(a.renderSettingsSections())
case ViewSettingsSection:
b.WriteString(a.renderSettingsKeys())
case ViewSettingsEdit:
b.WriteString(a.renderSettingsEditForm())
case ViewUsers:
b.WriteString(a.renderUsersMenu())
case ViewUsersCreate:
b.WriteString(a.renderUserForm("Create New User"))
case ViewUsersReset:
b.WriteString(a.renderUserForm("Reset Password"))
case ViewService:
b.WriteString(a.renderServiceMenu())
case ViewDNS:
b.WriteString(a.renderDNSMenu())
case ViewDNSBuild:
b.WriteString(a.renderOutput("Building DNS Server"))
case ViewDNSManage:
b.WriteString(a.renderDNSManageMenu())
case ViewDNSZones:
b.WriteString(a.renderDNSZones())
case ViewDNSZoneEdit:
b.WriteString(a.renderDNSZoneForm())
case ViewConfirm:
b.WriteString(a.renderConfirm())
case ViewResult:
b.WriteString(a.renderResult())
}
b.WriteString("\n")
b.WriteString(a.renderStatusBar())
return b.String()
}
// ── Banner ──────────────────────────────────────────────────────────
func (a App) renderBanner() string {
banner := `
`
title := lipgloss.NewStyle().
Foreground(colorCyan).
Bold(true).
Align(lipgloss.Center).
Render("S E R V E R M A N A G E R v1.0")
sub := styleDim.Render(" darkHal Security Group & Setec Security Labs")
// Live service status bar
statusLine := a.renderServiceStatusBar()
return styleBanner.Render(banner) + "\n" + title + "\n" + sub + "\n" + statusLine + "\n"
}
func (a App) renderServiceStatusBar() string {
webStatus, webUp := getProcessStatus("autarch-web", "autarch_web.py")
dnsStatus, dnsUp := getProcessStatus("autarch-dns", "autarch-dns")
webInd := styleStatusBad.Render("○")
if webUp {
webInd = styleStatusOK.Render("●")
}
dnsInd := styleStatusBad.Render("○")
if dnsUp {
dnsInd = styleStatusOK.Render("●")
}
_ = webStatus
_ = dnsStatus
return styleDim.Render(" ") +
webInd + styleDim.Render(" Web ") +
dnsInd + styleDim.Render(" DNS")
}
func (a App) renderHR() string {
w := a.width
if w < 10 {
w = 66
}
if w > 80 {
w = 80
}
return styleHR.Render(strings.Repeat("─", w-4)) + "\n"
}
// ── Main Menu ───────────────────────────────────────────────────────
func (a App) renderMainMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("MAIN MENU"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for i, item := range a.mainMenu {
cursor := " "
if i == a.cursor {
cursor = styleSelected.Render("▸ ")
label := styleKey.Render("["+item.Key+"]") + " " +
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(item.Label)
desc := styleDim.Render(" " + item.Desc)
b.WriteString(cursor + label + "\n")
b.WriteString(" " + desc + "\n")
} else {
label := styleDim.Render("["+item.Key+"]") + " " +
lipgloss.NewStyle().Foreground(colorWhite).Render(item.Label)
b.WriteString(cursor + " " + label + "\n")
}
}
return b.String()
}
// ── Confirm ─────────────────────────────────────────────────────────
func (a App) renderConfirm() string {
var b strings.Builder
b.WriteString(styleTitle.Render("CONFIRM"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleWarning.Render(" " + a.confirmPrompt))
b.WriteString("\n\n")
b.WriteString(styleDim.Render(" [Y] Yes [N] No"))
b.WriteString("\n")
return b.String()
}
// ── Result ──────────────────────────────────────────────────────────
func (a App) renderResult() string {
var b strings.Builder
title := a.resultTitle
if a.resultIsErr {
b.WriteString(styleError.Render(" " + title))
} else {
b.WriteString(styleSuccess.Render(" " + title))
}
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for _, line := range a.resultLines {
b.WriteString(" " + line + "\n")
}
b.WriteString("\n")
b.WriteString(styleDim.Render(" Press any key to continue..."))
b.WriteString("\n")
return b.String()
}
// ── Streaming Output ────────────────────────────────────────────────
func (a App) renderOutput(title string) string {
var b strings.Builder
b.WriteString(styleTitle.Render(title))
b.WriteString("\n")
b.WriteString(a.renderHR())
// Progress bar
if a.progressTotal > 0 && !a.outputDone {
pct := float64(a.progressStep) / float64(a.progressTotal)
barWidth := 40
filled := int(pct * float64(barWidth))
if filled > barWidth {
filled = barWidth
}
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
pctStr := fmt.Sprintf("%3.0f%%", pct*100)
b.WriteString(" " + styleKey.Render("["+bar+"]") + " " +
styleWarning.Render(pctStr) + " " +
styleDim.Render(fmt.Sprintf("Step %d/%d: %s", a.progressStep, a.progressTotal, a.progressLabel)))
b.WriteString("\n")
b.WriteString(a.renderHR())
}
b.WriteString("\n")
// Show last N lines that fit the screen
maxLines := a.height - 22
if maxLines < 10 {
maxLines = 20
}
start := 0
if len(a.outputLines) > maxLines {
start = len(a.outputLines) - maxLines
}
for _, line := range a.outputLines[start:] {
b.WriteString(" " + line + "\n")
}
if !a.outputDone {
b.WriteString("\n")
b.WriteString(styleDim.Render(" Working..."))
}
return b.String()
}
// ── Status Bar ──────────────────────────────────────────────────────
func (a App) renderStatusBar() string {
nav := styleDim.Render(" ↑↓ navigate")
esc := styleDim.Render(" esc back")
quit := styleDim.Render(" q quit")
path := ""
for _, v := range a.viewStack {
path += viewName(v) + " > "
}
path += viewName(a.view)
left := styleDim.Render(" " + path)
right := nav + esc + quit
gap := a.width - lipgloss.Width(left) - lipgloss.Width(right)
if gap < 1 {
gap = 1
}
return "\n" + styleHR.Render(strings.Repeat("─", clamp(a.width-4, 20, 80))) + "\n" +
left + strings.Repeat(" ", gap) + right + "\n"
}
func viewName(v ViewID) string {
names := map[ViewID]string{
ViewMain: "Main",
ViewDeps: "Dependencies",
ViewDepsInstall: "Install",
ViewModules: "Modules",
ViewModuleToggle: "Toggle",
ViewSettings: "Settings",
ViewSettingsSection: "Section",
ViewSettingsEdit: "Edit",
ViewUsers: "Users",
ViewUsersCreate: "Create",
ViewUsersReset: "Reset",
ViewService: "Services",
ViewDNS: "DNS",
ViewDNSBuild: "Build",
ViewDNSManage: "Manage",
ViewDNSZones: "Zones",
ViewDeploy: "Deploy",
ViewDNSZoneEdit: "Edit Zone",
ViewConfirm: "Confirm",
ViewResult: "Result",
}
if n, ok := names[v]; ok {
return n
}
return "?"
}
// ── User Form Rendering ─────────────────────────────────────────────
func (a App) renderUserForm(title string) string {
var b strings.Builder
b.WriteString(styleTitle.Render(title))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for i, label := range a.labels {
prefix := " "
if i == a.focusIdx {
prefix = styleSelected.Render("▸ ")
}
b.WriteString(prefix + styleDim.Render(label+": "))
b.WriteString(a.inputs[i].View())
b.WriteString("\n\n")
}
b.WriteString("\n")
b.WriteString(styleDim.Render(" tab next field | enter submit | esc cancel"))
b.WriteString("\n")
return b.String()
}
// ── Settings Edit Form ──────────────────────────────────────────────
func (a App) renderSettingsEditForm() string {
var b strings.Builder
b.WriteString(styleTitle.Render(fmt.Sprintf("Edit [%s]", a.settingsSection)))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for i, label := range a.labels {
prefix := " "
if i == a.focusIdx {
prefix = styleSelected.Render("▸ ")
}
b.WriteString(prefix + styleKey.Render(label) + " = ")
b.WriteString(a.inputs[i].View())
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(styleDim.Render(" tab next | enter save all | esc cancel"))
b.WriteString("\n")
return b.String()
}
// ── DNS Zone Form ───────────────────────────────────────────────────
func (a App) renderDNSZoneForm() string {
var b strings.Builder
b.WriteString(styleTitle.Render("Create DNS Zone"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for i, label := range a.labels {
prefix := " "
if i == a.focusIdx {
prefix = styleSelected.Render("▸ ")
}
b.WriteString(prefix + styleDim.Render(label+": "))
b.WriteString(a.inputs[i].View())
b.WriteString("\n\n")
}
b.WriteString("\n")
b.WriteString(styleDim.Render(" tab next field | enter submit | esc cancel"))
b.WriteString("\n")
return b.String()
}
// ── Helpers ─────────────────────────────────────────────────────────
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}

View File

@ -0,0 +1,48 @@
package tui
import (
"os"
"path/filepath"
)
// findAutarchDir walks up from the server-manager binary location to find
// the AUTARCH project root (identified by autarch_settings.conf).
func findAutarchDir() string {
// Try well-known paths first
candidates := []string{
"/opt/autarch",
"/srv/autarch",
"/home/autarch",
}
// Also try relative to the executable
exe, err := os.Executable()
if err == nil {
dir := filepath.Dir(exe)
// services/server-manager/ → ../../
candidates = append([]string{
filepath.Join(dir, "..", ".."),
filepath.Join(dir, ".."),
dir,
}, candidates...)
}
// Also check cwd
if cwd, err := os.Getwd(); err == nil {
candidates = append([]string{cwd, filepath.Join(cwd, "..", "..")}, candidates...)
}
for _, c := range candidates {
abs, err := filepath.Abs(c)
if err != nil {
continue
}
conf := filepath.Join(abs, "autarch_settings.conf")
if _, err := os.Stat(conf); err == nil {
return abs
}
}
// Fallback
return "/opt/autarch"
}

View File

@ -0,0 +1,99 @@
package tui
import (
"os"
tea "github.com/charmbracelet/bubbletea"
)
// ── Input View Handling ─────────────────────────────────────────────
func (a App) handleInputKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
switch key {
case "esc":
a.popView()
return a, nil
case "tab", "shift+tab":
// Cycle focus
if key == "tab" {
a.focusIdx = (a.focusIdx + 1) % len(a.inputs)
} else {
a.focusIdx = (a.focusIdx - 1 + len(a.inputs)) % len(a.inputs)
}
for i := range a.inputs {
if i == a.focusIdx {
a.inputs[i].Focus()
} else {
a.inputs[i].Blur()
}
}
return a, nil
case "enter":
// If not on last field, advance
if a.focusIdx < len(a.inputs)-1 {
a.focusIdx++
for i := range a.inputs {
if i == a.focusIdx {
a.inputs[i].Focus()
} else {
a.inputs[i].Blur()
}
}
return a, nil
}
// Submit
switch a.view {
case ViewUsersCreate:
return a.submitUserCreate()
case ViewUsersReset:
return a.submitUserReset()
case ViewSettingsEdit:
return a.saveSettings()
case ViewDNSZoneEdit:
return a.submitDNSZone()
}
return a, nil
}
// Forward key to focused input
if a.focusIdx >= 0 && a.focusIdx < len(a.inputs) {
var cmd tea.Cmd
a.inputs[a.focusIdx], cmd = a.inputs[a.focusIdx].Update(msg)
return a, cmd
}
return a, nil
}
func (a App) updateInputs(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.focusIdx >= 0 && a.focusIdx < len(a.inputs) {
var cmd tea.Cmd
a.inputs[a.focusIdx], cmd = a.inputs[a.focusIdx].Update(msg)
return a, cmd
}
return a, nil
}
// ── File Helpers (used by multiple views) ────────────────────────────
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func readFileBytes(path string) ([]byte, error) {
return os.ReadFile(path)
}
func writeFile(path string, data []byte, perm os.FileMode) error {
return os.WriteFile(path, data, perm)
}
func renameFile(src, dst string) error {
return os.Rename(src, dst)
}

View File

@ -0,0 +1,161 @@
package tui
import (
"bufio"
"fmt"
"os/exec"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
)
// ── Streaming Messages ──────────────────────────────────────────────
// ProgressMsg updates the progress bar in the output view.
type ProgressMsg struct {
Step int
Total int
Label string
}
// ── Step Definition ─────────────────────────────────────────────────
// CmdStep defines a single command to run in a streaming sequence.
type CmdStep struct {
Label string // Human-readable label (shown in output)
Args []string // Command + arguments
Dir string // Working directory (empty = inherit)
}
// ── Streaming Execution Engine ──────────────────────────────────────
// streamSteps runs a sequence of CmdSteps, sending OutputLineMsg per line
// and ProgressMsg per step, then DoneMsg when finished.
// It writes to a buffered channel that the TUI reads via waitForOutput().
func streamSteps(ch chan<- tea.Msg, steps []CmdStep) {
defer close(ch)
total := len(steps)
var errors []string
for i, step := range steps {
// Send progress update
ch <- ProgressMsg{
Step: i + 1,
Total: total,
Label: step.Label,
}
// Show command being executed
cmdStr := strings.Join(step.Args, " ")
ch <- OutputLineMsg(styleKey.Render(fmt.Sprintf("═══ [%d/%d] %s ═══", i+1, total, step.Label)))
ch <- OutputLineMsg(styleDim.Render(" $ " + cmdStr))
// Build command
cmd := exec.Command(step.Args[0], step.Args[1:]...)
if step.Dir != "" {
cmd.Dir = step.Dir
}
// Get pipes for real-time output
stdout, err := cmd.StdoutPipe()
if err != nil {
ch <- OutputLineMsg(styleError.Render(" Failed to create stdout pipe: " + err.Error()))
errors = append(errors, step.Label+": "+err.Error())
continue
}
cmd.Stderr = cmd.Stdout // merge stderr into stdout
// Start command
startTime := time.Now()
if err := cmd.Start(); err != nil {
ch <- OutputLineMsg(styleError.Render(" Failed to start: " + err.Error()))
errors = append(errors, step.Label+": "+err.Error())
continue
}
// Read output line by line
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 64*1024), 256*1024) // handle long lines
lineCount := 0
for scanner.Scan() {
line := scanner.Text()
lineCount++
// Parse apt/pip progress indicators for speed display
if parsed := parseProgressLine(line); parsed != "" {
ch <- OutputLineMsg(" " + parsed)
} else {
// Throttle verbose output: show every line for first 30,
// then every 5th line, but always show errors
if lineCount <= 30 || lineCount%5 == 0 || isErrorLine(line) {
ch <- OutputLineMsg(" " + line)
}
}
}
// Wait for command to finish
err = cmd.Wait()
elapsed := time.Since(startTime)
if err != nil {
ch <- OutputLineMsg(styleError.Render(fmt.Sprintf(" ✘ Failed (%s): %s", elapsed.Round(time.Millisecond), err.Error())))
errors = append(errors, step.Label+": "+err.Error())
} else {
ch <- OutputLineMsg(styleSuccess.Render(fmt.Sprintf(" ✔ Done (%s)", elapsed.Round(time.Millisecond))))
}
ch <- OutputLineMsg("")
}
// Final summary
if len(errors) > 0 {
ch <- OutputLineMsg(styleWarning.Render(fmt.Sprintf("═══ Completed with %d error(s) ═══", len(errors))))
for _, e := range errors {
ch <- OutputLineMsg(styleError.Render(" ✘ " + e))
}
ch <- DoneMsg{Err: fmt.Errorf("%d step(s) failed", len(errors))}
} else {
ch <- OutputLineMsg(styleSuccess.Render("═══ All steps completed successfully ═══"))
ch <- DoneMsg{}
}
}
// ── Progress Parsing ────────────────────────────────────────────────
// parseProgressLine extracts progress info from apt/pip/npm output.
func parseProgressLine(line string) string {
// apt progress: "Progress: [ 45%]" or percentage patterns
if strings.Contains(line, "Progress:") || strings.Contains(line, "progress:") {
return styleWarning.Render(strings.TrimSpace(line))
}
// pip: "Downloading foo-1.2.3.whl (2.3 MB)" or "Installing collected packages:"
if strings.HasPrefix(line, "Downloading ") || strings.HasPrefix(line, "Collecting ") {
return styleCyan.Render(strings.TrimSpace(line))
}
if strings.HasPrefix(line, "Installing collected packages:") {
return styleWarning.Render(strings.TrimSpace(line))
}
// npm: "added X packages"
if strings.Contains(line, "added") && strings.Contains(line, "packages") {
return styleSuccess.Render(strings.TrimSpace(line))
}
return ""
}
// isErrorLine checks if an output line looks like an error.
func isErrorLine(line string) bool {
lower := strings.ToLower(line)
return strings.Contains(lower, "error") ||
strings.Contains(lower, "failed") ||
strings.Contains(lower, "fatal") ||
strings.Contains(lower, "warning") ||
strings.Contains(lower, "unable to")
}
// ── Style for progress lines ────────────────────────────────────────
var styleCyan = styleKey // reuse existing cyan style

View File

@ -0,0 +1,493 @@
package tui
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
autarchGitRepo = "https://github.com/DigijEth/autarch.git"
autarchBranch = "main"
defaultInstDir = "/opt/autarch"
)
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderDeployMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DEPLOY AUTARCH"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
installDir := defaultInstDir
if a.autarchDir != "" && a.autarchDir != defaultInstDir {
installDir = a.autarchDir
}
// Check current state
confExists := fileExists(filepath.Join(installDir, "autarch_settings.conf"))
gitExists := fileExists(filepath.Join(installDir, ".git"))
venvExists := fileExists(filepath.Join(installDir, "venv", "bin", "python3"))
b.WriteString(styleKey.Render(" Install directory: ") +
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(installDir))
b.WriteString("\n")
b.WriteString(styleKey.Render(" Git repository: ") +
styleDim.Render(autarchGitRepo))
b.WriteString("\n\n")
// Status checks
if gitExists {
// Get current commit
out, _ := exec.Command("git", "-C", installDir, "log", "--oneline", "-1").Output()
commit := strings.TrimSpace(string(out))
b.WriteString(" " + styleStatusOK.Render("✔ Git repo present") + " " + styleDim.Render(commit))
} else {
b.WriteString(" " + styleStatusBad.Render("✘ Not cloned"))
}
b.WriteString("\n")
if confExists {
b.WriteString(" " + styleStatusOK.Render("✔ Config file present"))
} else {
b.WriteString(" " + styleStatusBad.Render("✘ No config file"))
}
b.WriteString("\n")
if venvExists {
// Count pip packages
out, _ := exec.Command(filepath.Join(installDir, "venv", "bin", "pip3"), "list", "--format=columns").Output()
count := strings.Count(string(out), "\n") - 2
if count < 0 {
count = 0
}
b.WriteString(" " + styleStatusOK.Render(fmt.Sprintf("✔ Python venv (%d packages)", count)))
} else {
b.WriteString(" " + styleStatusBad.Render("✘ No Python venv"))
}
b.WriteString("\n")
// Check node_modules
nodeExists := fileExists(filepath.Join(installDir, "node_modules"))
if nodeExists {
b.WriteString(" " + styleStatusOK.Render("✔ Node modules installed"))
} else {
b.WriteString(" " + styleStatusBad.Render("✘ No node_modules"))
}
b.WriteString("\n")
// Check services
_, webUp := getProcessStatus("autarch-web", "autarch_web.py")
_, dnsUp := getProcessStatus("autarch-dns", "autarch-dns")
if webUp {
b.WriteString(" " + styleStatusOK.Render("✔ Web service running"))
} else {
b.WriteString(" " + styleStatusBad.Render("○ Web service stopped"))
}
b.WriteString("\n")
if dnsUp {
b.WriteString(" " + styleStatusOK.Render("✔ DNS service running"))
} else {
b.WriteString(" " + styleStatusBad.Render("○ DNS service stopped"))
}
b.WriteString("\n\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if !gitExists {
b.WriteString(styleKey.Render(" [c]") + " Clone AUTARCH from GitHub " + styleDim.Render("(full install)") + "\n")
} else {
b.WriteString(styleKey.Render(" [u]") + " Update (git pull + reinstall deps)\n")
}
b.WriteString(styleKey.Render(" [f]") + " Full setup " + styleDim.Render("(clone/pull + venv + pip + npm + build + systemd + permissions)") + "\n")
b.WriteString(styleKey.Render(" [v]") + " Setup venv + pip install only\n")
b.WriteString(styleKey.Render(" [n]") + " Setup npm + build hardware JS only\n")
b.WriteString(styleKey.Render(" [p]") + " Fix permissions " + styleDim.Render("(chown/chmod)") + "\n")
b.WriteString(styleKey.Render(" [s]") + " Install systemd service units\n")
b.WriteString(styleKey.Render(" [d]") + " Build DNS server from source\n")
b.WriteString(styleKey.Render(" [g]") + " Generate self-signed TLS cert\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleDeployMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "c":
return a.deployClone()
case "u":
return a.deployUpdate()
case "f":
return a.deployFull()
case "v":
return a.deployVenv()
case "n":
return a.deployNpm()
case "p":
return a.deployPermissions()
case "s":
return a.deploySystemd()
case "d":
return a.deployDNSBuild()
case "g":
return a.deployTLSCert()
}
return a, nil
}
// ── Deploy Commands ─────────────────────────────────────────────────
func (a App) deployClone() (App, tea.Cmd) {
dir := defaultInstDir
// Quick check — if already cloned, show result without streaming
if fileExists(filepath.Join(dir, ".git")) {
return a, func() tea.Msg {
return ResultMsg{
Title: "Already Cloned",
Lines: []string{"AUTARCH is already cloned at " + dir, "", "Use [u] to update or [f] for full setup."},
IsError: false,
}
}
}
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
ch := make(chan tea.Msg, 256)
a.outputCh = ch
go func() {
os.MkdirAll(filepath.Dir(dir), 0755)
steps := []CmdStep{
{Label: "Clone AUTARCH from GitHub", Args: []string{"git", "clone", "--branch", autarchBranch, "--progress", autarchGitRepo, dir}},
}
streamSteps(ch, steps)
}()
return a, a.waitForOutput()
}
func (a App) deployUpdate() (App, tea.Cmd) {
return a, func() tea.Msg {
dir := defaultInstDir
if a.autarchDir != "" {
dir = a.autarchDir
}
var lines []string
// Git pull
lines = append(lines, styleKey.Render("$ git -C "+dir+" pull"))
cmd := exec.Command("git", "-C", dir, "pull", "--ff-only")
out, err := cmd.CombinedOutput()
for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") {
lines = append(lines, " "+l)
}
if err != nil {
lines = append(lines, styleError.Render(" ✘ Pull failed: "+err.Error()))
return ResultMsg{Title: "Update Failed", Lines: lines, IsError: true}
}
lines = append(lines, styleSuccess.Render(" ✔ Updated"))
return ResultMsg{Title: "AUTARCH Updated", Lines: lines}
}
}
func (a App) deployFull() (App, tea.Cmd) {
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
ch := make(chan tea.Msg, 256)
a.outputCh = ch
go func() {
dir := defaultInstDir
var steps []CmdStep
// Step 1: Clone or pull
if !fileExists(filepath.Join(dir, ".git")) {
os.MkdirAll(filepath.Dir(dir), 0755)
steps = append(steps, CmdStep{
Label: "Clone AUTARCH from GitHub",
Args: []string{"git", "clone", "--branch", autarchBranch, "--progress", autarchGitRepo, dir},
})
} else {
steps = append(steps, CmdStep{
Label: "Update from GitHub",
Args: []string{"git", "-C", dir, "pull", "--ff-only"},
})
}
// Step 2: System deps
steps = append(steps, CmdStep{
Label: "Update package lists",
Args: []string{"apt-get", "update", "-qq"},
})
aptPkgs := []string{
"python3", "python3-pip", "python3-venv", "python3-dev",
"build-essential", "cmake", "pkg-config",
"git", "curl", "wget", "openssl",
"libffi-dev", "libssl-dev", "libpcap-dev", "libxml2-dev", "libxslt1-dev",
"nmap", "tshark", "whois", "dnsutils",
"adb", "fastboot",
"wireguard-tools", "miniupnpc", "net-tools",
"nodejs", "npm", "ffmpeg",
}
steps = append(steps, CmdStep{
Label: "Install system dependencies",
Args: append([]string{"apt-get", "install", "-y"}, aptPkgs...),
})
// Step 3: System user
steps = append(steps, CmdStep{
Label: "Create autarch system user",
Args: []string{"useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "autarch"},
})
// Step 4-5: Python venv + pip
venv := filepath.Join(dir, "venv")
pip := filepath.Join(venv, "bin", "pip3")
steps = append(steps,
CmdStep{Label: "Create Python virtual environment", Args: []string{"python3", "-m", "venv", venv}},
CmdStep{Label: "Upgrade pip, setuptools, wheel", Args: []string{pip, "install", "--upgrade", "pip", "setuptools", "wheel"}},
CmdStep{Label: "Install Python packages", Args: []string{pip, "install", "-r", filepath.Join(dir, "requirements.txt")}},
)
// Step 6: npm
steps = append(steps,
CmdStep{Label: "Install npm packages", Args: []string{"npm", "install"}, Dir: dir},
)
if fileExists(filepath.Join(dir, "scripts", "build-hw-libs.sh")) {
steps = append(steps, CmdStep{
Label: "Build hardware JS bundles",
Args: []string{"bash", "scripts/build-hw-libs.sh"},
Dir: dir,
})
}
// Step 7: Permissions
steps = append(steps,
CmdStep{Label: "Set ownership", Args: []string{"chown", "-R", "root:root", dir}},
CmdStep{Label: "Set permissions", Args: []string{"chmod", "-R", "755", dir}},
)
// Step 8: Data directories (quick inline, not a CmdStep)
dataDirs := []string{"data", "data/certs", "data/dns", "results", "dossiers", "models"}
for _, d := range dataDirs {
os.MkdirAll(filepath.Join(dir, d), 0755)
}
// Step 9: Sensitive file permissions
steps = append(steps,
CmdStep{Label: "Secure config file", Args: []string{"chmod", "600", filepath.Join(dir, "autarch_settings.conf")}},
)
// Step 10: TLS cert
certDir := filepath.Join(dir, "data", "certs")
certPath := filepath.Join(certDir, "autarch.crt")
keyPath := filepath.Join(certDir, "autarch.key")
if !fileExists(certPath) || !fileExists(keyPath) {
steps = append(steps, CmdStep{
Label: "Generate self-signed TLS certificate",
Args: []string{"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", keyPath, "-out", certPath,
"-days", "3650", "-nodes",
"-subj", "/CN=AUTARCH/O=darkHal"},
})
}
// Step 11: Systemd units — write files inline then reload
writeSystemdUnits(dir)
steps = append(steps, CmdStep{
Label: "Reload systemd daemon",
Args: []string{"systemctl", "daemon-reload"},
})
streamSteps(ch, steps)
}()
return a, a.waitForOutput()
}
func (a App) deployVenv() (App, tea.Cmd) {
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
ch := make(chan tea.Msg, 256)
a.outputCh = ch
dir := resolveDir(a.autarchDir)
go func() {
streamSteps(ch, buildVenvSteps(dir))
}()
return a, a.waitForOutput()
}
func (a App) deployNpm() (App, tea.Cmd) {
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
ch := make(chan tea.Msg, 256)
a.outputCh = ch
dir := resolveDir(a.autarchDir)
go func() {
streamSteps(ch, buildNpmSteps(dir))
}()
return a, a.waitForOutput()
}
func (a App) deployPermissions() (App, tea.Cmd) {
return a, func() tea.Msg {
dir := resolveDir(a.autarchDir)
var lines []string
exec.Command("chown", "-R", "root:root", dir).Run()
lines = append(lines, styleSuccess.Render("✔ chown -R root:root "+dir))
exec.Command("chmod", "-R", "755", dir).Run()
lines = append(lines, styleSuccess.Render("✔ chmod -R 755 "+dir))
// Sensitive files
confPath := filepath.Join(dir, "autarch_settings.conf")
if fileExists(confPath) {
exec.Command("chmod", "600", confPath).Run()
lines = append(lines, styleSuccess.Render("✔ chmod 600 autarch_settings.conf"))
}
credPath := filepath.Join(dir, "data", "web_credentials.json")
if fileExists(credPath) {
exec.Command("chmod", "600", credPath).Run()
lines = append(lines, styleSuccess.Render("✔ chmod 600 web_credentials.json"))
}
// Ensure data dirs exist
for _, d := range []string{"data", "data/certs", "data/dns", "results", "dossiers", "models"} {
os.MkdirAll(filepath.Join(dir, d), 0755)
}
lines = append(lines, styleSuccess.Render("✔ Data directories created"))
return ResultMsg{Title: "Permissions Fixed", Lines: lines}
}
}
func (a App) deploySystemd() (App, tea.Cmd) {
// Reuse the existing installServiceUnits
return a.installServiceUnits()
}
func (a App) deployDNSBuild() (App, tea.Cmd) {
return a.buildDNSServer()
}
func (a App) deployTLSCert() (App, tea.Cmd) {
return a, func() tea.Msg {
dir := resolveDir(a.autarchDir)
certDir := filepath.Join(dir, "data", "certs")
os.MkdirAll(certDir, 0755)
certPath := filepath.Join(certDir, "autarch.crt")
keyPath := filepath.Join(certDir, "autarch.key")
cmd := exec.Command("openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", keyPath, "-out", certPath,
"-days", "3650", "-nodes",
"-subj", "/CN=AUTARCH/O=darkHal Security Group")
out, err := cmd.CombinedOutput()
if err != nil {
return ResultMsg{
Title: "Error",
Lines: []string{string(out), err.Error()},
IsError: true,
}
}
return ResultMsg{
Title: "TLS Certificate Generated",
Lines: []string{
styleSuccess.Render("✔ Certificate: ") + certPath,
styleSuccess.Render("✔ Private key: ") + keyPath,
"",
styleDim.Render("Valid for 10 years. Self-signed."),
styleDim.Render("For production, use Let's Encrypt via Setec Manager."),
},
}
}
}
// ── Helpers ─────────────────────────────────────────────────────────
func resolveDir(autarchDir string) string {
if autarchDir != "" {
return autarchDir
}
return defaultInstDir
}
func writeSystemdUnits(dir string) {
units := map[string]string{
"autarch-web.service": fmt.Sprintf(`[Unit]
Description=AUTARCH Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/venv/bin/python3 %s/autarch_web.py
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
`, dir, dir, dir),
"autarch-dns.service": fmt.Sprintf(`[Unit]
Description=AUTARCH DNS Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/services/dns-server/autarch-dns --config %s/data/dns/config.json
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`, dir, dir, dir),
}
for name, content := range units {
path := "/etc/systemd/system/" + name
os.WriteFile(path, []byte(content), 0644)
}
}

View File

@ -0,0 +1,416 @@
package tui
import (
"fmt"
"os/exec"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
// ── Dependency Categories ───────────────────────────────────────────
type depCheck struct {
Name string
Cmd string // command to check existence
Pkg string // apt package name
Kind string // "system", "python", "npm"
Desc string
}
var systemDeps = []depCheck{
// Core runtime
{"python3", "python3", "python3", "system", "Python 3.10+ interpreter"},
{"pip", "pip3", "python3-pip", "system", "Python package manager"},
{"python3-venv", "python3 -m venv --help", "python3-venv", "system", "Python virtual environments"},
{"python3-dev", "python3-config --includes", "python3-dev", "system", "Python C headers (for native extensions)"},
// Build tools
{"gcc", "gcc", "build-essential", "system", "C/C++ compiler toolchain"},
{"cmake", "cmake", "cmake", "system", "CMake build system (for llama-cpp)"},
{"pkg-config", "pkg-config", "pkg-config", "system", "Package config helper"},
// Core system utilities
{"git", "git", "git", "system", "Version control"},
{"curl", "curl", "curl", "system", "HTTP client"},
{"wget", "wget", "wget", "system", "File downloader"},
{"openssl", "openssl", "openssl", "system", "TLS/crypto toolkit"},
// C libraries for Python packages
{"libffi-dev", "pkg-config --exists libffi", "libffi-dev", "system", "FFI library (for cffi/cryptography)"},
{"libssl-dev", "pkg-config --exists openssl", "libssl-dev", "system", "OpenSSL headers (for cryptography)"},
{"libpcap-dev", "pkg-config --exists libpcap", "libpcap-dev", "system", "Packet capture headers (for scapy)"},
{"libxml2-dev", "pkg-config --exists libxml-2.0", "libxml2-dev", "system", "XML parser headers (for lxml)"},
{"libxslt1-dev", "pkg-config --exists libxslt", "libxslt1-dev", "system", "XSLT headers (for lxml)"},
// Security tools
{"nmap", "nmap", "nmap", "system", "Network scanner"},
{"tshark", "tshark", "tshark", "system", "Packet analysis (Wireshark CLI)"},
{"whois", "whois", "whois", "system", "WHOIS lookup"},
{"dnsutils", "dig", "dnsutils", "system", "DNS utilities (dig, nslookup)"},
// Android tools
{"adb", "adb", "adb", "system", "Android Debug Bridge"},
{"fastboot", "fastboot", "fastboot", "system", "Android Fastboot"},
// Network tools
{"wg", "wg", "wireguard-tools", "system", "WireGuard VPN tools"},
{"upnpc", "upnpc", "miniupnpc", "system", "UPnP port mapping client"},
{"net-tools", "ifconfig", "net-tools", "system", "Network utilities (ifconfig)"},
// Node.js
{"node", "node", "nodejs", "system", "Node.js (for hardware WebUSB libs)"},
{"npm", "npm", "npm", "system", "Node package manager"},
// Go
{"go", "go", "golang", "system", "Go compiler (for DNS server build)"},
// Media / misc
{"ffmpeg", "ffmpeg", "ffmpeg", "system", "Media processing"},
}
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderDepsMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DEPENDENCIES"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if len(a.listItems) == 0 {
b.WriteString(styleDim.Render(" Loading..."))
b.WriteString("\n")
return b.String()
}
// Count installed vs total
installed, total := 0, 0
for _, item := range a.listItems {
if item.Extra == "system" {
total++
if item.Enabled {
installed++
}
}
}
// System packages
b.WriteString(styleKey.Render(fmt.Sprintf(" System Packages (%d/%d installed)", installed, total)))
b.WriteString("\n\n")
for _, item := range a.listItems {
if item.Extra != "system" {
continue
}
status := styleStatusOK.Render("✔ installed")
if !item.Enabled {
status = styleStatusBad.Render("✘ missing ")
}
b.WriteString(fmt.Sprintf(" %s %-14s %s\n", status, item.Name, styleDim.Render(item.Status)))
}
// Python venv
b.WriteString("\n")
b.WriteString(styleKey.Render(" Python Virtual Environment"))
b.WriteString("\n\n")
for _, item := range a.listItems {
if item.Extra != "venv" {
continue
}
status := styleStatusOK.Render("✔ ready ")
if !item.Enabled {
status = styleStatusBad.Render("✘ missing")
}
b.WriteString(fmt.Sprintf(" %s %s\n", status, item.Name))
}
// Actions
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [a]") + " Install all missing system packages\n")
b.WriteString(styleKey.Render(" [v]") + " Create/recreate Python venv + install pip packages\n")
b.WriteString(styleKey.Render(" [n]") + " Install npm packages + build hardware JS bundles\n")
b.WriteString(styleKey.Render(" [f]") + " Full install (system + venv + pip + npm)\n")
b.WriteString(styleKey.Render(" [g]") + " Install Go compiler (for DNS server)\n")
b.WriteString(styleKey.Render(" [r]") + " Refresh status\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleDepsMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "a":
return a.startDepsInstall("system")
case "v":
return a.startDepsInstall("venv")
case "n":
return a.startDepsInstall("npm")
case "f":
return a.startDepsInstall("full")
case "g":
return a.startDepsInstall("go")
case "r":
return a, a.loadDepsStatus()
}
return a, nil
}
// ── Commands ────────────────────────────────────────────────────────
func (a App) loadDepsStatus() tea.Cmd {
return func() tea.Msg {
var items []ListItem
// Check system deps
for _, d := range systemDeps {
installed := false
parts := strings.Fields(d.Cmd)
if len(parts) == 1 {
_, err := exec.LookPath(d.Cmd)
installed = err == nil
} else {
cmd := exec.Command(parts[0], parts[1:]...)
installed = cmd.Run() == nil
}
items = append(items, ListItem{
Name: d.Name,
Status: d.Desc,
Enabled: installed,
Extra: "system",
})
}
// Check venv
venvPath := fmt.Sprintf("%s/venv", findAutarchDir())
_, err := exec.LookPath(venvPath + "/bin/python3")
items = append(items, ListItem{
Name: "venv (" + venvPath + ")",
Enabled: err == nil,
Extra: "venv",
})
// Check pip packages in venv
venvPip := venvPath + "/bin/pip3"
if _, err := exec.LookPath(venvPip); err == nil {
out, _ := exec.Command(venvPip, "list", "--format=columns").Output()
count := strings.Count(string(out), "\n") - 2
if count < 0 {
count = 0
}
items = append(items, ListItem{
Name: fmt.Sprintf("pip packages (%d installed)", count),
Enabled: count > 5,
Extra: "venv",
})
} else {
items = append(items, ListItem{
Name: "pip packages (venv not found)",
Enabled: false,
Extra: "venv",
})
}
return depsLoadedMsg{items: items}
}
}
type depsLoadedMsg struct{ items []ListItem }
func (a App) startDepsInstall(mode string) (App, tea.Cmd) {
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
a.progressLabel = ""
ch := make(chan tea.Msg, 256)
a.outputCh = ch
autarchDir := findAutarchDir()
go func() {
var steps []CmdStep
switch mode {
case "system":
steps = buildSystemInstallSteps()
case "venv":
steps = buildVenvSteps(autarchDir)
case "npm":
steps = buildNpmSteps(autarchDir)
case "go":
steps = []CmdStep{
{Label: "Update package lists", Args: []string{"apt-get", "update", "-qq"}},
{Label: "Install Go compiler", Args: []string{"apt-get", "install", "-y", "golang"}},
}
case "full":
steps = buildSystemInstallSteps()
steps = append(steps, buildVenvSteps(autarchDir)...)
steps = append(steps, buildNpmSteps(autarchDir)...)
}
if len(steps) == 0 {
ch <- OutputLineMsg(styleSuccess.Render("Nothing to install — all dependencies are present."))
close(ch)
return
}
streamSteps(ch, steps)
}()
return a, a.waitForOutput()
}
// ── Step Builders ───────────────────────────────────────────────────
func buildSystemInstallSteps() []CmdStep {
// Collect missing packages
var pkgs []string
for _, d := range systemDeps {
parts := strings.Fields(d.Cmd)
if len(parts) == 1 {
if _, err := exec.LookPath(d.Cmd); err != nil {
pkgs = append(pkgs, d.Pkg)
}
} else {
cmd := exec.Command(parts[0], parts[1:]...)
if cmd.Run() != nil {
pkgs = append(pkgs, d.Pkg)
}
}
}
// Deduplicate packages (some deps share packages like build-essential)
seen := make(map[string]bool)
var uniquePkgs []string
for _, p := range pkgs {
if !seen[p] {
seen[p] = true
uniquePkgs = append(uniquePkgs, p)
}
}
if len(uniquePkgs) == 0 {
return nil
}
steps := []CmdStep{
{Label: "Update package lists", Args: []string{"apt-get", "update", "-qq"}},
}
// Install in batches to show progress per category
// Group: core runtime
corePackages := filterPackages(uniquePkgs, []string{
"python3", "python3-pip", "python3-venv", "python3-dev",
"build-essential", "cmake", "pkg-config",
"git", "curl", "wget", "openssl",
})
if len(corePackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install core packages (%s)", strings.Join(corePackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, corePackages...),
})
}
// Group: C library headers
libPackages := filterPackages(uniquePkgs, []string{
"libffi-dev", "libssl-dev", "libpcap-dev", "libxml2-dev", "libxslt1-dev",
})
if len(libPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install C library headers (%s)", strings.Join(libPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, libPackages...),
})
}
// Group: security & network tools
toolPackages := filterPackages(uniquePkgs, []string{
"nmap", "tshark", "whois", "dnsutils",
"adb", "fastboot",
"wireguard-tools", "miniupnpc", "net-tools",
"ffmpeg",
})
if len(toolPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install security/network tools (%s)", strings.Join(toolPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, toolPackages...),
})
}
// Group: node + go
devPackages := filterPackages(uniquePkgs, []string{
"nodejs", "npm", "golang",
})
if len(devPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install dev tools (%s)", strings.Join(devPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, devPackages...),
})
}
return steps
}
func buildVenvSteps(autarchDir string) []CmdStep {
venv := autarchDir + "/venv"
pip := venv + "/bin/pip3"
reqFile := autarchDir + "/requirements.txt"
steps := []CmdStep{
{Label: "Create Python virtual environment", Args: []string{"python3", "-m", "venv", venv}},
{Label: "Upgrade pip, setuptools, wheel", Args: []string{pip, "install", "--upgrade", "pip", "setuptools", "wheel"}},
}
if fileExists(reqFile) {
steps = append(steps, CmdStep{
Label: "Install Python packages from requirements.txt",
Args: []string{pip, "install", "-r", reqFile},
})
}
return steps
}
func buildNpmSteps(autarchDir string) []CmdStep {
steps := []CmdStep{
{Label: "Install npm packages", Args: []string{"npm", "install"}, Dir: autarchDir},
}
if fileExists(autarchDir + "/scripts/build-hw-libs.sh") {
steps = append(steps, CmdStep{
Label: "Build hardware JS bundles",
Args: []string{"bash", "scripts/build-hw-libs.sh"},
Dir: autarchDir,
})
}
return steps
}
// filterPackages returns only packages from wanted that exist in available.
func filterPackages(available, wanted []string) []string {
avail := make(map[string]bool)
for _, p := range available {
avail[p] = true
}
var result []string
for _, p := range wanted {
if avail[p] {
result = append(result, p)
}
}
return result
}

View File

@ -0,0 +1,761 @@
package tui
import (
"encoding/json"
"fmt"
"net/http"
"os/exec"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderDNSMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DNS SERVER"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
// Check DNS server status
_, dnsRunning := getServiceStatus("autarch-dns")
if dnsRunning {
b.WriteString(" " + styleStatusOK.Render("● DNS Server is running"))
} else {
b.WriteString(" " + styleStatusBad.Render("○ DNS Server is stopped"))
}
b.WriteString("\n")
// Check if binary exists
dir := findAutarchDir()
binaryPath := dir + "/services/dns-server/autarch-dns"
if fileExists(binaryPath) {
b.WriteString(" " + styleSuccess.Render("✔ Binary found: ") + styleDim.Render(binaryPath))
} else {
b.WriteString(" " + styleWarning.Render("⚠ Binary not found — build required"))
}
b.WriteString("\n")
// Check if source exists
sourcePath := dir + "/services/dns-server/main.go"
if fileExists(sourcePath) {
b.WriteString(" " + styleSuccess.Render("✔ Source code present"))
} else {
b.WriteString(" " + styleError.Render("✘ Source not found at " + dir + "/services/dns-server/"))
}
b.WriteString("\n\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [b]") + " Build DNS server from source\n")
b.WriteString(styleKey.Render(" [s]") + " Start / Stop DNS server\n")
b.WriteString(styleKey.Render(" [m]") + " Manage DNS (zones, records, hosts, blocklist)\n")
b.WriteString(styleKey.Render(" [c]") + " Edit DNS config\n")
b.WriteString(styleKey.Render(" [t]") + " Test DNS resolution\n")
b.WriteString(styleKey.Render(" [l]") + " View DNS logs\n")
b.WriteString(styleKey.Render(" [i]") + " Install systemd service unit\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
func (a App) renderDNSManageMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DNS MANAGEMENT"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
// Try to get status from API
status := getDNSAPIStatus()
if status != nil {
b.WriteString(" " + styleKey.Render("Queries: ") +
lipgloss.NewStyle().Foreground(colorWhite).Render(fmt.Sprintf("%v", status["total_queries"])))
b.WriteString("\n")
b.WriteString(" " + styleKey.Render("Cache: ") +
lipgloss.NewStyle().Foreground(colorWhite).Render(fmt.Sprintf("hits=%v misses=%v", status["cache_hits"], status["cache_misses"])))
b.WriteString("\n")
b.WriteString(" " + styleKey.Render("Blocked: ") +
lipgloss.NewStyle().Foreground(colorWhite).Render(fmt.Sprintf("%v", status["blocked_queries"])))
b.WriteString("\n\n")
} else {
b.WriteString(styleWarning.Render(" ⚠ Cannot reach DNS API — is the server running?"))
b.WriteString("\n\n")
}
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [z]") + " Manage zones\n")
b.WriteString(styleKey.Render(" [h]") + " Manage hosts file\n")
b.WriteString(styleKey.Render(" [b]") + " Manage blocklist\n")
b.WriteString(styleKey.Render(" [f]") + " Manage forwarding rules\n")
b.WriteString(styleKey.Render(" [c]") + " Flush cache\n")
b.WriteString(styleKey.Render(" [q]") + " Query log\n")
b.WriteString(styleKey.Render(" [t]") + " Top domains\n")
b.WriteString(styleKey.Render(" [e]") + " Encryption settings (DoT/DoH)\n")
b.WriteString(styleKey.Render(" [r]") + " Root server check\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
func (a App) renderDNSZones() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DNS ZONES"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if len(a.listItems) == 0 {
b.WriteString(styleDim.Render(" No zones configured (or API unreachable)."))
b.WriteString("\n\n")
} else {
for i, item := range a.listItems {
cursor := " "
if i == a.cursor {
cursor = styleSelected.Render(" ▸") + " "
}
b.WriteString(fmt.Sprintf("%s%s %s\n",
cursor,
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(item.Name),
styleDim.Render(item.Status),
))
}
b.WriteString("\n")
}
b.WriteString(a.renderHR())
b.WriteString(styleKey.Render(" [n]") + " New zone ")
b.WriteString(styleKey.Render("[enter]") + " View records ")
b.WriteString(styleKey.Render("[d]") + " Delete zone\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleDNSMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "b":
return a.buildDNSServer()
case "s":
return a.toggleDNSService()
case "m":
a.pushView(ViewDNSManage)
return a, nil
case "c":
return a.editDNSConfig()
case "t":
return a.testDNSResolution()
case "l":
return a.viewDNSLogs()
case "i":
return a.installDNSUnit()
}
return a, nil
}
func (a App) handleDNSManageMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "z":
return a.loadDNSZones()
case "h":
return a.manageDNSHosts()
case "b":
return a.manageDNSBlocklist()
case "f":
return a.manageDNSForwarding()
case "c":
return a.flushDNSCache()
case "q":
return a.viewDNSQueryLog()
case "t":
return a.viewDNSTopDomains()
case "e":
return a.viewDNSEncryption()
case "r":
return a.dnsRootCheck()
}
return a, nil
}
func (a App) handleDNSZonesMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "n":
return a.openDNSZoneForm()
case "enter":
if a.cursor >= 0 && a.cursor < len(a.listItems) {
return a.viewDNSZoneRecords(a.listItems[a.cursor].Name)
}
case "d":
if a.cursor >= 0 && a.cursor < len(a.listItems) {
zone := a.listItems[a.cursor].Name
a.confirmPrompt = fmt.Sprintf("Delete zone '%s' and all its records?", zone)
a.confirmAction = func() tea.Cmd {
return func() tea.Msg {
return dnsAPIDelete("/api/zones/" + zone)
}
}
a.pushView(ViewConfirm)
}
}
return a, nil
}
// ── DNS Commands ────────────────────────────────────────────────────
func (a App) buildDNSServer() (App, tea.Cmd) {
a.pushView(ViewDNSBuild)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
ch := make(chan tea.Msg, 256)
a.outputCh = ch
go func() {
dir := findAutarchDir()
dnsDir := dir + "/services/dns-server"
steps := []CmdStep{
{Label: "Download Go dependencies", Args: []string{"go", "mod", "download"}, Dir: dnsDir},
{Label: "Build DNS server binary", Args: []string{"go", "build", "-o", "autarch-dns", "."}, Dir: dnsDir},
}
streamSteps(ch, steps)
}()
return a, a.waitForOutput()
}
func (a App) toggleDNSService() (App, tea.Cmd) {
return a.toggleService(1) // Index 1 = autarch-dns
}
func (a App) editDNSConfig() (App, tea.Cmd) {
// Load DNS config as a settings section
dir := findAutarchDir()
configPath := dir + "/data/dns/config.json"
data, err := readFileBytes(configPath)
if err != nil {
return a, func() tea.Msg {
return ResultMsg{
Title: "DNS Config",
Lines: []string{
"No DNS config file found at: " + configPath,
"",
"Start the DNS server once to generate a default config,",
"or build and run: ./autarch-dns --config " + configPath,
},
IsError: true,
}
}
}
var cfg map[string]interface{}
if err := json.Unmarshal(data, &cfg); err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Invalid JSON: " + err.Error()}, IsError: true}
}
}
// Flatten to key=value for editing
var keys []string
var vals []string
for k, v := range cfg {
keys = append(keys, k)
vals = append(vals, fmt.Sprintf("%v", v))
}
a.settingsSection = "dns-config"
a.settingsKeys = keys
a.settingsVals = vals
a.pushView(ViewSettingsSection)
return a, nil
}
func (a App) testDNSResolution() (App, tea.Cmd) {
return a, func() tea.Msg {
domains := []string{"google.com", "github.com", "cloudflare.com"}
var lines []string
for _, domain := range domains {
out, err := exec.Command("dig", "@127.0.0.1", domain, "+short", "+time=2").Output()
if err != nil {
lines = append(lines, styleError.Render(fmt.Sprintf(" ✘ %s: %s", domain, err.Error())))
} else {
result := strings.TrimSpace(string(out))
if result == "" {
lines = append(lines, styleWarning.Render(fmt.Sprintf(" ⚠ %s: no answer", domain)))
} else {
lines = append(lines, styleSuccess.Render(fmt.Sprintf(" ✔ %s → %s", domain, result)))
}
}
}
return ResultMsg{Title: "DNS Resolution Test (@127.0.0.1)", Lines: lines}
}
}
func (a App) viewDNSLogs() (App, tea.Cmd) {
return a, func() tea.Msg {
out, _ := exec.Command("journalctl", "-u", "autarch-dns", "-n", "30", "--no-pager").Output()
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
return ResultMsg{Title: "DNS Server Logs (last 30)", Lines: lines}
}
}
func (a App) installDNSUnit() (App, tea.Cmd) {
// Delegate to the service installer for just the DNS unit
return a, func() tea.Msg {
dir := findAutarchDir()
unit := fmt.Sprintf(`[Unit]
Description=AUTARCH DNS Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/services/dns-server/autarch-dns --config %s/data/dns/config.json
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`, dir, dir, dir)
path := "/etc/systemd/system/autarch-dns.service"
if err := writeFileAtomic(path, []byte(unit)); err != nil {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
exec.Command("systemctl", "daemon-reload").Run()
return ResultMsg{
Title: "DNS Service Unit Installed",
Lines: []string{
"Installed: " + path,
"",
"Start with: systemctl start autarch-dns",
"Enable on boot: systemctl enable autarch-dns",
},
}
}
}
// ── DNS API Management Commands ─────────────────────────────────────
func (a App) loadDNSZones() (App, tea.Cmd) {
a.pushView(ViewDNSZones)
return a, func() tea.Msg {
zones := dnsAPIGet("/api/zones")
if zones == nil {
return dnsZonesMsg{items: nil}
}
zoneList, ok := zones.([]interface{})
if !ok {
return dnsZonesMsg{items: nil}
}
var items []ListItem
for _, z := range zoneList {
zMap, ok := z.(map[string]interface{})
if !ok {
continue
}
name := fmt.Sprintf("%v", zMap["domain"])
recordCount := 0
if records, ok := zMap["records"].([]interface{}); ok {
recordCount = len(records)
}
items = append(items, ListItem{
Name: name,
Status: fmt.Sprintf("%d records", recordCount),
})
}
return dnsZonesMsg{items: items}
}
}
type dnsZonesMsg struct{ items []ListItem }
func (a App) openDNSZoneForm() (App, tea.Cmd) {
a.labels = []string{"Domain", "Primary NS", "Admin Email", "Default TTL"}
a.inputs = make([]textinput.Model, 4)
defaults := []string{"", "ns1.example.com", "admin.example.com", "3600"}
for i := range a.inputs {
ti := textinput.New()
ti.CharLimit = 256
ti.Width = 40
ti.SetValue(defaults[i])
if i == 0 {
ti.Focus()
ti.SetValue("")
}
a.inputs[i] = ti
}
a.focusIdx = 0
a.pushView(ViewDNSZoneEdit)
return a, nil
}
func (a App) submitDNSZone() (App, tea.Cmd) {
domain := a.inputs[0].Value()
ns := a.inputs[1].Value()
admin := a.inputs[2].Value()
ttl := a.inputs[3].Value()
if domain == "" {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Domain cannot be empty."}, IsError: true}
}
}
a.popView()
return a, func() tea.Msg {
body := fmt.Sprintf(`{"domain":"%s","soa":{"primary_ns":"%s","admin_email":"%s","ttl":%s}}`,
domain, ns, admin, ttl)
return dnsAPIPost("/api/zones", body)
}
}
func (a App) viewDNSZoneRecords(zone string) (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/zones/" + zone + "/records")
if data == nil {
return ResultMsg{Title: "Zone: " + zone, Lines: []string{"No records or API unreachable."}, IsError: true}
}
records, ok := data.([]interface{})
if !ok {
return ResultMsg{Title: "Zone: " + zone, Lines: []string{"Unexpected response format."}, IsError: true}
}
var lines []string
lines = append(lines, fmt.Sprintf("Zone: %s — %d records", zone, len(records)))
lines = append(lines, "")
lines = append(lines, styleDim.Render(fmt.Sprintf(" %-8s %-30s %-6s %s", "TYPE", "NAME", "TTL", "VALUE")))
lines = append(lines, styleDim.Render(" "+strings.Repeat("─", 70)))
for _, r := range records {
rec, ok := r.(map[string]interface{})
if !ok {
continue
}
lines = append(lines, fmt.Sprintf(" %-8v %-30v %-6v %v",
rec["type"], rec["name"], rec["ttl"], rec["value"]))
}
return ResultMsg{Title: "Zone: " + zone, Lines: lines}
}
}
func (a App) manageDNSHosts() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/hosts")
if data == nil {
return ResultMsg{Title: "DNS Hosts", Lines: []string{"API unreachable."}, IsError: true}
}
hosts, ok := data.([]interface{})
if !ok {
return ResultMsg{Title: "DNS Hosts", Lines: []string{"No hosts entries."}}
}
var lines []string
lines = append(lines, fmt.Sprintf("%d host entries", len(hosts)))
lines = append(lines, "")
for _, h := range hosts {
hMap, _ := h.(map[string]interface{})
lines = append(lines, fmt.Sprintf(" %-16v %v", hMap["ip"], hMap["hostname"]))
}
return ResultMsg{Title: "DNS Hosts", Lines: lines}
}
}
func (a App) manageDNSBlocklist() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/blocklist")
if data == nil {
return ResultMsg{Title: "DNS Blocklist", Lines: []string{"API unreachable."}, IsError: true}
}
bl, ok := data.(map[string]interface{})
if !ok {
return ResultMsg{Title: "DNS Blocklist", Lines: []string{"Unexpected format."}}
}
var lines []string
if domains, ok := bl["domains"].([]interface{}); ok {
lines = append(lines, fmt.Sprintf("%d blocked domains", len(domains)))
lines = append(lines, "")
max := 30
if len(domains) < max {
max = len(domains)
}
for _, d := range domains[:max] {
lines = append(lines, " "+fmt.Sprintf("%v", d))
}
if len(domains) > 30 {
lines = append(lines, styleDim.Render(fmt.Sprintf(" ... and %d more", len(domains)-30)))
}
} else {
lines = append(lines, "Blocklist is empty.")
}
return ResultMsg{Title: "DNS Blocklist", Lines: lines}
}
}
func (a App) manageDNSForwarding() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/forwarding")
if data == nil {
return ResultMsg{Title: "DNS Forwarding", Lines: []string{"API unreachable."}, IsError: true}
}
rules, ok := data.([]interface{})
if !ok {
return ResultMsg{Title: "DNS Forwarding", Lines: []string{"No forwarding rules configured."}}
}
var lines []string
lines = append(lines, fmt.Sprintf("%d forwarding rules", len(rules)))
lines = append(lines, "")
for _, r := range rules {
rMap, _ := r.(map[string]interface{})
lines = append(lines, fmt.Sprintf(" %v → %v", rMap["zone"], rMap["upstream"]))
}
return ResultMsg{Title: "DNS Forwarding Rules", Lines: lines}
}
}
func (a App) flushDNSCache() (App, tea.Cmd) {
return a, func() tea.Msg {
return dnsAPIDelete("/api/cache")
}
}
func (a App) viewDNSQueryLog() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/querylog?limit=30")
if data == nil {
return ResultMsg{Title: "DNS Query Log", Lines: []string{"API unreachable."}, IsError: true}
}
entries, ok := data.([]interface{})
if !ok {
return ResultMsg{Title: "DNS Query Log", Lines: []string{"No entries."}}
}
var lines []string
for _, e := range entries {
eMap, _ := e.(map[string]interface{})
lines = append(lines, fmt.Sprintf(" %-20v %-6v %-30v %v",
eMap["time"], eMap["type"], eMap["name"], eMap["client"]))
}
return ResultMsg{Title: "DNS Query Log (last 30)", Lines: lines}
}
}
func (a App) viewDNSTopDomains() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/stats/top-domains?limit=20")
if data == nil {
return ResultMsg{Title: "Top Domains", Lines: []string{"API unreachable."}, IsError: true}
}
domains, ok := data.([]interface{})
if !ok {
return ResultMsg{Title: "Top Domains", Lines: []string{"No data."}}
}
var lines []string
for i, d := range domains {
dMap, _ := d.(map[string]interface{})
lines = append(lines, fmt.Sprintf(" %2d. %-40v %v queries", i+1, dMap["domain"], dMap["count"]))
}
return ResultMsg{Title: "Top 20 Queried Domains", Lines: lines}
}
}
func (a App) viewDNSEncryption() (App, tea.Cmd) {
return a, func() tea.Msg {
data := dnsAPIGet("/api/encryption")
if data == nil {
return ResultMsg{Title: "DNS Encryption", Lines: []string{"API unreachable."}, IsError: true}
}
enc, _ := data.(map[string]interface{})
var lines []string
for k, v := range enc {
status := styleStatusBad.Render("disabled")
if v == true {
status = styleStatusOK.Render("enabled")
}
lines = append(lines, fmt.Sprintf(" %-20s %s", k, status))
}
return ResultMsg{Title: "DNS Encryption Status", Lines: lines}
}
}
func (a App) dnsRootCheck() (App, tea.Cmd) {
return a, func() tea.Msg {
body := dnsAPIPostRaw("/api/rootcheck", "")
if body == nil {
return ResultMsg{Title: "Root Check", Lines: []string{"API unreachable."}, IsError: true}
}
results, ok := body.([]interface{})
if !ok {
return ResultMsg{Title: "Root Check", Lines: []string{"Unexpected format."}}
}
var lines []string
for _, r := range results {
rMap, _ := r.(map[string]interface{})
latency := fmt.Sprintf("%v", rMap["latency"])
status := styleSuccess.Render("✔")
if rMap["error"] != nil && rMap["error"] != "" {
status = styleError.Render("✘")
latency = fmt.Sprintf("%v", rMap["error"])
}
lines = append(lines, fmt.Sprintf(" %s %-20v %s", status, rMap["server"], latency))
}
return ResultMsg{Title: "Root Server Latency Check", Lines: lines}
}
}
// ── DNS API Helpers ─────────────────────────────────────────────────
func getDNSAPIBase() string {
return "http://127.0.0.1:5380"
}
func getDNSAPIToken() string {
dir := findAutarchDir()
configPath := dir + "/data/dns/config.json"
data, err := readFileBytes(configPath)
if err != nil {
return ""
}
var cfg map[string]interface{}
if err := json.Unmarshal(data, &cfg); err != nil {
return ""
}
if token, ok := cfg["api_token"].(string); ok {
return token
}
return ""
}
func getDNSAPIStatus() map[string]interface{} {
data := dnsAPIGet("/api/metrics")
if data == nil {
return nil
}
m, ok := data.(map[string]interface{})
if !ok {
return nil
}
return m
}
func dnsAPIGet(path string) interface{} {
url := getDNSAPIBase() + path
token := getDNSAPIToken()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
var result interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result
}
func dnsAPIPost(path, body string) tea.Msg {
result := dnsAPIPostRaw(path, body)
if result == nil {
return ResultMsg{Title: "Error", Lines: []string{"API request failed."}, IsError: true}
}
return ResultMsg{Title: "Success", Lines: []string{"Operation completed."}}
}
func dnsAPIPostRaw(path, body string) interface{} {
url := getDNSAPIBase() + path
token := getDNSAPIToken()
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil {
return nil
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
var result interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result
}
func dnsAPIDelete(path string) tea.Msg {
url := getDNSAPIBase() + path
token := getDNSAPIToken()
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
defer resp.Body.Close()
return ResultMsg{Title: "Success", Lines: []string{"Deleted."}}
}

View File

@ -0,0 +1,52 @@
package tui
import tea "github.com/charmbracelet/bubbletea"
func (a App) handleMainMenu(key string) (tea.Model, tea.Cmd) {
// Number key shortcut
for _, item := range a.mainMenu {
if key == item.Key {
if item.Key == "q" {
return a, tea.Quit
}
return a.navigateToView(item.View)
}
}
// Enter on selected item
if key == "enter" {
if a.cursor >= 0 && a.cursor < len(a.mainMenu) {
item := a.mainMenu[a.cursor]
if item.Key == "q" {
return a, tea.Quit
}
return a.navigateToView(item.View)
}
}
return a, nil
}
func (a App) navigateToView(v ViewID) (tea.Model, tea.Cmd) {
a.pushView(v)
switch v {
case ViewDeploy:
// Static menu, no async loading
case ViewDeps:
// Load dependency status
return a, a.loadDepsStatus()
case ViewModules:
return a, a.loadModules()
case ViewSettings:
return a, a.loadSettings()
case ViewUsers:
// Static menu, no loading
case ViewService:
return a, a.loadServiceStatus()
case ViewDNS:
// Static menu
}
return a, nil
}

View File

@ -0,0 +1,273 @@
package tui
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ── Module Categories ───────────────────────────────────────────────
var moduleCategories = map[string]string{
"defender.py": "Defense",
"defender_monitor.py": "Defense",
"defender_windows.py": "Defense",
"container_sec.py": "Defense",
"msf.py": "Offense",
"exploit_dev.py": "Offense",
"loadtest.py": "Offense",
"phishmail.py": "Offense",
"deauth.py": "Offense",
"mitm_proxy.py": "Offense",
"c2_framework.py": "Offense",
"api_fuzzer.py": "Offense",
"webapp_scanner.py": "Offense",
"cloud_scan.py": "Offense",
"starlink_hack.py": "Offense",
"rcs_tools.py": "Offense",
"sms_forge.py": "Offense",
"pineapple.py": "Offense",
"password_toolkit.py": "Offense",
"counter.py": "Counter",
"anti_forensics.py": "Counter",
"analyze.py": "Analysis",
"forensics.py": "Analysis",
"llm_trainer.py": "Analysis",
"report_engine.py": "Analysis",
"threat_intel.py": "Analysis",
"ble_scanner.py": "Analysis",
"rfid_tools.py": "Analysis",
"reverse_eng.py": "Analysis",
"steganography.py": "Analysis",
"incident_resp.py": "Analysis",
"net_mapper.py": "Analysis",
"log_correlator.py": "Analysis",
"malware_sandbox.py": "Analysis",
"email_sec.py": "Analysis",
"vulnerab_scanner.py": "Analysis",
"recon.py": "OSINT",
"dossier.py": "OSINT",
"geoip.py": "OSINT",
"adultscan.py": "OSINT",
"yandex_osint.py": "OSINT",
"social_eng.py": "OSINT",
"ipcapture.py": "OSINT",
"snoop_decoder.py": "OSINT",
"simulate.py": "Simulate",
"android_apps.py": "Android",
"android_advanced.py": "Android",
"android_boot.py": "Android",
"android_payload.py": "Android",
"android_protect.py": "Android",
"android_recon.py": "Android",
"android_root.py": "Android",
"android_screen.py": "Android",
"android_sms.py": "Android",
"hardware_local.py": "Hardware",
"hardware_remote.py": "Hardware",
"iphone_local.py": "Hardware",
"wireshark.py": "Hardware",
"sdr_tools.py": "Hardware",
"upnp_manager.py": "System",
"wireguard_manager.py": "System",
"revshell.py": "System",
"hack_hijack.py": "System",
"chat.py": "Core",
"agent.py": "Core",
"agent_hal.py": "Core",
"mysystem.py": "Core",
"setup.py": "Core",
"workflow.py": "Core",
"nettest.py": "Core",
"rsf.py": "Core",
"ad_audit.py": "Offense",
"router_sploit.py": "Offense",
"wifi_audit.py": "Offense",
}
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderModulesList() string {
var b strings.Builder
b.WriteString(styleTitle.Render("MODULES"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if len(a.listItems) == 0 {
b.WriteString(styleDim.Render(" Loading..."))
b.WriteString("\n")
return b.String()
}
// Group by category
groups := make(map[string][]int)
for i, item := range a.listItems {
groups[item.Extra] = append(groups[item.Extra], i)
}
// Sort category names
var cats []string
for c := range groups {
cats = append(cats, c)
}
sort.Strings(cats)
for _, cat := range cats {
b.WriteString(styleKey.Render(fmt.Sprintf(" ── %s ", cat)))
b.WriteString(styleDim.Render(fmt.Sprintf("(%d)", len(groups[cat]))))
b.WriteString("\n")
for _, idx := range groups[cat] {
item := a.listItems[idx]
cursor := " "
if idx == a.cursor {
cursor = styleSelected.Render(" ▸") + " "
}
status := styleStatusOK.Render("●")
if !item.Enabled {
status = styleStatusBad.Render("○")
}
name := item.Name
if idx == a.cursor {
name = lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(name)
}
b.WriteString(fmt.Sprintf("%s%s %s\n", cursor, status, name))
}
b.WriteString("\n")
}
b.WriteString(a.renderHR())
b.WriteString(styleKey.Render(" [enter]") + " Toggle enabled/disabled ")
b.WriteString(styleKey.Render("[r]") + " Refresh\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleModulesMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "enter":
if a.cursor >= 0 && a.cursor < len(a.listItems) {
return a.toggleModule(a.cursor)
}
case "r":
return a, a.loadModules()
}
return a, nil
}
func (a App) handleModuleToggle(key string) (tea.Model, tea.Cmd) {
return a.handleModulesMenu(key)
}
// ── Commands ────────────────────────────────────────────────────────
func (a App) loadModules() tea.Cmd {
return func() tea.Msg {
dir := findAutarchDir()
modulesDir := filepath.Join(dir, "modules")
entries, err := os.ReadDir(modulesDir)
if err != nil {
return ResultMsg{
Title: "Error",
Lines: []string{"Cannot read modules directory: " + err.Error()},
IsError: true,
}
}
var items []ListItem
for _, e := range entries {
name := e.Name()
if !strings.HasSuffix(name, ".py") || name == "__init__.py" {
continue
}
cat := "Other"
if c, ok := moduleCategories[name]; ok {
cat = c
}
// Check if module has a run() function (basic check)
content, _ := os.ReadFile(filepath.Join(modulesDir, name))
hasRun := strings.Contains(string(content), "def run(")
items = append(items, ListItem{
Name: strings.TrimSuffix(name, ".py"),
Enabled: hasRun,
Extra: cat,
Status: name,
})
}
// Sort by category then name
sort.Slice(items, func(i, j int) bool {
if items[i].Extra != items[j].Extra {
return items[i].Extra < items[j].Extra
}
return items[i].Name < items[j].Name
})
return modulesLoadedMsg{items: items}
}
}
type modulesLoadedMsg struct{ items []ListItem }
func (a App) toggleModule(idx int) (App, tea.Cmd) {
if idx < 0 || idx >= len(a.listItems) {
return a, nil
}
item := a.listItems[idx]
dir := findAutarchDir()
modulesDir := filepath.Join(dir, "modules")
disabledDir := filepath.Join(modulesDir, "disabled")
srcFile := filepath.Join(modulesDir, item.Status)
dstFile := filepath.Join(disabledDir, item.Status)
if item.Enabled {
// Disable: move to disabled/
os.MkdirAll(disabledDir, 0755)
if err := os.Rename(srcFile, dstFile); err != nil {
return a, func() tea.Msg {
return ResultMsg{
Title: "Error",
Lines: []string{"Cannot disable module: " + err.Error()},
IsError: true,
}
}
}
a.listItems[idx].Enabled = false
} else {
// Enable: move from disabled/ back
if err := os.Rename(dstFile, srcFile); err != nil {
// It might just be a module without run()
return a, func() tea.Msg {
return ResultMsg{
Title: "Note",
Lines: []string{"Module " + item.Name + " is present but has no run() entry point."},
IsError: false,
}
}
}
a.listItems[idx].Enabled = true
}
return a, nil
}

View File

@ -0,0 +1,380 @@
package tui
import (
"fmt"
"os/exec"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ── Service Definitions ─────────────────────────────────────────────
type serviceInfo struct {
Name string
Unit string // systemd unit name
Desc string
Binary string // path to check
}
var managedServices = []serviceInfo{
{"AUTARCH Web", "autarch-web", "Web dashboard (Flask)", "autarch_web.py"},
{"AUTARCH DNS", "autarch-dns", "DNS server (Go)", "autarch-dns"},
{"AUTARCH Autonomy", "autarch-autonomy", "Autonomous AI daemon", ""},
}
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderServiceMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("SERVICE MANAGEMENT"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
// Show service statuses — checks both systemd and raw processes
svcChecks := []struct {
Info serviceInfo
Process string // process name to pgrep for
}{
{managedServices[0], "autarch_web.py"},
{managedServices[1], "autarch-dns"},
{managedServices[2], "autonomy"},
}
for _, sc := range svcChecks {
status, running := getProcessStatus(sc.Info.Unit, sc.Process)
indicator := styleStatusOK.Render("● running")
if !running {
indicator = styleStatusBad.Render("○ stopped")
}
b.WriteString(fmt.Sprintf(" %s %s\n",
indicator,
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(sc.Info.Name),
))
b.WriteString(fmt.Sprintf(" %s %s\n",
styleDim.Render(sc.Info.Desc),
styleDim.Render("("+status+")"),
))
b.WriteString("\n")
}
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [1]") + " Start/Stop AUTARCH Web\n")
b.WriteString(styleKey.Render(" [2]") + " Start/Stop AUTARCH DNS\n")
b.WriteString(styleKey.Render(" [3]") + " Start/Stop Autonomy Daemon\n")
b.WriteString("\n")
b.WriteString(styleKey.Render(" [r]") + " Restart all running services\n")
b.WriteString(styleKey.Render(" [e]") + " Enable all services on boot\n")
b.WriteString(styleKey.Render(" [i]") + " Install/update systemd unit files\n")
b.WriteString(styleKey.Render(" [l]") + " View service logs (journalctl)\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleServiceMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "1":
return a.toggleService(0)
case "2":
return a.toggleService(1)
case "3":
return a.toggleService(2)
case "r":
return a.restartAllServices()
case "e":
return a.enableAllServices()
case "i":
return a.installServiceUnits()
case "l":
return a.viewServiceLogs()
}
return a, nil
}
// ── Commands ────────────────────────────────────────────────────────
func (a App) loadServiceStatus() tea.Cmd {
return nil // Services are checked live in render
}
func getServiceStatus(unit string) (string, bool) {
out, err := exec.Command("systemctl", "is-active", unit).Output()
status := strings.TrimSpace(string(out))
if err != nil || status != "active" {
// Check if unit exists
_, existErr := exec.Command("systemctl", "cat", unit).Output()
if existErr != nil {
return "not installed", false
}
return status, false
}
return status, true
}
// getProcessStatus checks both systemd and direct process for a service.
// Returns (status description, isRunning).
func getProcessStatus(unitName, processName string) (string, bool) {
// First try systemd
status, running := getServiceStatus(unitName)
if running {
return "systemd: " + status, true
}
// Fall back to process detection (pgrep)
out, err := exec.Command("pgrep", "-f", processName).Output()
if err == nil && strings.TrimSpace(string(out)) != "" {
pids := strings.Fields(strings.TrimSpace(string(out)))
return fmt.Sprintf("running (PID %s)", pids[0]), true
}
return status, false
}
func (a App) toggleService(idx int) (App, tea.Cmd) {
if idx < 0 || idx >= len(managedServices) {
return a, nil
}
svc := managedServices[idx]
processNames := []string{"autarch_web.py", "autarch-dns", "autonomy"}
procName := processNames[idx]
_, sysRunning := getServiceStatus(svc.Unit)
_, procRunning := getProcessStatus(svc.Unit, procName)
isRunning := sysRunning || procRunning
return a, func() tea.Msg {
dir := findAutarchDir()
if isRunning {
// Stop — try systemd first, then kill process
if sysRunning {
cmd := exec.Command("systemctl", "stop", svc.Unit)
cmd.CombinedOutput()
}
// Also kill any direct processes
exec.Command("pkill", "-f", procName).Run()
return ResultMsg{
Title: "Service " + svc.Name,
Lines: []string{svc.Name + " stopped."},
}
}
// Start — try systemd first, fall back to direct launch
if _, err := exec.Command("systemctl", "cat", svc.Unit).Output(); err == nil {
cmd := exec.Command("systemctl", "start", svc.Unit)
out, err := cmd.CombinedOutput()
if err != nil {
return ResultMsg{
Title: "Service Error",
Lines: []string{"systemctl start failed:", string(out), err.Error(), "", "Trying direct launch..."},
IsError: true,
}
}
return ResultMsg{
Title: "Service " + svc.Name,
Lines: []string{svc.Name + " started via systemd."},
}
}
// Direct launch (no systemd unit installed)
var startCmd *exec.Cmd
switch idx {
case 0: // Web
venvPy := dir + "/venv/bin/python3"
startCmd = exec.Command(venvPy, dir+"/autarch_web.py")
case 1: // DNS
binary := dir + "/services/dns-server/autarch-dns"
configFile := dir + "/data/dns/config.json"
startCmd = exec.Command(binary, "--config", configFile)
case 2: // Autonomy
venvPy := dir + "/venv/bin/python3"
startCmd = exec.Command(venvPy, "-c",
"import sys; sys.path.insert(0,'"+dir+"'); from core.autonomy import AutonomyDaemon; AutonomyDaemon().run()")
}
if startCmd != nil {
startCmd.Dir = dir
// Detach process so it survives manager exit
startCmd.Stdout = nil
startCmd.Stderr = nil
if err := startCmd.Start(); err != nil {
return ResultMsg{
Title: "Service Error",
Lines: []string{"Failed to start " + svc.Name + ":", err.Error()},
IsError: true,
}
}
// Release so it runs independently
go startCmd.Wait()
return ResultMsg{
Title: "Service " + svc.Name,
Lines: []string{
svc.Name + " started directly (PID " + fmt.Sprintf("%d", startCmd.Process.Pid) + ").",
"",
styleDim.Render("Tip: Install systemd units with [i] for persistent service management."),
},
}
}
return ResultMsg{
Title: "Error",
Lines: []string{"No start method available for " + svc.Name},
IsError: true,
}
}
}
func (a App) restartAllServices() (App, tea.Cmd) {
return a, func() tea.Msg {
var lines []string
for _, svc := range managedServices {
_, running := getServiceStatus(svc.Unit)
if running {
cmd := exec.Command("systemctl", "restart", svc.Unit)
out, err := cmd.CombinedOutput()
if err != nil {
lines = append(lines, styleError.Render("✘ "+svc.Name+": "+strings.TrimSpace(string(out))))
} else {
lines = append(lines, styleSuccess.Render("✔ "+svc.Name+": restarted"))
}
} else {
lines = append(lines, styleDim.Render("- "+svc.Name+": not running, skipped"))
}
}
return ResultMsg{Title: "Restart Services", Lines: lines}
}
}
func (a App) enableAllServices() (App, tea.Cmd) {
return a, func() tea.Msg {
var lines []string
for _, svc := range managedServices {
cmd := exec.Command("systemctl", "enable", svc.Unit)
_, err := cmd.CombinedOutput()
if err != nil {
lines = append(lines, styleWarning.Render("⚠ "+svc.Name+": could not enable (unit may not exist)"))
} else {
lines = append(lines, styleSuccess.Render("✔ "+svc.Name+": enabled on boot"))
}
}
return ResultMsg{Title: "Enable Services", Lines: lines}
}
}
func (a App) installServiceUnits() (App, tea.Cmd) {
return a, func() tea.Msg {
dir := findAutarchDir()
var lines []string
// Web service unit
webUnit := fmt.Sprintf(`[Unit]
Description=AUTARCH Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/venv/bin/python3 %s/autarch_web.py
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
`, dir, dir, dir)
// DNS service unit
dnsUnit := fmt.Sprintf(`[Unit]
Description=AUTARCH DNS Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/services/dns-server/autarch-dns --config %s/data/dns/config.json
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`, dir, dir, dir)
// Autonomy daemon unit
autoUnit := fmt.Sprintf(`[Unit]
Description=AUTARCH Autonomy Daemon
After=network.target autarch-web.service
[Service]
Type=simple
User=root
WorkingDirectory=%s
ExecStart=%s/venv/bin/python3 -c "from core.autonomy import AutonomyDaemon; AutonomyDaemon().run()"
Restart=on-failure
RestartSec=10
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
`, dir, dir)
units := map[string]string{
"autarch-web.service": webUnit,
"autarch-dns.service": dnsUnit,
"autarch-autonomy.service": autoUnit,
}
for name, content := range units {
path := "/etc/systemd/system/" + name
if err := writeFileAtomic(path, []byte(content)); err != nil {
lines = append(lines, styleError.Render("✘ "+name+": "+err.Error()))
} else {
lines = append(lines, styleSuccess.Render("✔ "+name+": installed"))
}
}
// Reload systemd
exec.Command("systemctl", "daemon-reload").Run()
lines = append(lines, "", styleSuccess.Render("✔ systemctl daemon-reload"))
return ResultMsg{Title: "Service Units Installed", Lines: lines}
}
}
func (a App) viewServiceLogs() (App, tea.Cmd) {
return a, func() tea.Msg {
var lines []string
for _, svc := range managedServices {
out, _ := exec.Command("journalctl", "-u", svc.Unit, "-n", "10", "--no-pager").Output()
lines = append(lines, styleKey.Render("── "+svc.Name+" ──"))
logLines := strings.Split(strings.TrimSpace(string(out)), "\n")
for _, l := range logLines {
lines = append(lines, " "+l)
}
lines = append(lines, "")
}
return ResultMsg{Title: "Service Logs (last 10 entries)", Lines: lines}
}
}
func writeFileAtomic(path string, data []byte) error {
tmp := path + ".tmp"
if err := writeFile(tmp, data, 0644); err != nil {
return err
}
return renameFile(tmp, path)
}

View File

@ -0,0 +1,249 @@
package tui
import (
"fmt"
"os"
"sort"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/darkhal/autarch-server-manager/internal/config"
)
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderSettingsSections() string {
var b strings.Builder
b.WriteString(styleTitle.Render("SETTINGS — autarch_settings.conf"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if len(a.settingsSections) == 0 {
b.WriteString(styleDim.Render(" Loading..."))
b.WriteString("\n")
return b.String()
}
for i, sec := range a.settingsSections {
cursor := " "
if i == a.cursor {
cursor = styleSelected.Render(" ▸") + " "
b.WriteString(cursor + styleKey.Render("["+sec+"]") + "\n")
} else {
b.WriteString(cursor + styleDim.Render("["+sec+"]") + "\n")
}
}
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString(styleKey.Render(" [enter]") + " Edit section ")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
func (a App) renderSettingsKeys() string {
var b strings.Builder
b.WriteString(styleTitle.Render(fmt.Sprintf("SETTINGS — [%s]", a.settingsSection)))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
for i, key := range a.settingsKeys {
val := ""
if i < len(a.settingsVals) {
val = a.settingsVals[i]
}
cursor := " "
if i == a.cursor {
cursor = styleSelected.Render(" ▸") + " "
}
// Mask sensitive values
displayVal := val
if isSensitiveKey(key) && len(val) > 4 {
displayVal = val[:4] + strings.Repeat("•", len(val)-4)
}
b.WriteString(fmt.Sprintf("%s%s = %s\n",
cursor,
styleKey.Render(key),
lipgloss.NewStyle().Foreground(colorWhite).Render(displayVal),
))
}
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString(styleKey.Render(" [enter]") + " Edit all values ")
b.WriteString(styleKey.Render("[d]") + " Edit selected ")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
func isSensitiveKey(key string) bool {
k := strings.ToLower(key)
return strings.Contains(k, "password") || strings.Contains(k, "secret") ||
strings.Contains(k, "api_key") || strings.Contains(k, "token")
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleSettingsMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "enter":
if a.cursor >= 0 && a.cursor < len(a.settingsSections) {
a.settingsSection = a.settingsSections[a.cursor]
return a.loadSettingsSection()
}
}
return a, nil
}
func (a App) handleSettingsSection(key string) (tea.Model, tea.Cmd) {
switch key {
case "enter":
// Edit all values in this section
return a.openSettingsEdit()
case "d":
// Edit single selected value
if a.cursor >= 0 && a.cursor < len(a.settingsKeys) {
return a.openSingleSettingEdit(a.cursor)
}
}
return a, nil
}
// ── Commands ────────────────────────────────────────────────────────
func (a App) loadSettings() tea.Cmd {
return func() tea.Msg {
confPath := findAutarchDir() + "/autarch_settings.conf"
sections, err := config.ListSections(confPath)
if err != nil {
return ResultMsg{
Title: "Error",
Lines: []string{"Cannot read config: " + err.Error()},
IsError: true,
}
}
sort.Strings(sections)
return settingsLoadedMsg{sections: sections}
}
}
type settingsLoadedMsg struct{ sections []string }
func (a App) loadSettingsSection() (App, tea.Cmd) {
confPath := findAutarchDir() + "/autarch_settings.conf"
keys, vals, err := config.GetSection(confPath, a.settingsSection)
if err != nil {
return a, func() tea.Msg {
return ResultMsg{
Title: "Error",
Lines: []string{err.Error()},
IsError: true,
}
}
}
a.settingsKeys = keys
a.settingsVals = vals
a.pushView(ViewSettingsSection)
return a, nil
}
func (a App) openSettingsEdit() (App, tea.Cmd) {
a.labels = make([]string, len(a.settingsKeys))
a.inputs = make([]textinput.Model, len(a.settingsKeys))
copy(a.labels, a.settingsKeys)
for i, val := range a.settingsVals {
ti := textinput.New()
ti.CharLimit = 512
ti.Width = 50
ti.SetValue(val)
if isSensitiveKey(a.settingsKeys[i]) {
ti.EchoMode = textinput.EchoPassword
}
if i == 0 {
ti.Focus()
}
a.inputs[i] = ti
}
a.focusIdx = 0
a.pushView(ViewSettingsEdit)
return a, nil
}
func (a App) openSingleSettingEdit(idx int) (App, tea.Cmd) {
a.labels = []string{a.settingsKeys[idx]}
a.inputs = make([]textinput.Model, 1)
ti := textinput.New()
ti.CharLimit = 512
ti.Width = 50
ti.SetValue(a.settingsVals[idx])
if isSensitiveKey(a.settingsKeys[idx]) {
ti.EchoMode = textinput.EchoPassword
}
ti.Focus()
a.inputs[0] = ti
a.focusIdx = 0
a.pushView(ViewSettingsEdit)
return a, nil
}
func (a App) saveSettings() (App, tea.Cmd) {
confPath := findAutarchDir() + "/autarch_settings.conf"
// Read the full config file
data, err := os.ReadFile(confPath)
if err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
}
content := string(data)
// Apply changes
for i, label := range a.labels {
newVal := a.inputs[i].Value()
content = config.SetValue(content, a.settingsSection, label, newVal)
}
if err := os.WriteFile(confPath, []byte(content), 0644); err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
}
a.popView()
// Reload the section
keys, vals, _ := config.GetSection(confPath, a.settingsSection)
a.settingsKeys = keys
a.settingsVals = vals
return a, func() tea.Msg {
return ResultMsg{
Title: "Settings Saved",
Lines: []string{
fmt.Sprintf("Updated [%s] section with %d values.", a.settingsSection, len(a.labels)),
"",
"Restart AUTARCH services for changes to take effect.",
},
}
}
}

View File

@ -0,0 +1,225 @@
package tui
import (
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/darkhal/autarch-server-manager/internal/users"
)
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderUsersMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("USER MANAGEMENT"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
// Show current credentials info
dir := findAutarchDir()
creds, err := users.LoadCredentials(dir)
if err != nil {
b.WriteString(styleWarning.Render(" No credentials file found — using defaults (admin/admin)"))
b.WriteString("\n\n")
} else {
b.WriteString(" " + styleKey.Render("Current user: ") +
lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(creds.Username))
b.WriteString("\n")
if creds.ForceChange {
b.WriteString(" " + styleWarning.Render("⚠ Password change required on next login"))
} else {
b.WriteString(" " + styleSuccess.Render("✔ Password is set"))
}
b.WriteString("\n\n")
}
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [c]") + " Create new user / change username\n")
b.WriteString(styleKey.Render(" [r]") + " Reset password\n")
b.WriteString(styleKey.Render(" [f]") + " Force password change on next login\n")
b.WriteString(styleKey.Render(" [d]") + " Reset to defaults (admin/admin)\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleUsersMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "c":
return a.openUserCreateForm()
case "r":
return a.openUserResetForm()
case "f":
return a.forcePasswordChange()
case "d":
a.confirmPrompt = "Reset credentials to admin/admin? This cannot be undone."
a.confirmAction = func() tea.Cmd {
return func() tea.Msg {
dir := findAutarchDir()
err := users.ResetToDefaults(dir)
if err != nil {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
return ResultMsg{
Title: "Credentials Reset",
Lines: []string{
"Username: admin",
"Password: admin",
"",
"Force change on next login: YES",
},
}
}
}
a.pushView(ViewConfirm)
return a, nil
}
return a, nil
}
// ── Forms ───────────────────────────────────────────────────────────
func (a App) openUserCreateForm() (App, tea.Cmd) {
a.labels = []string{"Username", "Password", "Confirm Password"}
a.inputs = make([]textinput.Model, 3)
for i := range a.inputs {
ti := textinput.New()
ti.CharLimit = 128
ti.Width = 40
if i > 0 {
ti.EchoMode = textinput.EchoPassword
}
if i == 0 {
ti.Focus()
}
a.inputs[i] = ti
}
a.focusIdx = 0
a.pushView(ViewUsersCreate)
return a, nil
}
func (a App) openUserResetForm() (App, tea.Cmd) {
a.labels = []string{"New Password", "Confirm Password"}
a.inputs = make([]textinput.Model, 2)
for i := range a.inputs {
ti := textinput.New()
ti.CharLimit = 128
ti.Width = 40
ti.EchoMode = textinput.EchoPassword
if i == 0 {
ti.Focus()
}
a.inputs[i] = ti
}
a.focusIdx = 0
a.pushView(ViewUsersReset)
return a, nil
}
func (a App) submitUserCreate() (App, tea.Cmd) {
username := a.inputs[0].Value()
password := a.inputs[1].Value()
confirm := a.inputs[2].Value()
if username == "" {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Username cannot be empty."}, IsError: true}
}
}
if len(password) < 4 {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Password must be at least 4 characters."}, IsError: true}
}
}
if password != confirm {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Passwords do not match."}, IsError: true}
}
}
dir := findAutarchDir()
err := users.CreateUser(dir, username, password)
a.popView()
if err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
}
return a, func() tea.Msg {
return ResultMsg{
Title: "User Created",
Lines: []string{
"Username: " + username,
"Password: (set)",
"",
"Restart the web dashboard for changes to take effect.",
},
}
}
}
func (a App) submitUserReset() (App, tea.Cmd) {
password := a.inputs[0].Value()
confirm := a.inputs[1].Value()
if len(password) < 4 {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Password must be at least 4 characters."}, IsError: true}
}
}
if password != confirm {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{"Passwords do not match."}, IsError: true}
}
}
dir := findAutarchDir()
err := users.ResetPassword(dir, password)
a.popView()
if err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
}
return a, func() tea.Msg {
return ResultMsg{
Title: "Password Reset",
Lines: []string{"Password has been updated.", "", "Force change on next login: NO"},
}
}
}
func (a App) forcePasswordChange() (App, tea.Cmd) {
dir := findAutarchDir()
err := users.SetForceChange(dir, true)
if err != nil {
return a, func() tea.Msg {
return ResultMsg{Title: "Error", Lines: []string{err.Error()}, IsError: true}
}
}
return a, func() tea.Msg {
return ResultMsg{
Title: "Force Change Enabled",
Lines: []string{"User will be required to change password on next login."},
}
}
}

View File

@ -0,0 +1,114 @@
// Package users manages AUTARCH web dashboard credentials.
// Credentials are stored in data/web_credentials.json as bcrypt hashes.
package users
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"golang.org/x/crypto/bcrypt"
)
// Credentials matches the Python web_credentials.json format.
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
ForceChange bool `json:"force_change"`
}
func credentialsPath(autarchDir string) string {
return filepath.Join(autarchDir, "data", "web_credentials.json")
}
// LoadCredentials reads the current credentials from disk.
func LoadCredentials(autarchDir string) (*Credentials, error) {
path := credentialsPath(autarchDir)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read credentials: %w", err)
}
var creds Credentials
if err := json.Unmarshal(data, &creds); err != nil {
return nil, fmt.Errorf("parse credentials: %w", err)
}
return &creds, nil
}
// SaveCredentials writes credentials to disk.
func SaveCredentials(autarchDir string, creds *Credentials) error {
path := credentialsPath(autarchDir)
// Ensure data directory exists
os.MkdirAll(filepath.Dir(path), 0755)
data, err := json.MarshalIndent(creds, "", " ")
if err != nil {
return fmt.Errorf("marshal credentials: %w", err)
}
if err := os.WriteFile(path, data, 0600); err != nil {
return fmt.Errorf("write credentials: %w", err)
}
return nil
}
// CreateUser creates a new user with bcrypt-hashed password.
func CreateUser(autarchDir, username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
creds := &Credentials{
Username: username,
Password: string(hash),
ForceChange: false,
}
return SaveCredentials(autarchDir, creds)
}
// ResetPassword changes the password for the existing user.
func ResetPassword(autarchDir, newPassword string) error {
creds, err := LoadCredentials(autarchDir)
if err != nil {
// If no file exists, create with default username
creds = &Credentials{Username: "admin"}
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
creds.Password = string(hash)
creds.ForceChange = false
return SaveCredentials(autarchDir, creds)
}
// SetForceChange sets the force_change flag.
func SetForceChange(autarchDir string, force bool) error {
creds, err := LoadCredentials(autarchDir)
if err != nil {
return err
}
creds.ForceChange = force
return SaveCredentials(autarchDir, creds)
}
// ResetToDefaults resets credentials to admin/admin with force change.
func ResetToDefaults(autarchDir string) error {
hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
creds := &Credentials{
Username: "admin",
Password: string(hash),
ForceChange: true,
}
return SaveCredentials(autarchDir, creds)
}

View File

@ -0,0 +1,12 @@
#!/bin/bash
# Build Setec App Manager for Debian 13 (linux/amd64)
set -e
echo "Building Setec App Manager..."
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o setec-manager ./cmd/
echo "Binary: setec-manager ($(du -h setec-manager | cut -f1))"
echo ""
echo "Deploy to VPS:"
echo " scp setec-manager root@<your-vps>:/opt/setec-manager/"
echo " ssh root@<your-vps> '/opt/setec-manager/setec-manager --setup'"

View File

@ -0,0 +1,258 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"setec-manager/internal/config"
"setec-manager/internal/db"
"setec-manager/internal/deploy"
"setec-manager/internal/nginx"
"setec-manager/internal/scheduler"
"setec-manager/internal/server"
)
const banner = `
A P P M A N A G E R v1.0
darkHal Security Group & Setec Security Labs
`
func main() {
configPath := flag.String("config", "/opt/setec-manager/config.yaml", "Path to config file")
setup := flag.Bool("setup", false, "Run first-time setup")
flag.Parse()
fmt.Print(banner)
// Load config
cfg, err := config.Load(*configPath)
if err != nil {
log.Fatalf("[setec] Failed to load config: %v", err)
}
// Open database
database, err := db.Open(cfg.Database.Path)
if err != nil {
log.Fatalf("[setec] Failed to open database: %v", err)
}
defer database.Close()
// First-time setup
if *setup {
runSetup(cfg, database, *configPath)
return
}
// Check if any admin users exist
count, _ := database.ManagerUserCount()
if count == 0 {
log.Println("[setec] No admin users found. Creating default admin account.")
log.Println("[setec] Username: admin")
log.Println("[setec] Password: autarch")
log.Println("[setec] ** CHANGE THIS IMMEDIATELY **")
database.CreateManagerUser("admin", "autarch", "admin")
}
// Load or create persistent JWT key
dataDir := filepath.Dir(cfg.Database.Path)
jwtKey, err := server.LoadOrCreateJWTKey(dataDir)
if err != nil {
log.Fatalf("[setec] Failed to load JWT key: %v", err)
}
// Create and start server
srv := server.New(cfg, database, jwtKey)
// Start scheduler
sched := scheduler.New(database)
sched.RegisterHandler(scheduler.JobSSLRenew, func(siteID *int64) error {
log.Println("[scheduler] Running SSL renewal")
_, err := exec.Command("certbot", "renew", "--non-interactive").CombinedOutput()
return err
})
sched.RegisterHandler(scheduler.JobCleanup, func(siteID *int64) error {
log.Println("[scheduler] Running cleanup")
return nil
})
sched.RegisterHandler(scheduler.JobBackup, func(siteID *int64) error {
if siteID == nil {
log.Println("[scheduler] Backup job requires a site ID, skipping")
return fmt.Errorf("backup job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("backup: site %d not found", *siteID)
}
backupDir := cfg.Backups.Dir
os.MkdirAll(backupDir, 0755)
timestamp := time.Now().Format("20060102-150405")
filename := fmt.Sprintf("site-%s-%s.tar.gz", site.Domain, timestamp)
backupPath := filepath.Join(backupDir, filename)
cmd := exec.Command("tar", "-czf", backupPath, "-C", filepath.Dir(site.AppRoot), filepath.Base(site.AppRoot))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("backup tar failed: %s: %w", string(out), err)
}
info, _ := os.Stat(backupPath)
size := int64(0)
if info != nil {
size = info.Size()
}
database.CreateBackup(siteID, "site", backupPath, size)
log.Printf("[scheduler] Backup complete for site %s: %s (%d bytes)", site.Domain, backupPath, size)
return nil
})
sched.RegisterHandler(scheduler.JobGitPull, func(siteID *int64) error {
if siteID == nil {
return fmt.Errorf("git_pull job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("git_pull: site %d not found", *siteID)
}
if site.GitRepo == "" {
return fmt.Errorf("git_pull: site %s has no git repo configured", site.Domain)
}
output, err := deploy.Pull(site.AppRoot)
if err != nil {
return fmt.Errorf("git_pull %s: %w", site.Domain, err)
}
log.Printf("[scheduler] Git pull for site %s: %s", site.Domain, strings.TrimSpace(output))
return nil
})
sched.RegisterHandler(scheduler.JobRestart, func(siteID *int64) error {
if siteID == nil {
return fmt.Errorf("restart job requires a site ID")
}
site, err := database.GetSite(*siteID)
if err != nil || site == nil {
return fmt.Errorf("restart: site %d not found", *siteID)
}
unitName := fmt.Sprintf("app-%s", site.Domain)
if err := deploy.Restart(unitName); err != nil {
return fmt.Errorf("restart %s: %w", site.Domain, err)
}
log.Printf("[scheduler] Restarted service for site %s (unit: %s)", site.Domain, unitName)
return nil
})
sched.Start()
// Graceful shutdown
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGTERM)
go func() {
if err := srv.Start(); err != nil {
log.Fatalf("[setec] Server error: %v", err)
}
}()
log.Printf("[setec] Dashboard: https://%s:%d", cfg.Server.Host, cfg.Server.Port)
<-done
log.Println("[setec] Shutting down...")
sched.Stop()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
func runSetup(cfg *config.Config, database *db.DB, configPath string) {
log.Println("[setup] Starting first-time setup...")
// Ensure directories exist
dirs := []string{
"/opt/setec-manager/data",
"/opt/setec-manager/data/acme",
"/opt/setec-manager/data/backups",
cfg.Nginx.Webroot,
cfg.Nginx.CertbotWebroot,
cfg.Nginx.SitesAvailable,
cfg.Nginx.SitesEnabled,
}
for _, d := range dirs {
os.MkdirAll(d, 0755)
}
// Install Nginx if needed
log.Println("[setup] Installing nginx...")
execQuiet("apt-get", "update", "-qq")
execQuiet("apt-get", "install", "-y", "nginx", "certbot", "ufw")
// Install nginx snippets
log.Println("[setup] Configuring nginx snippets...")
nginx.InstallSnippets(cfg)
// Create admin user
count, _ := database.ManagerUserCount()
if count == 0 {
log.Println("[setup] Creating default admin user (admin / autarch)")
database.CreateManagerUser("admin", "autarch", "admin")
}
// Save config
cfg.Save(configPath)
// Generate self-signed cert for manager if none exists
if _, err := os.Stat(cfg.Server.Cert); os.IsNotExist(err) {
log.Println("[setup] Generating self-signed TLS cert for manager...")
os.MkdirAll(cfg.ACME.AccountDir, 0755)
execQuiet("openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", cfg.Server.Key, "-out", cfg.Server.Cert,
"-days", "3650", "-nodes",
"-subj", "/CN=setec-manager/O=Setec Security Labs")
}
// Install systemd unit for setec-manager
unit := `[Unit]
Description=Setec App Manager
After=network.target
[Service]
Type=simple
User=root
ExecStart=/opt/setec-manager/setec-manager --config /opt/setec-manager/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`
os.WriteFile("/etc/systemd/system/setec-manager.service", []byte(unit), 0644)
execQuiet("systemctl", "daemon-reload")
log.Println("[setup] Setup complete!")
log.Println("[setup] Start with: systemctl start setec-manager")
log.Printf("[setup] Dashboard will be at: https://<your-ip>:%d\n", cfg.Server.Port)
}
func execQuiet(name string, args ...string) {
log.Printf("[setup] $ %s %s", name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("[setup] Warning: %v\n%s", err, string(out))
}
}

View File

@ -0,0 +1,44 @@
server:
host: "0.0.0.0"
port: 9090
tls: true
cert: "/opt/setec-manager/data/acme/manager.crt"
key: "/opt/setec-manager/data/acme/manager.key"
database:
path: "/opt/setec-manager/data/setec.db"
nginx:
sites_available: "/etc/nginx/sites-available"
sites_enabled: "/etc/nginx/sites-enabled"
snippets: "/etc/nginx/snippets"
webroot: "/var/www"
certbot_webroot: "/var/www/certbot"
acme:
email: ""
staging: false
account_dir: "/opt/setec-manager/data/acme"
autarch:
install_dir: "/var/www/autarch"
git_repo: "https://github.com/DigijEth/autarch.git"
git_branch: "main"
web_port: 8181
dns_port: 53
float:
enabled: false
max_sessions: 10
session_ttl: "24h"
backups:
dir: "/opt/setec-manager/data/backups"
max_age_days: 30
max_count: 50
logging:
level: "info"
file: "/var/log/setec-manager.log"
max_size_mb: 100
max_backups: 3

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,790 @@
# Custom Hosting Provider Guide
This guide walks you through creating a new hosting provider integration for Setec Manager. By the end, you will have a provider package that auto-registers with the system and can be used through the same unified API as the built-in Hostinger provider.
---
## Prerequisites
- Go 1.25+ (matching the project's `go.mod`)
- Familiarity with the Go `interface` pattern and HTTP client programming
- An API key or credentials for the hosting provider you are integrating
- A checkout of the `setec-manager` repository
---
## Project Structure
Provider implementations live under `internal/hosting/<provider_name>/`. Each provider is its own Go package.
```
internal/hosting/
provider.go -- Provider interface + model types + registry
store.go -- ProviderConfig, ProviderConfigStore
config.go -- Legacy config store
hostinger/ -- Built-in Hostinger provider
client.go -- HTTP client, auth, retry logic
dns.go -- DNS record operations
myprovider/ -- Your new provider (create this)
provider.go -- init() registration + interface methods
client.go -- HTTP client for the provider's API
dns.go -- (optional) DNS-specific logic
domains.go -- (optional) Domain-specific logic
vms.go -- (optional) VPS-specific logic
```
You can organize files however you like within the package; the only requirement is that the package calls `hosting.Register(...)` in an `init()` function.
---
## The Provider Interface
The `Provider` interface is defined in `internal/hosting/provider.go`. Every provider must implement all methods. Methods that your provider does not support should return `ErrNotSupported`.
```go
type Provider interface {
// Identity
Name() string
DisplayName() string
// Configuration
Configure(config map[string]string) error
TestConnection() error
// DNS
ListDNSRecords(domain string) ([]DNSRecord, error)
CreateDNSRecord(domain string, record DNSRecord) error
UpdateDNSRecords(domain string, records []DNSRecord, overwrite bool) error
DeleteDNSRecord(domain string, recordName, recordType string) error
ResetDNSRecords(domain string) error
// Domains
ListDomains() ([]Domain, error)
GetDomain(domain string) (*Domain, error)
CheckDomainAvailability(domains []string) ([]DomainAvailability, error)
PurchaseDomain(req DomainPurchaseRequest) (*Domain, error)
SetNameservers(domain string, nameservers []string) error
EnableDomainLock(domain string) error
DisableDomainLock(domain string) error
EnablePrivacyProtection(domain string) error
DisablePrivacyProtection(domain string) error
// VMs / VPS
ListVMs() ([]VM, error)
GetVM(id string) (*VM, error)
CreateVM(req VMCreateRequest) (*VM, error)
ListDataCenters() ([]DataCenter, error)
ListSSHKeys() ([]SSHKey, error)
AddSSHKey(name, publicKey string) (*SSHKey, error)
DeleteSSHKey(id string) error
// Billing
ListSubscriptions() ([]Subscription, error)
GetCatalog() ([]CatalogItem, error)
}
```
### Method Reference
#### Identity Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `Name()` | - | `string` | Short machine-readable name (lowercase, no spaces). Used as the registry key and in API URLs. Example: `"hostinger"`, `"cloudflare"`. |
| `DisplayName()` | - | `string` | Human-readable name shown in the UI. Example: `"Hostinger"`, `"Cloudflare"`. |
#### Configuration Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `Configure(config)` | `map[string]string` -- key-value config pairs. Common keys: `"api_key"`, `"api_secret"`, `"base_url"`. | `error` | Called when a user saves credentials. Store them in struct fields. Validate format but do not make API calls. |
| `TestConnection()` | - | `error` | Make a lightweight API call (e.g., list domains) to verify credentials are valid. Return `nil` on success. |
#### DNS Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `ListDNSRecords(domain)` | `domain string` -- the FQDN | `([]DNSRecord, error)` | Return all DNS records for the zone. |
| `CreateDNSRecord(domain, record)` | `domain string`, `record DNSRecord` | `error` | Add a single record without affecting existing records. |
| `UpdateDNSRecords(domain, records, overwrite)` | `domain string`, `records []DNSRecord`, `overwrite bool` | `error` | Batch update. If `overwrite` is true, replace all records; otherwise merge. |
| `DeleteDNSRecord(domain, recordName, recordType)` | `domain string`, `recordName string` (subdomain or `@`), `recordType string` (e.g. `"A"`) | `error` | Delete matching records. |
| `ResetDNSRecords(domain)` | `domain string` | `error` | Reset the zone to provider defaults. |
#### Domain Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `ListDomains()` | - | `([]Domain, error)` | Return all domains on the account. |
| `GetDomain(domain)` | `domain string` | `(*Domain, error)` | Return details for a single domain. |
| `CheckDomainAvailability(domains)` | `domains []string` | `([]DomainAvailability, error)` | Check if domains are available for registration and return pricing. |
| `PurchaseDomain(req)` | `req DomainPurchaseRequest` | `(*Domain, error)` | Register a new domain. |
| `SetNameservers(domain, nameservers)` | `domain string`, `nameservers []string` | `error` | Update the authoritative nameservers. |
| `EnableDomainLock(domain)` | `domain string` | `error` | Enable registrar lock (transfer protection). |
| `DisableDomainLock(domain)` | `domain string` | `error` | Disable registrar lock. |
| `EnablePrivacyProtection(domain)` | `domain string` | `error` | Enable WHOIS privacy. |
| `DisablePrivacyProtection(domain)` | `domain string` | `error` | Disable WHOIS privacy. |
#### VM / VPS Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `ListVMs()` | - | `([]VM, error)` | Return all VPS instances on the account. |
| `GetVM(id)` | `id string` | `(*VM, error)` | Return details for a single VM. |
| `CreateVM(req)` | `req VMCreateRequest` | `(*VM, error)` | Provision a new VPS instance. |
| `ListDataCenters()` | - | `([]DataCenter, error)` | Return available regions/data centers. |
| `ListSSHKeys()` | - | `([]SSHKey, error)` | Return all stored SSH public keys. |
| `AddSSHKey(name, publicKey)` | `name string`, `publicKey string` | `(*SSHKey, error)` | Upload a new SSH public key. |
| `DeleteSSHKey(id)` | `id string` | `error` | Remove an SSH key. |
#### Billing Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| `ListSubscriptions()` | - | `([]Subscription, error)` | Return all active subscriptions. |
| `GetCatalog()` | - | `([]CatalogItem, error)` | Return purchasable products and plans. |
---
## Type Reference
All model types are defined in `internal/hosting/provider.go`.
### DNSRecord
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Provider-assigned identifier. May be synthesized (e.g., `name/type/priority`). Optional on create. |
| `Type` | `string` | `type` | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SRV`, `CAA`. |
| `Name` | `string` | `name` | Subdomain label or `@` for the zone apex. |
| `Content` | `string` | `content` | Record value (IP address, hostname, text, etc.). |
| `TTL` | `int` | `ttl` | Time-to-live in seconds. |
| `Priority` | `int` | `priority` | Priority value for MX and SRV records. Zero for other types. |
### Domain
| Field | Type | JSON | Description |
|---|---|---|---|
| `Name` | `string` | `name` | Fully qualified domain name. |
| `Registrar` | `string` | `registrar` | Registrar name (optional). |
| `Status` | `string` | `status` | Registration status (e.g., `"active"`, `"expired"`, `"pending"`). |
| `ExpiresAt` | `time.Time` | `expires_at` | Expiration date. |
| `AutoRenew` | `bool` | `auto_renew` | Whether automatic renewal is enabled. |
| `Locked` | `bool` | `locked` | Whether transfer lock is enabled. |
| `PrivacyProtection` | `bool` | `privacy_protection` | Whether WHOIS privacy is enabled. |
| `Nameservers` | `[]string` | `nameservers` | Current authoritative nameservers. |
### DomainAvailability
| Field | Type | JSON | Description |
|---|---|---|---|
| `Domain` | `string` | `domain` | The queried domain name. |
| `Available` | `bool` | `available` | Whether the domain is available for registration. |
| `Price` | `float64` | `price` | Purchase price (zero if unavailable). |
| `Currency` | `string` | `currency` | Currency code (e.g., `"USD"`). |
### DomainPurchaseRequest
| Field | Type | JSON | Description |
|---|---|---|---|
| `Domain` | `string` | `domain` | Domain to purchase. |
| `Period` | `int` | `period` | Registration period in years. |
| `AutoRenew` | `bool` | `auto_renew` | Enable auto-renewal. |
| `Privacy` | `bool` | `privacy_protection` | Enable WHOIS privacy. |
| `PaymentID` | `string` | `payment_method_id` | Payment method identifier (optional, provider-specific). |
### VM
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Provider-assigned VM identifier. |
| `Name` | `string` | `name` | Human-readable VM name / hostname. |
| `Status` | `string` | `status` | Current state: `"running"`, `"stopped"`, `"creating"`, `"error"`. |
| `Plan` | `string` | `plan` | Plan/tier identifier. |
| `Region` | `string` | `region` | Data center / region identifier. |
| `IPv4` | `string` | `ipv4` | Public IPv4 address (optional). |
| `IPv6` | `string` | `ipv6` | Public IPv6 address (optional). |
| `OS` | `string` | `os` | Operating system template name (optional). |
| `CPUs` | `int` | `cpus` | Number of virtual CPUs. |
| `MemoryMB` | `int` | `memory_mb` | RAM in megabytes. |
| `DiskGB` | `int` | `disk_gb` | Disk size in gigabytes. |
| `BandwidthGB` | `int` | `bandwidth_gb` | Monthly bandwidth allowance in gigabytes. |
| `CreatedAt` | `time.Time` | `created_at` | Creation timestamp. |
| `Labels` | `map[string]string` | `labels` | Arbitrary key-value labels (optional). |
### VMCreateRequest
| Field | Type | JSON | Description |
|---|---|---|---|
| `Plan` | `string` | `plan` | Plan/tier identifier from the catalog. |
| `DataCenterID` | `string` | `data_center_id` | Target data center from `ListDataCenters()`. |
| `Template` | `string` | `template` | OS template identifier. |
| `Password` | `string` | `password` | Root/admin password for the VM. |
| `Hostname` | `string` | `hostname` | Desired hostname. |
| `SSHKeyID` | `string` | `ssh_key_id` | SSH key to install (optional). |
| `PaymentID` | `string` | `payment_method_id` | Payment method identifier (optional). |
### DataCenter
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Unique identifier used in `VMCreateRequest`. |
| `Name` | `string` | `name` | Short name (e.g., `"US East"`). |
| `Location` | `string` | `location` | City or locality. |
| `Country` | `string` | `country` | ISO country code. |
### SSHKey
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Provider-assigned key identifier. |
| `Name` | `string` | `name` | User-assigned label. |
| `Fingerprint` | `string` | `fingerprint` | Key fingerprint (e.g., `"SHA256:..."`). |
| `PublicKey` | `string` | `public_key` | Full public key string. |
### Subscription
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Subscription identifier. |
| `Name` | `string` | `name` | Product name. |
| `Status` | `string` | `status` | Status: `"active"`, `"cancelled"`, `"expired"`. |
| `Plan` | `string` | `plan` | Plan identifier. |
| `Price` | `float64` | `price` | Recurring price. |
| `Currency` | `string` | `currency` | Currency code. |
| `RenewsAt` | `time.Time` | `renews_at` | Next renewal date. |
| `CreatedAt` | `time.Time` | `created_at` | Subscription start date. |
### CatalogItem
| Field | Type | JSON | Description |
|---|---|---|---|
| `ID` | `string` | `id` | Product/plan identifier. |
| `Name` | `string` | `name` | Product name. |
| `Category` | `string` | `category` | Category: `"vps"`, `"hosting"`, `"domain"`, etc. |
| `PriceCents` | `int` | `price_cents` | Price in cents (e.g., 1199 = $11.99). |
| `Currency` | `string` | `currency` | Currency code. |
| `Period` | `string` | `period` | Billing period: `"monthly"`, `"yearly"`. |
| `Description` | `string` | `description` | Human-readable description (optional). |
### ProviderConfig
Stored in `internal/hosting/store.go`. This is the credential record persisted to disk.
| Field | Type | JSON | Description |
|---|---|---|---|
| `Provider` | `string` | `provider` | Provider name (must match `Provider.Name()`). |
| `APIKey` | `string` | `api_key` | Primary API key or bearer token. |
| `APISecret` | `string` | `api_secret` | Secondary secret (optional, provider-specific). |
| `Extra` | `map[string]string` | `extra` | Additional provider-specific config values. |
| `Connected` | `bool` | `connected` | Whether the last `TestConnection()` succeeded. |
---
## Implementing the Interface
### Step 1: Create the Package
```bash
mkdir -p internal/hosting/myprovider
```
### Step 2: Implement the Provider
Create `internal/hosting/myprovider/provider.go`:
```go
package myprovider
import (
"errors"
"fmt"
"net/http"
"time"
"setec-manager/internal/hosting"
)
// ErrNotSupported is returned by methods this provider does not implement.
var ErrNotSupported = errors.New("myprovider: operation not supported")
// Provider implements hosting.Provider for the MyProvider service.
type Provider struct {
client *http.Client
apiKey string
baseURL string
}
// init registers this provider with the hosting registry.
// This runs automatically when the package is imported.
func init() {
hosting.Register(&Provider{
client: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: "https://api.myprovider.com",
})
}
// ── Identity ────────────────────────────────────────────────────────
func (p *Provider) Name() string { return "myprovider" }
func (p *Provider) DisplayName() string { return "My Provider" }
// ── Configuration ───────────────────────────────────────────────────
func (p *Provider) Configure(config map[string]string) error {
key, ok := config["api_key"]
if !ok || key == "" {
return fmt.Errorf("myprovider: api_key is required")
}
p.apiKey = key
if baseURL, ok := config["base_url"]; ok && baseURL != "" {
p.baseURL = baseURL
}
return nil
}
func (p *Provider) TestConnection() error {
// Make a lightweight API call to verify credentials.
// For example, list domains or get account info.
_, err := p.ListDomains()
return err
}
// ── DNS ─────────────────────────────────────────────────────────────
func (p *Provider) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) {
// TODO: Implement API call to list DNS records
return nil, ErrNotSupported
}
func (p *Provider) CreateDNSRecord(domain string, record hosting.DNSRecord) error {
return ErrNotSupported
}
func (p *Provider) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error {
return ErrNotSupported
}
func (p *Provider) DeleteDNSRecord(domain string, recordName, recordType string) error {
return ErrNotSupported
}
func (p *Provider) ResetDNSRecords(domain string) error {
return ErrNotSupported
}
// ── Domains ─────────────────────────────────────────────────────────
func (p *Provider) ListDomains() ([]hosting.Domain, error) {
return nil, ErrNotSupported
}
func (p *Provider) GetDomain(domain string) (*hosting.Domain, error) {
return nil, ErrNotSupported
}
func (p *Provider) CheckDomainAvailability(domains []string) ([]hosting.DomainAvailability, error) {
return nil, ErrNotSupported
}
func (p *Provider) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.Domain, error) {
return nil, ErrNotSupported
}
func (p *Provider) SetNameservers(domain string, nameservers []string) error {
return ErrNotSupported
}
func (p *Provider) EnableDomainLock(domain string) error { return ErrNotSupported }
func (p *Provider) DisableDomainLock(domain string) error { return ErrNotSupported }
func (p *Provider) EnablePrivacyProtection(domain string) error { return ErrNotSupported }
func (p *Provider) DisablePrivacyProtection(domain string) error { return ErrNotSupported }
// ── VMs / VPS ───────────────────────────────────────────────────────
func (p *Provider) ListVMs() ([]hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) GetVM(id string) (*hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) CreateVM(req hosting.VMCreateRequest) (*hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) ListDataCenters() ([]hosting.DataCenter, error) { return nil, ErrNotSupported }
func (p *Provider) ListSSHKeys() ([]hosting.SSHKey, error) { return nil, ErrNotSupported }
func (p *Provider) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) { return nil, ErrNotSupported }
func (p *Provider) DeleteSSHKey(id string) error { return ErrNotSupported }
// ── Billing ─────────────────────────────────────────────────────────
func (p *Provider) ListSubscriptions() ([]hosting.Subscription, error) { return nil, ErrNotSupported }
func (p *Provider) GetCatalog() ([]hosting.CatalogItem, error) { return nil, ErrNotSupported }
```
---
## Registration
Registration happens automatically via Go's `init()` mechanism. When the main binary imports the provider package (even as a side-effect import), the `init()` function runs and calls `hosting.Register()`.
In `cmd/main.go` (or wherever the binary entry point is), add a blank import:
```go
import (
// Register hosting providers
_ "setec-manager/internal/hosting/hostinger"
_ "setec-manager/internal/hosting/myprovider"
)
```
The `hosting.Register()` function stores the provider instance in a global `map[string]Provider` protected by a `sync.RWMutex`:
```go
// From internal/hosting/provider.go
func Register(p Provider) {
registryMu.Lock()
defer registryMu.Unlock()
registry[p.Name()] = p
}
```
After registration, the provider is accessible via `hosting.Get("myprovider")` and appears in `hosting.List()`.
---
## Configuration Storage
When a user configures your provider (via the UI or API), the system:
1. Calls `provider.Configure(map[string]string{"api_key": "..."})` to set credentials in memory.
2. Calls `provider.TestConnection()` to verify the credentials work.
3. Saves a `ProviderConfig` to disk via `ProviderConfigStore.Save()`.
The config file is written to `<config_dir>/<provider_name>.json` with `0600` permissions:
```json
{
"provider": "myprovider",
"api_key": "sk-abc123...",
"api_secret": "",
"extra": {
"base_url": "https://api.myprovider.com/v2"
},
"connected": true
}
```
On startup, `ProviderConfigStore.loadAll()` reads all JSON files from the config directory, and for each one that matches a registered provider, calls `Configure()` to restore credentials.
---
## Error Handling
### The ErrNotSupported Pattern
Define a sentinel error in your provider package:
```go
var ErrNotSupported = errors.New("myprovider: operation not supported")
```
Return this error from any interface method your provider does not implement. The HTTP handler layer checks for this error and returns HTTP 501 (Not Implemented) to the client.
### API Errors
For errors from the upstream provider API, return a descriptive error with context:
```go
return fmt.Errorf("myprovider: list domains: %w", err)
```
### Rate Limiting
If the provider has rate limits, handle them inside your client. See the Hostinger implementation in `internal/hosting/hostinger/client.go` for a reference pattern:
1. Check for HTTP 429 responses.
2. Read the `Retry-After` header.
3. Sleep and retry (up to a maximum number of retries).
4. Return a clear error if retries are exhausted.
```go
if resp.StatusCode == http.StatusTooManyRequests {
retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
if attempt < maxRetries {
time.Sleep(retryAfter)
continue
}
return fmt.Errorf("myprovider: rate limited after %d retries", maxRetries)
}
```
---
## Testing
### Unit Tests
Create `internal/hosting/myprovider/provider_test.go`:
```go
package myprovider
import (
"testing"
"setec-manager/internal/hosting"
)
func TestProviderImplementsInterface(t *testing.T) {
var _ hosting.Provider = (*Provider)(nil)
}
func TestName(t *testing.T) {
p := &Provider{}
if p.Name() != "myprovider" {
t.Errorf("expected name 'myprovider', got %q", p.Name())
}
}
func TestConfigure(t *testing.T) {
p := &Provider{}
err := p.Configure(map[string]string{})
if err == nil {
t.Error("expected error when api_key is missing")
}
err = p.Configure(map[string]string{"api_key": "test-key"})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if p.apiKey != "test-key" {
t.Errorf("expected apiKey 'test-key', got %q", p.apiKey)
}
}
func TestUnsupportedMethodsReturnError(t *testing.T) {
p := &Provider{}
_, err := p.ListVMs()
if err != ErrNotSupported {
t.Errorf("ListVMs: expected ErrNotSupported, got %v", err)
}
_, err = p.GetCatalog()
if err != ErrNotSupported {
t.Errorf("GetCatalog: expected ErrNotSupported, got %v", err)
}
}
```
### Integration Tests
For integration tests against the real API, use build tags to prevent them from running in CI:
```go
//go:build integration
package myprovider
import (
"os"
"testing"
)
func TestListDomainsIntegration(t *testing.T) {
key := os.Getenv("MYPROVIDER_API_KEY")
if key == "" {
t.Skip("MYPROVIDER_API_KEY not set")
}
p := &Provider{}
p.Configure(map[string]string{"api_key": key})
domains, err := p.ListDomains()
if err != nil {
t.Fatalf("ListDomains failed: %v", err)
}
t.Logf("Found %d domains", len(domains))
}
```
Run integration tests:
```bash
go test -tags=integration ./internal/hosting/myprovider/ -v
```
### Registration Test
Verify that importing the package registers the provider:
```go
package myprovider_test
import (
"testing"
"setec-manager/internal/hosting"
_ "setec-manager/internal/hosting/myprovider"
)
func TestRegistration(t *testing.T) {
p, err := hosting.Get("myprovider")
if err != nil {
t.Fatalf("provider not registered: %v", err)
}
if p.DisplayName() == "" {
t.Error("DisplayName is empty")
}
}
```
---
## Example: Skeleton Provider (DNS Only)
This is a complete, minimal provider that implements only DNS management. All other methods return `ErrNotSupported`. You can copy this file and fill in the DNS methods with real API calls.
```go
package dnsonlyprovider
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"setec-manager/internal/hosting"
)
var ErrNotSupported = errors.New("dnsonlyprovider: operation not supported")
type Provider struct {
client *http.Client
apiKey string
baseURL string
}
func init() {
hosting.Register(&Provider{
client: &http.Client{Timeout: 30 * time.Second},
baseURL: "https://api.dns-only.example.com/v1",
})
}
func (p *Provider) Name() string { return "dnsonlyprovider" }
func (p *Provider) DisplayName() string { return "DNS-Only Provider" }
func (p *Provider) Configure(config map[string]string) error {
key, ok := config["api_key"]
if !ok || key == "" {
return fmt.Errorf("dnsonlyprovider: api_key is required")
}
p.apiKey = key
return nil
}
func (p *Provider) TestConnection() error {
// Try listing zones as a health check.
req, _ := http.NewRequest("GET", p.baseURL+"/zones", nil)
req.Header.Set("Authorization", "Bearer "+p.apiKey)
resp, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("dnsonlyprovider: connection failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("dnsonlyprovider: API returned %d: %s", resp.StatusCode, body)
}
return nil
}
// ── DNS (implemented) ───────────────────────────────────────────────
func (p *Provider) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) {
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/records", p.baseURL, domain), nil)
req.Header.Set("Authorization", "Bearer "+p.apiKey)
resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("dnsonlyprovider: list records: %w", err)
}
defer resp.Body.Close()
var records []hosting.DNSRecord
if err := json.NewDecoder(resp.Body).Decode(&records); err != nil {
return nil, fmt.Errorf("dnsonlyprovider: parse records: %w", err)
}
return records, nil
}
func (p *Provider) CreateDNSRecord(domain string, record hosting.DNSRecord) error {
// Implementation: POST to /zones/{domain}/records
return ErrNotSupported // replace with real implementation
}
func (p *Provider) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error {
// Implementation: PUT to /zones/{domain}/records
return ErrNotSupported // replace with real implementation
}
func (p *Provider) DeleteDNSRecord(domain string, recordName, recordType string) error {
// Implementation: DELETE /zones/{domain}/records?name=...&type=...
return ErrNotSupported // replace with real implementation
}
func (p *Provider) ResetDNSRecords(domain string) error {
return ErrNotSupported
}
// ── Everything else: not supported ──────────────────────────────────
func (p *Provider) ListDomains() ([]hosting.Domain, error) { return nil, ErrNotSupported }
func (p *Provider) GetDomain(domain string) (*hosting.Domain, error) { return nil, ErrNotSupported }
func (p *Provider) CheckDomainAvailability(domains []string) ([]hosting.DomainAvailability, error) { return nil, ErrNotSupported }
func (p *Provider) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.Domain, error) { return nil, ErrNotSupported }
func (p *Provider) SetNameservers(domain string, nameservers []string) error { return ErrNotSupported }
func (p *Provider) EnableDomainLock(domain string) error { return ErrNotSupported }
func (p *Provider) DisableDomainLock(domain string) error { return ErrNotSupported }
func (p *Provider) EnablePrivacyProtection(domain string) error { return ErrNotSupported }
func (p *Provider) DisablePrivacyProtection(domain string) error { return ErrNotSupported }
func (p *Provider) ListVMs() ([]hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) GetVM(id string) (*hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) CreateVM(req hosting.VMCreateRequest) (*hosting.VM, error) { return nil, ErrNotSupported }
func (p *Provider) ListDataCenters() ([]hosting.DataCenter, error) { return nil, ErrNotSupported }
func (p *Provider) ListSSHKeys() ([]hosting.SSHKey, error) { return nil, ErrNotSupported }
func (p *Provider) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) { return nil, ErrNotSupported }
func (p *Provider) DeleteSSHKey(id string) error { return ErrNotSupported }
func (p *Provider) ListSubscriptions() ([]hosting.Subscription, error) { return nil, ErrNotSupported }
func (p *Provider) GetCatalog() ([]hosting.CatalogItem, error) { return nil, ErrNotSupported }
```
---
## Example: Full Provider Structure
For a provider that implements all capabilities, organize the code across multiple files:
```
internal/hosting/fullprovider/
provider.go -- init(), Name(), DisplayName(), Configure(), TestConnection()
client.go -- HTTP client with auth, retry, rate-limit handling
dns.go -- ListDNSRecords, CreateDNSRecord, UpdateDNSRecords, DeleteDNSRecord, ResetDNSRecords
domains.go -- ListDomains, GetDomain, CheckDomainAvailability, PurchaseDomain, nameserver/lock/privacy methods
vms.go -- ListVMs, GetVM, CreateVM, ListDataCenters
ssh.go -- ListSSHKeys, AddSSHKey, DeleteSSHKey
billing.go -- ListSubscriptions, GetCatalog
types.go -- Provider-specific API request/response types
```
Each file focuses on a single capability area. The `client.go` file provides a shared `doRequest()` method (similar to the Hostinger client) that handles authentication headers, JSON marshaling, error parsing, and retry logic.
### Key Patterns from the Hostinger Implementation
1. **Separate API types from generic types.** Define provider-specific request/response structs (e.g., `hostingerDNSRecord`) and conversion functions (`toGenericDNSRecord`, `toHostingerDNSRecord`).
2. **Validate before mutating.** The Hostinger DNS implementation calls a `/validate` endpoint before applying updates. If your provider offers similar validation, use it.
3. **Synthesize IDs when the API does not provide them.** Hostinger does not return record IDs in zone listings, so the client synthesizes them from `name/type/priority`.
4. **Handle rate limits transparently.** The client retries on HTTP 429 with exponential back-off, capping at 60 seconds per retry and 3 retries total. This keeps rate-limit handling invisible to the caller.

View File

@ -0,0 +1,859 @@
# Hosting Provider Integration System
## Overview
Setec Manager includes a pluggable hosting provider architecture that lets you manage DNS records, domains, VPS instances, SSH keys, and billing subscriptions through a unified interface. The system is built around a Go `Provider` interface defined in `internal/hosting/provider.go`. Each hosting provider (e.g., Hostinger) implements this interface and auto-registers itself at import time via an `init()` function.
### Architecture
```
internal/hosting/
provider.go -- Provider interface, model types, global registry
store.go -- ProviderConfig type, ProviderConfigStore (disk persistence)
config.go -- Legacy config store (being superseded by store.go)
hostinger/
client.go -- Hostinger HTTP client with retry/rate-limit handling
dns.go -- Hostinger DNS implementation
```
The registry is a process-global `map[string]Provider` guarded by a `sync.RWMutex`. Providers call `hosting.Register(&Provider{})` inside their package `init()` function. The main binary imports the provider package (e.g., `_ "setec-manager/internal/hosting/hostinger"`) to trigger registration.
Provider credentials are stored as individual JSON files in a protected directory (`0700` directory, `0600` files) managed by `ProviderConfigStore`. Each file is named `<provider>.json` and contains the `ProviderConfig` struct:
```json
{
"provider": "hostinger",
"api_key": "Bearer ...",
"api_secret": "",
"extra": {},
"connected": true
}
```
---
## Supported Providers
### Hostinger (Built-in)
| Capability | Supported | Notes |
|---|---|---|
| DNS Management | Yes | Full CRUD, validation before writes, zone reset |
| Domain Management | Yes | List, lookup, availability check, purchase, nameservers, lock, privacy |
| VPS Management | Yes | List, create, get details, data center listing |
| SSH Key Management | Yes | Add, list, delete |
| Billing | Yes | Subscriptions and catalog |
The Hostinger provider communicates with `https://developers.hostinger.com` using a Bearer token. It includes automatic retry with back-off on HTTP 429 (rate limit) responses, up to 3 retries per request.
---
## Configuration
### Via the UI
1. Navigate to the Hosting Providers section in the Setec Manager dashboard.
2. Select "Hostinger" from the provider list.
3. Enter your API token (obtained from hPanel -- see [Hostinger Setup Guide](hostinger-setup.md)).
4. Click "Test Connection" to verify the token is valid.
5. Click "Save" to persist the configuration.
### Via Config Files
Provider configurations are stored as JSON files in the config directory (typically `/opt/setec-manager/data/hosting/`).
Create or edit the file directly:
```bash
mkdir -p /opt/setec-manager/data/hosting
cat > /opt/setec-manager/data/hosting/hostinger.json << 'EOF'
{
"provider": "hostinger",
"api_key": "YOUR_BEARER_TOKEN_HERE",
"api_secret": "",
"extra": {},
"connected": true
}
EOF
chmod 600 /opt/setec-manager/data/hosting/hostinger.json
```
### Via API
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_HOSTINGER_API_TOKEN"
}'
```
---
## API Reference
All hosting endpoints require authentication via JWT (cookie or `Authorization: Bearer` header). The base URL is `https://your-server:9090`.
### Provider Management
#### List Providers
```
GET /api/hosting/providers
```
Returns all registered hosting providers and their connection status.
```bash
curl -s https://your-server:9090/api/hosting/providers \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"name": "hostinger",
"display_name": "Hostinger",
"connected": true
}
]
```
#### Configure Provider
```
POST /api/hosting/providers/{provider}/configure
```
Sets the API credentials for a provider.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_TOKEN"
}'
```
**Response:**
```json
{
"status": "configured"
}
```
#### Test Connection
```
POST /api/hosting/providers/{provider}/test
```
Verifies that the saved credentials are valid by making a test API call.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/test \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "ok",
"message": "Connection successful"
}
```
#### Remove Provider Configuration
```
DELETE /api/hosting/providers/{provider}
```
Deletes saved credentials for a provider.
```bash
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "deleted"
}
```
---
## DNS Management
### List DNS Records
```
GET /api/hosting/providers/{provider}/dns/{domain}
```
Returns all DNS records for the specified domain.
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "@/A/0",
"type": "A",
"name": "@",
"content": "93.184.216.34",
"ttl": 14400,
"priority": 0
},
{
"id": "www/CNAME/0",
"type": "CNAME",
"name": "www",
"content": "example.com",
"ttl": 14400,
"priority": 0
},
{
"id": "@/MX/10",
"type": "MX",
"name": "@",
"content": "mail.example.com",
"ttl": 14400,
"priority": 10
}
]
```
### Create DNS Record
```
POST /api/hosting/providers/{provider}/dns/{domain}
```
Adds a new DNS record without overwriting existing records.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "api",
"content": "93.184.216.35",
"ttl": 3600
}'
```
**Response:**
```json
{
"status": "created"
}
```
### Update DNS Records (Batch)
```
PUT /api/hosting/providers/{provider}/dns/{domain}
```
Updates DNS records for a domain. If `overwrite` is `true`, all existing records are replaced; otherwise the records are merged.
The Hostinger provider validates records against the API before applying changes.
```bash
curl -X PUT https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"type": "A",
"name": "@",
"content": "93.184.216.34",
"ttl": 14400
},
{
"type": "CNAME",
"name": "www",
"content": "example.com",
"ttl": 14400
}
],
"overwrite": false
}'
```
**Response:**
```json
{
"status": "updated"
}
```
### Delete DNS Record
```
DELETE /api/hosting/providers/{provider}/dns/{domain}?name={name}&type={type}
```
Removes DNS records matching the given name and type.
```bash
curl -X DELETE "https://your-server:9090/api/hosting/providers/hostinger/dns/example.com?name=api&type=A" \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "deleted"
}
```
### Reset DNS Zone
```
POST /api/hosting/providers/{provider}/dns/{domain}/reset
```
Resets the domain's DNS zone to the provider's default records. This is destructive and removes all custom records.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/dns/example.com/reset \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "reset"
}
```
### Supported DNS Record Types
| Type | Description | Priority Field |
|---|---|---|
| A | IPv4 address | No |
| AAAA | IPv6 address | No |
| CNAME | Canonical name / alias | No |
| MX | Mail exchange | Yes |
| TXT | Text record (SPF, DKIM, etc.) | No |
| NS | Name server | No |
| SRV | Service record | Yes |
| CAA | Certificate Authority Authorization | No |
---
## Domain Management
### List Domains
```
GET /api/hosting/providers/{provider}/domains
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/domains \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"name": "example.com",
"registrar": "Hostinger",
"status": "active",
"expires_at": "2027-03-15T00:00:00Z",
"auto_renew": true,
"locked": true,
"privacy_protection": true,
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
}
]
```
### Get Domain Details
```
GET /api/hosting/providers/{provider}/domains/{domain}
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/domains/example.com \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"name": "example.com",
"registrar": "Hostinger",
"status": "active",
"expires_at": "2027-03-15T00:00:00Z",
"auto_renew": true,
"locked": true,
"privacy_protection": true,
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
}
```
### Check Domain Availability
```
POST /api/hosting/providers/{provider}/domains/check
```
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/check \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domains": ["cool-project.com", "cool-project.io", "cool-project.dev"]
}'
```
**Response:**
```json
[
{
"domain": "cool-project.com",
"available": true,
"price": 9.99,
"currency": "USD"
},
{
"domain": "cool-project.io",
"available": false
},
{
"domain": "cool-project.dev",
"available": true,
"price": 14.99,
"currency": "USD"
}
]
```
### Purchase Domain
```
POST /api/hosting/providers/{provider}/domains/purchase
```
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/purchase \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domain": "cool-project.com",
"period": 1,
"auto_renew": true,
"privacy_protection": true,
"payment_method_id": "pm_abc123"
}'
```
**Response:**
```json
{
"name": "cool-project.com",
"status": "active",
"expires_at": "2027-03-11T00:00:00Z",
"auto_renew": true,
"locked": false,
"privacy_protection": true,
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
}
```
### Set Nameservers
```
PUT /api/hosting/providers/{provider}/domains/{domain}/nameservers
```
```bash
curl -X PUT https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/nameservers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nameservers": ["ns1.cloudflare.com", "ns2.cloudflare.com"]
}'
```
**Response:**
```json
{
"status": "updated"
}
```
### Enable Domain Lock
```
POST /api/hosting/providers/{provider}/domains/{domain}/lock
```
Prevents unauthorized domain transfers.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/lock \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "locked"
}
```
### Disable Domain Lock
```
DELETE /api/hosting/providers/{provider}/domains/{domain}/lock
```
```bash
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/lock \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "unlocked"
}
```
### Enable Privacy Protection
```
POST /api/hosting/providers/{provider}/domains/{domain}/privacy
```
Enables WHOIS privacy protection to hide registrant details.
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "enabled"
}
```
### Disable Privacy Protection
```
DELETE /api/hosting/providers/{provider}/domains/{domain}/privacy
```
```bash
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "disabled"
}
```
---
## VPS Management
### List Virtual Machines
```
GET /api/hosting/providers/{provider}/vms
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/vms \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "vm-abc123",
"name": "production-1",
"status": "running",
"plan": "kvm-2",
"region": "us-east-1",
"ipv4": "93.184.216.34",
"ipv6": "2606:2800:220:1:248:1893:25c8:1946",
"os": "Ubuntu 22.04",
"cpus": 2,
"memory_mb": 4096,
"disk_gb": 80,
"bandwidth_gb": 4000,
"created_at": "2025-01-15T10:30:00Z",
"labels": {
"env": "production"
}
}
]
```
### Get VM Details
```
GET /api/hosting/providers/{provider}/vms/{id}
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/vms/vm-abc123 \
-H "Authorization: Bearer $TOKEN"
```
**Response:** Same shape as a single item from the list response.
### Create VM
```
POST /api/hosting/providers/{provider}/vms
```
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/vms \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"plan": "kvm-2",
"data_center_id": "us-east-1",
"template": "ubuntu-22.04",
"password": "SecurePassword123!",
"hostname": "web-server-2",
"ssh_key_id": "key-abc123",
"payment_method_id": "pm_abc123"
}'
```
**Response:**
```json
{
"id": "vm-def456",
"name": "web-server-2",
"status": "creating",
"plan": "kvm-2",
"region": "us-east-1",
"os": "Ubuntu 22.04",
"cpus": 2,
"memory_mb": 4096,
"disk_gb": 80,
"bandwidth_gb": 4000,
"created_at": "2026-03-11T14:00:00Z"
}
```
### List Data Centers
```
GET /api/hosting/providers/{provider}/datacenters
```
Returns available regions/data centers for VM creation.
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/datacenters \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "us-east-1",
"name": "US East",
"location": "New York",
"country": "US"
},
{
"id": "eu-west-1",
"name": "EU West",
"location": "Amsterdam",
"country": "NL"
}
]
```
---
## SSH Key Management
### List SSH Keys
```
GET /api/hosting/providers/{provider}/ssh-keys
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/ssh-keys \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "key-abc123",
"name": "deploy-key",
"fingerprint": "SHA256:abcd1234...",
"public_key": "ssh-ed25519 AAAAC3Nz..."
}
]
```
### Add SSH Key
```
POST /api/hosting/providers/{provider}/ssh-keys
```
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/ssh-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "new-deploy-key",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host"
}'
```
**Response:**
```json
{
"id": "key-def456",
"name": "new-deploy-key",
"fingerprint": "SHA256:efgh5678...",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
}
```
### Delete SSH Key
```
DELETE /api/hosting/providers/{provider}/ssh-keys/{id}
```
```bash
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/ssh-keys/key-def456 \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
{
"status": "deleted"
}
```
---
## Billing
### List Subscriptions
```
GET /api/hosting/providers/{provider}/subscriptions
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/subscriptions \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "sub-abc123",
"name": "Premium Web Hosting",
"status": "active",
"plan": "premium-hosting-48m",
"price": 2.99,
"currency": "USD",
"renews_at": "2027-03-15T00:00:00Z",
"created_at": "2023-03-15T00:00:00Z"
}
]
```
### Get Product Catalog
```
GET /api/hosting/providers/{provider}/catalog
```
```bash
curl -s https://your-server:9090/api/hosting/providers/hostinger/catalog \
-H "Authorization: Bearer $TOKEN"
```
**Response:**
```json
[
{
"id": "kvm-2",
"name": "KVM 2",
"category": "vps",
"price_cents": 1199,
"currency": "USD",
"period": "monthly",
"description": "2 vCPU, 4 GB RAM, 80 GB SSD"
},
{
"id": "premium-hosting-12m",
"name": "Premium Web Hosting",
"category": "hosting",
"price_cents": 299,
"currency": "USD",
"period": "monthly",
"description": "100 websites, 100 GB SSD, free SSL"
}
]
```
---
## Error Responses
All endpoints return errors in a consistent format:
```json
{
"error": "description of what went wrong"
}
```
| HTTP Status | Meaning |
|---|---|
| 400 | Bad request (invalid parameters) |
| 401 | Authentication required or token invalid |
| 404 | Provider or resource not found |
| 409 | Conflict (e.g., duplicate resource) |
| 429 | Rate limited by the upstream provider |
| 500 | Internal server error |
| 501 | Provider does not support this operation (`ErrNotSupported`) |
When a provider does not implement a particular capability, the endpoint returns HTTP 501 with an `ErrNotSupported` error message. This allows partial implementations where a provider only supports DNS management, for example.

View File

@ -0,0 +1,365 @@
# Hostinger Setup Guide
This guide covers configuring the Hostinger hosting provider integration in Setec Manager.
---
## Getting Your API Token
Hostinger provides API access through bearer tokens generated in the hPanel control panel.
### Step-by-Step
1. **Log in to hPanel.** Go to [https://hpanel.hostinger.com](https://hpanel.hostinger.com) and sign in with your Hostinger account.
2. **Navigate to your profile.** Click your profile icon or name in the top-right corner of the dashboard.
3. **Open Account Settings.** Select "Account Settings" or "Profile" from the dropdown menu.
4. **Go to the API section.** Look for the "API" or "API Tokens" tab. This may be under "Account" > "API" depending on your hPanel version.
5. **Generate a new token.** Click "Create API Token" or "Generate Token."
- Give the token a descriptive name (e.g., `setec-manager`).
- Select the permissions/scopes you need. For full Setec Manager integration, grant:
- DNS management (read/write)
- Domain management (read/write)
- VPS management (read/write)
- Billing (read)
- Set an expiration if desired (recommended: no expiration for server-to-server use, but rotate periodically).
6. **Copy the token.** The token is shown only once. Copy it immediately and store it securely. It will look like a long alphanumeric string.
**Important:** Treat this token like a password. Anyone with the token has API access to your Hostinger account.
---
## Configuring in Setec Manager
### Via the Web UI
1. Log in to your Setec Manager dashboard at `https://your-server:9090`.
2. Navigate to the Hosting Providers section.
3. Click "Hostinger" from the provider list.
4. Paste your API token into the "API Key" field.
5. Click "Test Connection" -- you should see a success message confirming the token is valid.
6. Click "Save Configuration" to persist the credentials.
### Via the API
```bash
# Set your Setec Manager JWT token
export TOKEN="your-setec-manager-jwt"
# Configure the Hostinger provider
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_HOSTINGER_BEARER_TOKEN"
}'
# Verify the connection
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/test \
-H "Authorization: Bearer $TOKEN"
```
### Via Config File
Create the config file directly on the server:
```bash
sudo mkdir -p /opt/setec-manager/data/hosting
sudo tee /opt/setec-manager/data/hosting/hostinger.json > /dev/null << 'EOF'
{
"provider": "hostinger",
"api_key": "YOUR_HOSTINGER_BEARER_TOKEN",
"api_secret": "",
"extra": {},
"connected": true
}
EOF
sudo chmod 600 /opt/setec-manager/data/hosting/hostinger.json
```
Restart Setec Manager for the config to be loaded:
```bash
sudo systemctl restart setec-manager
```
---
## Available Features
The Hostinger provider supports all major integration capabilities:
| Feature | Status | Notes |
|---|---|---|
| DNS Record Listing | Supported | Lists all records in a zone |
| DNS Record Creation | Supported | Adds records without overwriting |
| DNS Record Update (Batch) | Supported | Validates before applying; supports overwrite mode |
| DNS Record Deletion | Supported | Filter by name and/or type |
| DNS Zone Reset | Supported | Resets to Hostinger default records |
| Domain Listing | Supported | All domains on the account |
| Domain Details | Supported | Full WHOIS and registration info |
| Domain Availability Check | Supported | Batch check with pricing |
| Domain Purchase | Supported | Requires valid payment method |
| Nameserver Management | Supported | Update authoritative nameservers |
| Domain Lock | Supported | Enable/disable transfer lock |
| Privacy Protection | Supported | Enable/disable WHOIS privacy |
| VPS Listing | Supported | All VPS instances |
| VPS Details | Supported | Full specs, IP, status |
| VPS Creation | Supported | Requires plan, template, data center |
| Data Center Listing | Supported | Available regions for VM creation |
| SSH Key Management | Supported | Add, list, delete public keys |
| Subscription Listing | Supported | Active billing subscriptions |
| Product Catalog | Supported | Available plans and pricing |
---
## Rate Limits
The Hostinger API enforces rate limiting on all endpoints. The Setec Manager integration handles rate limits automatically:
- **Detection:** HTTP 429 (Too Many Requests) responses are detected.
- **Retry-After header:** The client reads the `Retry-After` header to determine how long to wait.
- **Automatic retry:** Up to 3 retries are attempted with the specified back-off.
- **Back-off cap:** Individual retry delays are capped at 60 seconds.
- **Failure:** If all retries are exhausted, the error is returned to the caller.
### Best Practices
- Avoid rapid-fire bulk operations. Space out batch DNS updates.
- Use the batch `UpdateDNSRecords` endpoint with multiple records in one call instead of creating records one at a time.
- Cache domain and VM listings on the client side when possible.
- If you see frequent 429 errors in logs, reduce the frequency of polling operations.
---
## DNS Record Management
### Hostinger API Endpoints Used
| Operation | Hostinger API Path |
|---|---|
| List records | `GET /api/dns/v1/zones/{domain}` |
| Update records | `PUT /api/dns/v1/zones/{domain}` |
| Validate records | `POST /api/dns/v1/zones/{domain}/validate` |
| Delete records | `DELETE /api/dns/v1/zones/{domain}` |
| Reset zone | `POST /api/dns/v1/zones/{domain}/reset` |
### Supported Record Types
| Type | Example Content | Priority | Notes |
|---|---|---|---|
| A | `93.184.216.34` | No | IPv4 address |
| AAAA | `2606:2800:220:1::` | No | IPv6 address |
| CNAME | `example.com` | No | Must be a hostname, not an IP |
| MX | `mail.example.com` | Yes | Priority determines delivery order (lower = higher priority) |
| TXT | `v=spf1 include:...` | No | Used for SPF, DKIM, domain verification |
| NS | `ns1.example.com` | No | Nameserver delegation |
| SRV | `sip.example.com` | Yes | Service location records |
| CAA | `letsencrypt.org` | No | Certificate authority authorization |
### Record ID Synthesis
Hostinger does not return unique record IDs in zone listings. Setec Manager synthesizes an ID from `name/type/priority` for each record. For example, an MX record for the root domain with priority 10 gets the ID `@/MX/10`. This ID is used internally for tracking but should not be passed back to the Hostinger API.
### Validation Before Write
The Hostinger provider validates DNS records before applying changes. When you call `UpdateDNSRecords`, the system:
1. Converts generic `DNSRecord` structs to Hostinger-specific format.
2. Sends the records to the `/validate` endpoint.
3. If validation passes, sends the actual update to the zone endpoint.
4. If validation fails, returns the validation error without modifying the zone.
This prevents malformed records from corrupting your DNS zone.
---
## Domain Management
### Purchasing Domains
Before purchasing a domain:
1. Check availability using the availability check endpoint.
2. Note the price and currency in the response.
3. Ensure you have a valid payment method configured in your Hostinger account.
4. Submit the purchase request with the `payment_method_id` from your Hostinger account.
```bash
# Check availability
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/check \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domains": ["my-new-site.com"]}'
# Purchase (if available)
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/purchase \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domain": "my-new-site.com",
"period": 1,
"auto_renew": true,
"privacy_protection": true
}'
```
### Domain Transfers
Domain transfers are initiated outside of Setec Manager through the Hostinger hPanel. Once a domain is transferred to your Hostinger account, it will appear in `ListDomains` and can be managed through Setec Manager.
### WHOIS Privacy
Hostinger offers WHOIS privacy protection (also called "Domain Privacy Protection") that replaces your personal contact information in WHOIS records with proxy information. Enable it to keep your registrant details private:
```bash
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
-H "Authorization: Bearer $TOKEN"
```
---
## VPS Management
### Creating a VM
To create a VPS instance, you need three pieces of information:
1. **Plan ID** -- Get from the catalog endpoint (`GET /api/hosting/providers/hostinger/catalog`).
2. **Data Center ID** -- Get from the data centers endpoint (`GET /api/hosting/providers/hostinger/datacenters`).
3. **Template** -- The OS template name (e.g., `"ubuntu-22.04"`, `"debian-12"`, `"centos-9"`).
```bash
# List available plans
curl -s https://your-server:9090/api/hosting/providers/hostinger/catalog \
-H "Authorization: Bearer $TOKEN" | jq '.[] | select(.category == "vps")'
# List data centers
curl -s https://your-server:9090/api/hosting/providers/hostinger/datacenters \
-H "Authorization: Bearer $TOKEN"
# Create the VM
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/vms \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"plan": "kvm-2",
"data_center_id": "us-east-1",
"template": "ubuntu-22.04",
"password": "YourSecurePassword!",
"hostname": "app-server",
"ssh_key_id": "key-abc123"
}'
```
### Docker Support
Hostinger VPS instances support Docker out of the box on Linux templates. After creating a VM:
1. SSH into the new VM.
2. Install Docker using the standard installation method for your chosen OS.
3. Alternatively, select a Docker-optimized template if available in your Hostinger account.
### VM Status Values
| Status | Description |
|---|---|
| `running` | VM is powered on and operational |
| `stopped` | VM is powered off |
| `creating` | VM is being provisioned (may take a few minutes) |
| `error` | VM encountered an error during provisioning |
| `suspended` | VM is suspended (usually billing-related) |
---
## Troubleshooting
### Common Errors
#### "hostinger API error 401: Unauthorized"
**Cause:** The API token is invalid, expired, or revoked.
**Fix:**
1. Log in to hPanel and verify the token exists and is not expired.
2. Generate a new token if needed.
3. Update the configuration in Setec Manager.
#### "hostinger API error 403: Forbidden"
**Cause:** The API token does not have the required permissions/scopes.
**Fix:**
1. Check the token's permissions in hPanel.
2. Ensure the token has read/write access for the feature you are trying to use (DNS, domains, VPS, billing).
3. Generate a new token with the correct scopes if needed.
#### "hostinger API error 429: rate limited"
**Cause:** Too many API requests in a short period.
**Fix:**
- The client retries automatically up to 3 times. If you still see this error, you are making requests too frequently.
- Space out bulk operations.
- Use batch endpoints (e.g., `UpdateDNSRecords` with multiple records) instead of individual calls.
#### "hostinger API error 404: Not Found"
**Cause:** The domain, VM, or resource does not exist in your Hostinger account.
**Fix:**
- Verify the domain is registered with Hostinger (not just DNS-hosted).
- Check that the VM ID is correct.
- Ensure the domain's DNS zone is active in Hostinger.
#### "validate DNS records: hostinger API error 422"
**Cause:** One or more DNS records failed validation.
**Fix:**
- Check record types are valid (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA).
- Verify content format matches the record type (e.g., A records must be valid IPv4 addresses).
- Ensure TTL is a positive integer.
- MX and SRV records require a priority value.
- CNAME records cannot coexist with other record types at the same name.
#### "connection failed" or "execute request" errors
**Cause:** Network connectivity issue between Setec Manager and `developers.hostinger.com`.
**Fix:**
- Verify the server has outbound HTTPS access.
- Check DNS resolution: `dig developers.hostinger.com`.
- Check if a firewall is blocking outbound port 443.
- Verify the server's system clock is accurate (TLS certificate validation requires correct time).
#### "hosting provider 'hostinger' not registered"
**Cause:** The Hostinger provider package was not imported in the binary.
**Fix:**
- Ensure `cmd/main.go` includes the blank import: `_ "setec-manager/internal/hosting/hostinger"`.
- Rebuild and restart Setec Manager.
### Checking Logs
Setec Manager logs hosting provider operations to the configured log file (default: `/var/log/setec-manager.log`). Look for lines containing `hostinger` or `hosting`:
```bash
grep -i hostinger /var/log/setec-manager.log | tail -20
```
### Testing Connectivity Manually
You can test the Hostinger API directly from the server to rule out Setec Manager issues:
```bash
curl -s -H "Authorization: Bearer YOUR_HOSTINGER_TOKEN" \
https://developers.hostinger.com/api/dns/v1/zones/your-domain.com
```
If this succeeds but Setec Manager fails, the issue is in the Setec Manager configuration. If this also fails, the issue is with the token or network connectivity.

View File

@ -0,0 +1,25 @@
module setec-manager
go 1.25.0
require (
github.com/go-chi/chi/v5 v5.2.5
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
golang.org/x/crypto v0.48.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/sys v0.41.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

View File

@ -0,0 +1,65 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@ -0,0 +1,361 @@
package acme
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
// Client wraps the certbot CLI for Let's Encrypt ACME certificate management.
type Client struct {
Email string
Staging bool
Webroot string
AccountDir string
}
// CertInfo holds parsed certificate metadata.
type CertInfo struct {
Domain string `json:"domain"`
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
ChainPath string `json:"chain_path"`
ExpiresAt time.Time `json:"expires_at"`
Issuer string `json:"issuer"`
DaysLeft int `json:"days_left"`
}
// domainRegex validates domain names (basic RFC 1123 hostname check).
var domainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
// NewClient creates a new ACME client.
func NewClient(email string, staging bool, webroot, accountDir string) *Client {
return &Client{
Email: email,
Staging: staging,
Webroot: webroot,
AccountDir: accountDir,
}
}
// validateDomain checks that a domain name is syntactically valid before passing
// it to certbot. This prevents command injection and catches obvious typos.
func validateDomain(domain string) error {
if domain == "" {
return fmt.Errorf("domain name is empty")
}
if len(domain) > 253 {
return fmt.Errorf("domain name too long: %d characters (max 253)", len(domain))
}
if !domainRegex.MatchString(domain) {
return fmt.Errorf("invalid domain name: %q", domain)
}
return nil
}
// Issue requests a new certificate from Let's Encrypt for the given domain
// using the webroot challenge method.
func (c *Client) Issue(domain string) (*CertInfo, error) {
if err := validateDomain(domain); err != nil {
return nil, fmt.Errorf("issue: %w", err)
}
if err := c.EnsureCertbotInstalled(); err != nil {
return nil, fmt.Errorf("issue: %w", err)
}
// Ensure webroot directory exists
if err := os.MkdirAll(c.Webroot, 0755); err != nil {
return nil, fmt.Errorf("issue: create webroot: %w", err)
}
args := []string{
"certonly", "--webroot",
"-w", c.Webroot,
"-d", domain,
"--non-interactive",
"--agree-tos",
"-m", c.Email,
}
if c.Staging {
args = append(args, "--staging")
}
cmd := exec.Command("certbot", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("certbot certonly failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return c.GetCertInfo(domain)
}
// Renew renews the certificate for a specific domain.
func (c *Client) Renew(domain string) error {
if err := validateDomain(domain); err != nil {
return fmt.Errorf("renew: %w", err)
}
if err := c.EnsureCertbotInstalled(); err != nil {
return fmt.Errorf("renew: %w", err)
}
cmd := exec.Command("certbot", "renew",
"--cert-name", domain,
"--non-interactive",
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("certbot renew failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// RenewAll renews all certificates managed by certbot that are due for renewal.
func (c *Client) RenewAll() (string, error) {
if err := c.EnsureCertbotInstalled(); err != nil {
return "", fmt.Errorf("renew all: %w", err)
}
cmd := exec.Command("certbot", "renew", "--non-interactive")
out, err := cmd.CombinedOutput()
output := string(out)
if err != nil {
return output, fmt.Errorf("certbot renew --all failed: %s: %w", strings.TrimSpace(output), err)
}
return output, nil
}
// Revoke revokes the certificate for a given domain.
func (c *Client) Revoke(domain string) error {
if err := validateDomain(domain); err != nil {
return fmt.Errorf("revoke: %w", err)
}
if err := c.EnsureCertbotInstalled(); err != nil {
return fmt.Errorf("revoke: %w", err)
}
cmd := exec.Command("certbot", "revoke",
"--cert-name", domain,
"--non-interactive",
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("certbot revoke failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// Delete removes a certificate and its renewal configuration from certbot.
func (c *Client) Delete(domain string) error {
if err := validateDomain(domain); err != nil {
return fmt.Errorf("delete: %w", err)
}
if err := c.EnsureCertbotInstalled(); err != nil {
return fmt.Errorf("delete: %w", err)
}
cmd := exec.Command("certbot", "delete",
"--cert-name", domain,
"--non-interactive",
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("certbot delete failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// ListCerts scans /etc/letsencrypt/live/ and parses each certificate to return
// metadata including expiry dates and issuer information.
func (c *Client) ListCerts() ([]CertInfo, error) {
liveDir := "/etc/letsencrypt/live"
entries, err := os.ReadDir(liveDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // No certs directory yet
}
return nil, fmt.Errorf("list certs: read live dir: %w", err)
}
var certs []CertInfo
for _, entry := range entries {
if !entry.IsDir() {
continue
}
domain := entry.Name()
// Skip the README directory certbot sometimes creates
if domain == "README" {
continue
}
info, err := c.GetCertInfo(domain)
if err != nil {
// Log but skip certs we can't parse
continue
}
certs = append(certs, *info)
}
return certs, nil
}
// GetCertInfo reads and parses the X.509 certificate at the standard Let's
// Encrypt live path for a domain, returning structured metadata.
func (c *Client) GetCertInfo(domain string) (*CertInfo, error) {
if err := validateDomain(domain); err != nil {
return nil, fmt.Errorf("get cert info: %w", err)
}
liveDir := filepath.Join("/etc/letsencrypt/live", domain)
certPath := filepath.Join(liveDir, "fullchain.pem")
keyPath := filepath.Join(liveDir, "privkey.pem")
chainPath := filepath.Join(liveDir, "chain.pem")
data, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("get cert info: read cert: %w", err)
}
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("get cert info: no PEM block found in %s", certPath)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("get cert info: parse x509: %w", err)
}
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
return &CertInfo{
Domain: domain,
CertPath: certPath,
KeyPath: keyPath,
ChainPath: chainPath,
ExpiresAt: cert.NotAfter,
Issuer: cert.Issuer.CommonName,
DaysLeft: daysLeft,
}, nil
}
// EnsureCertbotInstalled checks whether certbot is available in PATH. If not,
// it attempts to install it via apt-get.
func (c *Client) EnsureCertbotInstalled() error {
if _, err := exec.LookPath("certbot"); err == nil {
return nil // Already installed
}
// Attempt to install via apt-get
cmd := exec.Command("apt-get", "update", "-qq")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apt-get update failed: %s: %w", strings.TrimSpace(string(out)), err)
}
cmd = exec.Command("apt-get", "install", "-y", "-qq", "certbot")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apt-get install certbot failed: %s: %w", strings.TrimSpace(string(out)), err)
}
// Verify installation succeeded
if _, err := exec.LookPath("certbot"); err != nil {
return fmt.Errorf("certbot still not found after installation attempt")
}
return nil
}
// GenerateSelfSigned creates a self-signed X.509 certificate and private key
// for testing or as a fallback when Let's Encrypt is unavailable.
func (c *Client) GenerateSelfSigned(domain, certPath, keyPath string) error {
if err := validateDomain(domain); err != nil {
return fmt.Errorf("generate self-signed: %w", err)
}
// Ensure output directories exist
if err := os.MkdirAll(filepath.Dir(certPath), 0755); err != nil {
return fmt.Errorf("generate self-signed: create cert dir: %w", err)
}
if err := os.MkdirAll(filepath.Dir(keyPath), 0755); err != nil {
return fmt.Errorf("generate self-signed: create key dir: %w", err)
}
// Generate ECDSA P-256 private key
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("generate self-signed: generate key: %w", err)
}
// Build the certificate template
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return fmt.Errorf("generate self-signed: serial number: %w", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour) // 1 year
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: domain,
Organization: []string{"Setec Security Labs"},
},
DNSNames: []string{domain},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
// Self-sign the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
if err != nil {
return fmt.Errorf("generate self-signed: create cert: %w", err)
}
// Write certificate PEM
certFile, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("generate self-signed: write cert: %w", err)
}
defer certFile.Close()
if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
return fmt.Errorf("generate self-signed: encode cert PEM: %w", err)
}
// Write private key PEM
keyDER, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
return fmt.Errorf("generate self-signed: marshal key: %w", err)
}
keyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("generate self-signed: write key: %w", err)
}
defer keyFile.Close()
if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil {
return fmt.Errorf("generate self-signed: encode key PEM: %w", err)
}
return nil
}

View File

@ -0,0 +1,146 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Nginx NginxConfig `yaml:"nginx"`
ACME ACMEConfig `yaml:"acme"`
Autarch AutarchConfig `yaml:"autarch"`
Float FloatConfig `yaml:"float"`
Backups BackupsConfig `yaml:"backups"`
Logging LoggingConfig `yaml:"logging"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
TLS bool `yaml:"tls"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
}
type DatabaseConfig struct {
Path string `yaml:"path"`
}
type NginxConfig struct {
SitesAvailable string `yaml:"sites_available"`
SitesEnabled string `yaml:"sites_enabled"`
Snippets string `yaml:"snippets"`
Webroot string `yaml:"webroot"`
CertbotWebroot string `yaml:"certbot_webroot"`
}
type ACMEConfig struct {
Email string `yaml:"email"`
Staging bool `yaml:"staging"`
AccountDir string `yaml:"account_dir"`
}
type AutarchConfig struct {
InstallDir string `yaml:"install_dir"`
GitRepo string `yaml:"git_repo"`
GitBranch string `yaml:"git_branch"`
WebPort int `yaml:"web_port"`
DNSPort int `yaml:"dns_port"`
}
type FloatConfig struct {
Enabled bool `yaml:"enabled"`
MaxSessions int `yaml:"max_sessions"`
SessionTTL string `yaml:"session_ttl"`
}
type BackupsConfig struct {
Dir string `yaml:"dir"`
MaxAgeDays int `yaml:"max_age_days"`
MaxCount int `yaml:"max_count"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
File string `yaml:"file"`
MaxSizeMB int `yaml:"max_size_mb"`
MaxBackups int `yaml:"max_backups"`
}
func DefaultConfig() *Config {
return &Config{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 9090,
TLS: true,
Cert: "/opt/setec-manager/data/acme/manager.crt",
Key: "/opt/setec-manager/data/acme/manager.key",
},
Database: DatabaseConfig{
Path: "/opt/setec-manager/data/setec.db",
},
Nginx: NginxConfig{
SitesAvailable: "/etc/nginx/sites-available",
SitesEnabled: "/etc/nginx/sites-enabled",
Snippets: "/etc/nginx/snippets",
Webroot: "/var/www",
CertbotWebroot: "/var/www/certbot",
},
ACME: ACMEConfig{
Email: "",
Staging: false,
AccountDir: "/opt/setec-manager/data/acme",
},
Autarch: AutarchConfig{
InstallDir: "/var/www/autarch",
GitRepo: "https://github.com/DigijEth/autarch.git",
GitBranch: "main",
WebPort: 8181,
DNSPort: 53,
},
Float: FloatConfig{
Enabled: false,
MaxSessions: 10,
SessionTTL: "24h",
},
Backups: BackupsConfig{
Dir: "/opt/setec-manager/data/backups",
MaxAgeDays: 30,
MaxCount: 50,
},
Logging: LoggingConfig{
Level: "info",
File: "/var/log/setec-manager.log",
MaxSizeMB: 100,
MaxBackups: 3,
},
}
}
func Load(path string) (*Config, error) {
cfg := DefaultConfig()
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return nil, err
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
return cfg, nil
}
func (c *Config) Save(path string) error {
data, err := yaml.Marshal(c)
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}

View File

@ -0,0 +1,46 @@
package db
import "time"
type Backup struct {
ID int64 `json:"id"`
SiteID *int64 `json:"site_id"`
BackupType string `json:"backup_type"`
FilePath string `json:"file_path"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt time.Time `json:"created_at"`
}
func (d *DB) CreateBackup(siteID *int64, backupType, filePath string, sizeBytes int64) (int64, error) {
result, err := d.conn.Exec(`INSERT INTO backups (site_id, backup_type, file_path, size_bytes)
VALUES (?, ?, ?, ?)`, siteID, backupType, filePath, sizeBytes)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (d *DB) ListBackups() ([]Backup, error) {
rows, err := d.conn.Query(`SELECT id, site_id, backup_type, file_path, size_bytes, created_at
FROM backups ORDER BY id DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var backups []Backup
for rows.Next() {
var b Backup
if err := rows.Scan(&b.ID, &b.SiteID, &b.BackupType, &b.FilePath,
&b.SizeBytes, &b.CreatedAt); err != nil {
return nil, err
}
backups = append(backups, b)
}
return backups, rows.Err()
}
func (d *DB) DeleteBackup(id int64) error {
_, err := d.conn.Exec(`DELETE FROM backups WHERE id=?`, id)
return err
}

View File

@ -0,0 +1,163 @@
package db
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
type DB struct {
conn *sql.DB
}
func Open(path string) (*DB, error) {
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, fmt.Errorf("create db dir: %w", err)
}
conn, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
conn.SetMaxOpenConns(1) // SQLite single-writer
db := &DB{conn: conn}
if err := db.migrate(); err != nil {
conn.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
func (d *DB) Close() error {
return d.conn.Close()
}
func (d *DB) Conn() *sql.DB {
return d.conn
}
func (d *DB) migrate() error {
migrations := []string{
migrateSites,
migrateSystemUsers,
migrateManagerUsers,
migrateDeployments,
migrateCronJobs,
migrateFirewallRules,
migrateFloatSessions,
migrateBackups,
migrateAuditLog,
}
for _, m := range migrations {
if _, err := d.conn.Exec(m); err != nil {
return err
}
}
return nil
}
const migrateSites = `CREATE TABLE IF NOT EXISTS sites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL UNIQUE,
aliases TEXT DEFAULT '',
app_type TEXT NOT NULL DEFAULT 'static',
app_root TEXT NOT NULL,
app_port INTEGER DEFAULT 0,
app_entry TEXT DEFAULT '',
git_repo TEXT DEFAULT '',
git_branch TEXT DEFAULT 'main',
ssl_enabled BOOLEAN DEFAULT FALSE,
ssl_cert_path TEXT DEFAULT '',
ssl_key_path TEXT DEFAULT '',
ssl_auto BOOLEAN DEFAULT TRUE,
enabled BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const migrateSystemUsers = `CREATE TABLE IF NOT EXISTS system_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
uid INTEGER,
home_dir TEXT,
shell TEXT DEFAULT '/bin/bash',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const migrateManagerUsers = `CREATE TABLE IF NOT EXISTS manager_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'admin',
force_change BOOLEAN DEFAULT FALSE,
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const migrateDeployments = `CREATE TABLE IF NOT EXISTS deployments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id),
action TEXT NOT NULL,
status TEXT DEFAULT 'pending',
output TEXT DEFAULT '',
started_at DATETIME,
finished_at DATETIME
);`
const migrateCronJobs = `CREATE TABLE IF NOT EXISTS cron_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id),
job_type TEXT NOT NULL,
schedule TEXT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
last_run DATETIME,
next_run DATETIME
);`
const migrateFirewallRules = `CREATE TABLE IF NOT EXISTS firewall_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
direction TEXT DEFAULT 'in',
protocol TEXT DEFAULT 'tcp',
port TEXT NOT NULL,
source TEXT DEFAULT 'any',
action TEXT DEFAULT 'allow',
comment TEXT DEFAULT '',
enabled BOOLEAN DEFAULT TRUE
);`
const migrateFloatSessions = `CREATE TABLE IF NOT EXISTS float_sessions (
id TEXT PRIMARY KEY,
user_id INTEGER REFERENCES manager_users(id),
client_ip TEXT,
client_agent TEXT,
usb_bridge BOOLEAN DEFAULT FALSE,
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_ping DATETIME,
expires_at DATETIME
);`
const migrateAuditLog = `CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
ip TEXT,
action TEXT NOT NULL,
detail TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
const migrateBackups = `CREATE TABLE IF NOT EXISTS backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER REFERENCES sites(id),
backup_type TEXT DEFAULT 'site',
file_path TEXT NOT NULL,
size_bytes INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`

View File

@ -0,0 +1,60 @@
package db
import "time"
type Deployment struct {
ID int64 `json:"id"`
SiteID *int64 `json:"site_id"`
Action string `json:"action"`
Status string `json:"status"`
Output string `json:"output"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
}
func (d *DB) CreateDeployment(siteID *int64, action string) (int64, error) {
result, err := d.conn.Exec(`INSERT INTO deployments (site_id, action, status, started_at)
VALUES (?, ?, 'running', CURRENT_TIMESTAMP)`, siteID, action)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (d *DB) FinishDeployment(id int64, status, output string) error {
_, err := d.conn.Exec(`UPDATE deployments SET status=?, output=?, finished_at=CURRENT_TIMESTAMP
WHERE id=?`, status, output, id)
return err
}
func (d *DB) ListDeployments(siteID *int64, limit int) ([]Deployment, error) {
var rows_query string
var args []interface{}
if siteID != nil {
rows_query = `SELECT id, site_id, action, status, output, started_at, finished_at
FROM deployments WHERE site_id=? ORDER BY id DESC LIMIT ?`
args = []interface{}{*siteID, limit}
} else {
rows_query = `SELECT id, site_id, action, status, output, started_at, finished_at
FROM deployments ORDER BY id DESC LIMIT ?`
args = []interface{}{limit}
}
rows, err := d.conn.Query(rows_query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var deps []Deployment
for rows.Next() {
var dep Deployment
if err := rows.Scan(&dep.ID, &dep.SiteID, &dep.Action, &dep.Status,
&dep.Output, &dep.StartedAt, &dep.FinishedAt); err != nil {
return nil, err
}
deps = append(deps, dep)
}
return deps, rows.Err()
}

View File

@ -0,0 +1,70 @@
package db
import "time"
type FloatSession struct {
ID string `json:"id"`
UserID int64 `json:"user_id"`
ClientIP string `json:"client_ip"`
ClientAgent string `json:"client_agent"`
USBBridge bool `json:"usb_bridge"`
ConnectedAt time.Time `json:"connected_at"`
LastPing *time.Time `json:"last_ping"`
ExpiresAt time.Time `json:"expires_at"`
}
func (d *DB) CreateFloatSession(id string, userID int64, clientIP, agent string, expiresAt time.Time) error {
_, err := d.conn.Exec(`INSERT INTO float_sessions (id, user_id, client_ip, client_agent, expires_at)
VALUES (?, ?, ?, ?, ?)`, id, userID, clientIP, agent, expiresAt)
return err
}
func (d *DB) GetFloatSession(id string) (*FloatSession, error) {
var s FloatSession
err := d.conn.QueryRow(`SELECT id, user_id, client_ip, client_agent, usb_bridge,
connected_at, last_ping, expires_at FROM float_sessions WHERE id=?`, id).
Scan(&s.ID, &s.UserID, &s.ClientIP, &s.ClientAgent, &s.USBBridge,
&s.ConnectedAt, &s.LastPing, &s.ExpiresAt)
if err != nil {
return nil, err
}
return &s, nil
}
func (d *DB) ListFloatSessions() ([]FloatSession, error) {
rows, err := d.conn.Query(`SELECT id, user_id, client_ip, client_agent, usb_bridge,
connected_at, last_ping, expires_at FROM float_sessions ORDER BY connected_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []FloatSession
for rows.Next() {
var s FloatSession
if err := rows.Scan(&s.ID, &s.UserID, &s.ClientIP, &s.ClientAgent, &s.USBBridge,
&s.ConnectedAt, &s.LastPing, &s.ExpiresAt); err != nil {
return nil, err
}
sessions = append(sessions, s)
}
return sessions, rows.Err()
}
func (d *DB) DeleteFloatSession(id string) error {
_, err := d.conn.Exec(`DELETE FROM float_sessions WHERE id=?`, id)
return err
}
func (d *DB) PingFloatSession(id string) error {
_, err := d.conn.Exec(`UPDATE float_sessions SET last_ping=CURRENT_TIMESTAMP WHERE id=?`, id)
return err
}
func (d *DB) CleanExpiredFloatSessions() (int64, error) {
result, err := d.conn.Exec(`DELETE FROM float_sessions WHERE expires_at < CURRENT_TIMESTAMP`)
if err != nil {
return 0, err
}
return result.RowsAffected()
}

View File

@ -0,0 +1,107 @@
package db
import (
"database/sql"
"time"
)
type Site struct {
ID int64 `json:"id"`
Domain string `json:"domain"`
Aliases string `json:"aliases"`
AppType string `json:"app_type"`
AppRoot string `json:"app_root"`
AppPort int `json:"app_port"`
AppEntry string `json:"app_entry"`
GitRepo string `json:"git_repo"`
GitBranch string `json:"git_branch"`
SSLEnabled bool `json:"ssl_enabled"`
SSLCertPath string `json:"ssl_cert_path"`
SSLKeyPath string `json:"ssl_key_path"`
SSLAuto bool `json:"ssl_auto"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (d *DB) ListSites() ([]Site, error) {
rows, err := d.conn.Query(`SELECT id, domain, aliases, app_type, app_root, app_port,
app_entry, git_repo, git_branch, ssl_enabled, ssl_cert_path, ssl_key_path,
ssl_auto, enabled, created_at, updated_at FROM sites ORDER BY domain`)
if err != nil {
return nil, err
}
defer rows.Close()
var sites []Site
for rows.Next() {
var s Site
if err := rows.Scan(&s.ID, &s.Domain, &s.Aliases, &s.AppType, &s.AppRoot,
&s.AppPort, &s.AppEntry, &s.GitRepo, &s.GitBranch, &s.SSLEnabled,
&s.SSLCertPath, &s.SSLKeyPath, &s.SSLAuto, &s.Enabled,
&s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, err
}
sites = append(sites, s)
}
return sites, rows.Err()
}
func (d *DB) GetSite(id int64) (*Site, error) {
var s Site
err := d.conn.QueryRow(`SELECT id, domain, aliases, app_type, app_root, app_port,
app_entry, git_repo, git_branch, ssl_enabled, ssl_cert_path, ssl_key_path,
ssl_auto, enabled, created_at, updated_at FROM sites WHERE id = ?`, id).
Scan(&s.ID, &s.Domain, &s.Aliases, &s.AppType, &s.AppRoot,
&s.AppPort, &s.AppEntry, &s.GitRepo, &s.GitBranch, &s.SSLEnabled,
&s.SSLCertPath, &s.SSLKeyPath, &s.SSLAuto, &s.Enabled,
&s.CreatedAt, &s.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &s, err
}
func (d *DB) GetSiteByDomain(domain string) (*Site, error) {
var s Site
err := d.conn.QueryRow(`SELECT id, domain, aliases, app_type, app_root, app_port,
app_entry, git_repo, git_branch, ssl_enabled, ssl_cert_path, ssl_key_path,
ssl_auto, enabled, created_at, updated_at FROM sites WHERE domain = ?`, domain).
Scan(&s.ID, &s.Domain, &s.Aliases, &s.AppType, &s.AppRoot,
&s.AppPort, &s.AppEntry, &s.GitRepo, &s.GitBranch, &s.SSLEnabled,
&s.SSLCertPath, &s.SSLKeyPath, &s.SSLAuto, &s.Enabled,
&s.CreatedAt, &s.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &s, err
}
func (d *DB) CreateSite(s *Site) (int64, error) {
result, err := d.conn.Exec(`INSERT INTO sites (domain, aliases, app_type, app_root, app_port,
app_entry, git_repo, git_branch, ssl_enabled, ssl_cert_path, ssl_key_path, ssl_auto, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
s.Domain, s.Aliases, s.AppType, s.AppRoot, s.AppPort,
s.AppEntry, s.GitRepo, s.GitBranch, s.SSLEnabled,
s.SSLCertPath, s.SSLKeyPath, s.SSLAuto, s.Enabled)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (d *DB) UpdateSite(s *Site) error {
_, err := d.conn.Exec(`UPDATE sites SET domain=?, aliases=?, app_type=?, app_root=?,
app_port=?, app_entry=?, git_repo=?, git_branch=?, ssl_enabled=?,
ssl_cert_path=?, ssl_key_path=?, ssl_auto=?, enabled=?, updated_at=CURRENT_TIMESTAMP
WHERE id=?`,
s.Domain, s.Aliases, s.AppType, s.AppRoot, s.AppPort,
s.AppEntry, s.GitRepo, s.GitBranch, s.SSLEnabled,
s.SSLCertPath, s.SSLKeyPath, s.SSLAuto, s.Enabled, s.ID)
return err
}
func (d *DB) DeleteSite(id int64) error {
_, err := d.conn.Exec(`DELETE FROM sites WHERE id=?`, id)
return err
}

View File

@ -0,0 +1,124 @@
package db
import (
"database/sql"
"time"
"golang.org/x/crypto/bcrypt"
)
type ManagerUser struct {
ID int64 `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"-"`
Role string `json:"role"`
ForceChange bool `json:"force_change"`
LastLogin *time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
}
func (d *DB) ListManagerUsers() ([]ManagerUser, error) {
rows, err := d.conn.Query(`SELECT id, username, password_hash, role, force_change,
last_login, created_at FROM manager_users ORDER BY username`)
if err != nil {
return nil, err
}
defer rows.Close()
var users []ManagerUser
for rows.Next() {
var u ManagerUser
if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role,
&u.ForceChange, &u.LastLogin, &u.CreatedAt); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
func (d *DB) GetManagerUser(username string) (*ManagerUser, error) {
var u ManagerUser
err := d.conn.QueryRow(`SELECT id, username, password_hash, role, force_change,
last_login, created_at FROM manager_users WHERE username = ?`, username).
Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role,
&u.ForceChange, &u.LastLogin, &u.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &u, err
}
func (d *DB) GetManagerUserByID(id int64) (*ManagerUser, error) {
var u ManagerUser
err := d.conn.QueryRow(`SELECT id, username, password_hash, role, force_change,
last_login, created_at FROM manager_users WHERE id = ?`, id).
Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role,
&u.ForceChange, &u.LastLogin, &u.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &u, err
}
func (d *DB) CreateManagerUser(username, password, role string) (int64, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return 0, err
}
result, err := d.conn.Exec(`INSERT INTO manager_users (username, password_hash, role)
VALUES (?, ?, ?)`, username, string(hash), role)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (d *DB) UpdateManagerUserPassword(id int64, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = d.conn.Exec(`UPDATE manager_users SET password_hash=?, force_change=FALSE WHERE id=?`,
string(hash), id)
return err
}
func (d *DB) UpdateManagerUserRole(id int64, role string) error {
_, err := d.conn.Exec(`UPDATE manager_users SET role=? WHERE id=?`, role, id)
return err
}
func (d *DB) DeleteManagerUser(id int64) error {
_, err := d.conn.Exec(`DELETE FROM manager_users WHERE id=?`, id)
return err
}
func (d *DB) UpdateLoginTimestamp(id int64) error {
_, err := d.conn.Exec(`UPDATE manager_users SET last_login=CURRENT_TIMESTAMP WHERE id=?`, id)
return err
}
func (d *DB) ManagerUserCount() (int, error) {
var count int
err := d.conn.QueryRow(`SELECT COUNT(*) FROM manager_users`).Scan(&count)
return count, err
}
func (d *DB) AuthenticateUser(username, password string) (*ManagerUser, error) {
u, err := d.GetManagerUser(username)
if err != nil {
return nil, err
}
if u == nil {
return nil, nil
}
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
return nil, nil
}
d.UpdateLoginTimestamp(u.ID)
return u, nil
}

View File

@ -0,0 +1,144 @@
package deploy
import (
"fmt"
"os/exec"
"strings"
)
// CommitInfo holds metadata for a single git commit.
type CommitInfo struct {
Hash string
Author string
Date string
Message string
}
// Clone clones a git repository into dest, checking out the given branch.
func Clone(repo, branch, dest string) (string, error) {
git, err := exec.LookPath("git")
if err != nil {
return "", fmt.Errorf("git not found: %w", err)
}
args := []string{"clone", "--branch", branch, "--progress", repo, dest}
out, err := exec.Command(git, args...).CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("git clone: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// Pull performs a fast-forward-only pull in the given directory.
func Pull(dir string) (string, error) {
git, err := exec.LookPath("git")
if err != nil {
return "", fmt.Errorf("git not found: %w", err)
}
cmd := exec.Command(git, "pull", "--ff-only")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("git pull: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// CurrentCommit returns the hash and message of the latest commit in dir.
func CurrentCommit(dir string) (hash string, message string, err error) {
git, err := exec.LookPath("git")
if err != nil {
return "", "", fmt.Errorf("git not found: %w", err)
}
cmd := exec.Command(git, "log", "--oneline", "-1")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", "", fmt.Errorf("git log: %w", err)
}
line := strings.TrimSpace(string(out))
if line == "" {
return "", "", fmt.Errorf("git log: no commits found")
}
parts := strings.SplitN(line, " ", 2)
hash = parts[0]
if len(parts) > 1 {
message = parts[1]
}
return hash, message, nil
}
// GetBranch returns the current branch name for the repository in dir.
func GetBranch(dir string) (string, error) {
git, err := exec.LookPath("git")
if err != nil {
return "", fmt.Errorf("git not found: %w", err)
}
cmd := exec.Command(git, "rev-parse", "--abbrev-ref", "HEAD")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse: %w", err)
}
return strings.TrimSpace(string(out)), nil
}
// HasChanges returns true if the working tree in dir has uncommitted changes.
func HasChanges(dir string) (bool, error) {
git, err := exec.LookPath("git")
if err != nil {
return false, fmt.Errorf("git not found: %w", err)
}
cmd := exec.Command(git, "status", "--porcelain")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("git status: %w", err)
}
return strings.TrimSpace(string(out)) != "", nil
}
// Log returns the last n commits from the repository in dir.
func Log(dir string, n int) ([]CommitInfo, error) {
git, err := exec.LookPath("git")
if err != nil {
return nil, fmt.Errorf("git not found: %w", err)
}
// Use a delimiter unlikely to appear in commit messages.
const sep = "||SETEC||"
format := fmt.Sprintf("%%h%s%%an%s%%ai%s%%s", sep, sep, sep)
cmd := exec.Command(git, "log", fmt.Sprintf("-n%d", n), fmt.Sprintf("--format=%s", format))
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git log: %w", err)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
var commits []CommitInfo
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.SplitN(line, sep, 4)
if len(parts) < 4 {
continue
}
commits = append(commits, CommitInfo{
Hash: parts[0],
Author: parts[1],
Date: parts[2],
Message: parts[3],
})
}
return commits, nil
}

View File

@ -0,0 +1,100 @@
package deploy
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// NpmInstall runs npm install in the given directory.
func NpmInstall(dir string) (string, error) {
npm, err := exec.LookPath("npm")
if err != nil {
return "", fmt.Errorf("npm not found: %w", err)
}
cmd := exec.Command(npm, "install")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("npm install: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// NpmBuild runs npm run build in the given directory.
func NpmBuild(dir string) (string, error) {
npm, err := exec.LookPath("npm")
if err != nil {
return "", fmt.Errorf("npm not found: %w", err)
}
cmd := exec.Command(npm, "run", "build")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("npm run build: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// NpmAudit runs npm audit in the given directory and returns the report.
func NpmAudit(dir string) (string, error) {
npm, err := exec.LookPath("npm")
if err != nil {
return "", fmt.Errorf("npm not found: %w", err)
}
cmd := exec.Command(npm, "audit")
cmd.Dir = dir
// npm audit exits non-zero when vulnerabilities are found, which is not
// an execution error — we still want the output.
out, err := cmd.CombinedOutput()
if err != nil {
// Return the output even on non-zero exit; the caller can inspect it.
return string(out), fmt.Errorf("npm audit: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// HasPackageJSON returns true if a package.json file exists in dir.
func HasPackageJSON(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "package.json"))
return err == nil && !info.IsDir()
}
// HasNodeModules returns true if a node_modules directory exists in dir.
func HasNodeModules(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "node_modules"))
return err == nil && info.IsDir()
}
// NodeVersion returns the installed Node.js version string.
func NodeVersion() (string, error) {
node, err := exec.LookPath("node")
if err != nil {
return "", fmt.Errorf("node not found: %w", err)
}
out, err := exec.Command(node, "--version").Output()
if err != nil {
return "", fmt.Errorf("node --version: %w", err)
}
return strings.TrimSpace(string(out)), nil
}
// NpmVersion returns the installed npm version string.
func NpmVersion() (string, error) {
npm, err := exec.LookPath("npm")
if err != nil {
return "", fmt.Errorf("npm not found: %w", err)
}
out, err := exec.Command(npm, "--version").Output()
if err != nil {
return "", fmt.Errorf("npm --version: %w", err)
}
return strings.TrimSpace(string(out)), nil
}

View File

@ -0,0 +1,93 @@
package deploy
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// PipPackage holds the name and version of an installed pip package.
type PipPackage struct {
Name string `json:"name"`
Version string `json:"version"`
}
// CreateVenv creates a Python virtual environment at <dir>/venv.
func CreateVenv(dir string) error {
python, err := exec.LookPath("python3")
if err != nil {
return fmt.Errorf("python3 not found: %w", err)
}
venvPath := filepath.Join(dir, "venv")
out, err := exec.Command(python, "-m", "venv", venvPath).CombinedOutput()
if err != nil {
return fmt.Errorf("create venv: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// UpgradePip upgrades pip, setuptools, and wheel inside the virtual environment
// rooted at venvDir.
func UpgradePip(venvDir string) error {
pip := filepath.Join(venvDir, "bin", "pip")
if _, err := os.Stat(pip); err != nil {
return fmt.Errorf("pip not found at %s: %w", pip, err)
}
out, err := exec.Command(pip, "install", "--upgrade", "pip", "setuptools", "wheel").CombinedOutput()
if err != nil {
return fmt.Errorf("upgrade pip: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// InstallRequirements installs packages from a requirements file into the
// virtual environment rooted at venvDir.
func InstallRequirements(venvDir, reqFile string) (string, error) {
pip := filepath.Join(venvDir, "bin", "pip")
if _, err := os.Stat(pip); err != nil {
return "", fmt.Errorf("pip not found at %s: %w", pip, err)
}
if _, err := os.Stat(reqFile); err != nil {
return "", fmt.Errorf("requirements file not found: %w", err)
}
out, err := exec.Command(pip, "install", "-r", reqFile).CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("pip install: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// ListPackages returns all installed packages in the virtual environment
// rooted at venvDir.
func ListPackages(venvDir string) ([]PipPackage, error) {
pip := filepath.Join(venvDir, "bin", "pip")
if _, err := os.Stat(pip); err != nil {
return nil, fmt.Errorf("pip not found at %s: %w", pip, err)
}
out, err := exec.Command(pip, "list", "--format=json").Output()
if err != nil {
return nil, fmt.Errorf("pip list: %w", err)
}
var packages []PipPackage
if err := json.Unmarshal(out, &packages); err != nil {
return nil, fmt.Errorf("parse pip list output: %w", err)
}
return packages, nil
}
// VenvExists returns true if a virtual environment with a working python3
// binary exists at <dir>/venv.
func VenvExists(dir string) bool {
python := filepath.Join(dir, "venv", "bin", "python3")
info, err := os.Stat(python)
return err == nil && !info.IsDir()
}

View File

@ -0,0 +1,246 @@
package deploy
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// UnitConfig holds the parameters needed to generate a systemd unit file.
type UnitConfig struct {
Name string
Description string
ExecStart string
WorkingDirectory string
User string
Environment map[string]string
After string
RestartPolicy string
}
// GenerateUnit produces the contents of a systemd service unit file from cfg.
func GenerateUnit(cfg UnitConfig) string {
var b strings.Builder
// [Unit]
b.WriteString("[Unit]\n")
if cfg.Description != "" {
fmt.Fprintf(&b, "Description=%s\n", cfg.Description)
}
after := cfg.After
if after == "" {
after = "network.target"
}
fmt.Fprintf(&b, "After=%s\n", after)
// [Service]
b.WriteString("\n[Service]\n")
b.WriteString("Type=simple\n")
if cfg.User != "" {
fmt.Fprintf(&b, "User=%s\n", cfg.User)
}
if cfg.WorkingDirectory != "" {
fmt.Fprintf(&b, "WorkingDirectory=%s\n", cfg.WorkingDirectory)
}
fmt.Fprintf(&b, "ExecStart=%s\n", cfg.ExecStart)
restart := cfg.RestartPolicy
if restart == "" {
restart = "on-failure"
}
fmt.Fprintf(&b, "Restart=%s\n", restart)
b.WriteString("RestartSec=5\n")
// Environment variables — sorted for deterministic output.
if len(cfg.Environment) > 0 {
keys := make([]string, 0, len(cfg.Environment))
for k := range cfg.Environment {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Fprintf(&b, "Environment=%s=%s\n", k, cfg.Environment[k])
}
}
// [Install]
b.WriteString("\n[Install]\n")
b.WriteString("WantedBy=multi-user.target\n")
return b.String()
}
// InstallUnit writes a systemd unit file and reloads the daemon.
func InstallUnit(name, content string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
unitPath := filepath.Join("/etc/systemd/system", name+".service")
if err := os.WriteFile(unitPath, []byte(content), 0644); err != nil {
return fmt.Errorf("write unit file: %w", err)
}
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
if err != nil {
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// RemoveUnit stops, disables, and removes a systemd unit file, then reloads.
func RemoveUnit(name string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
unit := name + ".service"
// Best-effort stop and disable — ignore errors if already stopped/disabled.
exec.Command(systemctl, "stop", unit).Run()
exec.Command(systemctl, "disable", unit).Run()
unitPath := filepath.Join("/etc/systemd/system", unit)
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove unit file: %w", err)
}
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
if err != nil {
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
// Start starts a systemd unit.
func Start(unit string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "start", unit).CombinedOutput()
if err != nil {
return fmt.Errorf("start %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
}
return nil
}
// Stop stops a systemd unit.
func Stop(unit string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "stop", unit).CombinedOutput()
if err != nil {
return fmt.Errorf("stop %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
}
return nil
}
// Restart restarts a systemd unit.
func Restart(unit string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "restart", unit).CombinedOutput()
if err != nil {
return fmt.Errorf("restart %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
}
return nil
}
// Enable enables a systemd unit to start on boot.
func Enable(unit string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "enable", unit).CombinedOutput()
if err != nil {
return fmt.Errorf("enable %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
}
return nil
}
// Disable disables a systemd unit from starting on boot.
func Disable(unit string) error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "disable", unit).CombinedOutput()
if err != nil {
return fmt.Errorf("disable %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
}
return nil
}
// IsActive returns true if the given systemd unit is currently active.
func IsActive(unit string) (bool, error) {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return false, fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "is-active", unit).Output()
status := strings.TrimSpace(string(out))
if status == "active" {
return true, nil
}
// is-active exits non-zero for inactive/failed — that is not an error
// in our context, just means the unit is not active.
return false, nil
}
// Status returns the full systemctl status output for a unit.
func Status(unit string) (string, error) {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return "", fmt.Errorf("systemctl not found: %w", err)
}
// systemctl status exits non-zero for stopped services, so we use
// CombinedOutput and only treat missing-binary as a real error.
out, _ := exec.Command(systemctl, "status", unit).CombinedOutput()
return string(out), nil
}
// Logs returns the last n lines of journal output for a systemd unit.
func Logs(unit string, lines int) (string, error) {
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return "", fmt.Errorf("journalctl not found: %w", err)
}
out, err := exec.Command(journalctl, "-u", unit, "-n", fmt.Sprintf("%d", lines), "--no-pager").CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("journalctl: %s: %w", strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
// DaemonReload runs systemctl daemon-reload.
func DaemonReload() error {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return fmt.Errorf("systemctl not found: %w", err)
}
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
if err != nil {
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}

View File

@ -0,0 +1,366 @@
package float
import (
"log"
"net/http"
"sync"
"time"
"setec-manager/internal/db"
"github.com/gorilla/websocket"
)
// Bridge manages WebSocket connections for USB passthrough in Float Mode.
type Bridge struct {
db *db.DB
sessions map[string]*bridgeConn
mu sync.RWMutex
upgrader websocket.Upgrader
}
// bridgeConn tracks a single active WebSocket connection and its associated session.
type bridgeConn struct {
sessionID string
conn *websocket.Conn
devices []USBDevice
mu sync.Mutex
done chan struct{}
}
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingInterval = 30 * time.Second
maxMessageSize = 64 * 1024 // 64 KB max frame payload
)
// NewBridge creates a new Bridge with the given database reference.
func NewBridge(database *db.DB) *Bridge {
return &Bridge{
db: database,
sessions: make(map[string]*bridgeConn),
upgrader: websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true // Accept all origins; auth is handled via session token
},
},
}
}
// HandleWebSocket upgrades an HTTP connection to WebSocket and manages the
// binary frame protocol for USB passthrough. The session ID must be provided
// as a "session" query parameter.
func (b *Bridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session")
if sessionID == "" {
http.Error(w, "missing session parameter", http.StatusBadRequest)
return
}
// Validate session exists and is not expired
sess, err := b.db.GetFloatSession(sessionID)
if err != nil {
http.Error(w, "invalid session", http.StatusUnauthorized)
return
}
if time.Now().After(sess.ExpiresAt) {
http.Error(w, "session expired", http.StatusUnauthorized)
return
}
// Upgrade to WebSocket
conn, err := b.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("[float/bridge] upgrade failed for session %s: %v", sessionID, err)
return
}
bc := &bridgeConn{
sessionID: sessionID,
conn: conn,
done: make(chan struct{}),
}
// Register active connection
b.mu.Lock()
// Close any existing connection for this session
if existing, ok := b.sessions[sessionID]; ok {
close(existing.done)
existing.conn.Close()
}
b.sessions[sessionID] = bc
b.mu.Unlock()
log.Printf("[float/bridge] session %s connected from %s", sessionID, r.RemoteAddr)
// Start read/write loops
go b.writePump(bc)
b.readPump(bc)
}
// readPump reads binary frames from the WebSocket and dispatches them.
func (b *Bridge) readPump(bc *bridgeConn) {
defer b.cleanup(bc)
bc.conn.SetReadLimit(maxMessageSize)
bc.conn.SetReadDeadline(time.Now().Add(pongWait))
bc.conn.SetPongHandler(func(string) error {
bc.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
messageType, data, err := bc.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
log.Printf("[float/bridge] session %s read error: %v", bc.sessionID, err)
}
return
}
if messageType != websocket.BinaryMessage {
b.sendError(bc, 0x0001, "expected binary message")
continue
}
frameType, payload, err := DecodeFrame(data)
if err != nil {
b.sendError(bc, 0x0002, "malformed frame: "+err.Error())
continue
}
// Update session ping in DB
b.db.PingFloatSession(bc.sessionID)
switch frameType {
case FrameEnumerate:
b.handleEnumerate(bc)
case FrameOpen:
b.handleOpen(bc, payload)
case FrameClose:
b.handleClose(bc, payload)
case FrameTransferOut:
b.handleTransfer(bc, payload)
case FrameInterrupt:
b.handleInterrupt(bc, payload)
case FramePong:
// Client responded to our ping; no action needed
default:
b.sendError(bc, 0x0003, "unknown frame type")
}
}
}
// writePump sends periodic pings to keep the connection alive.
func (b *Bridge) writePump(bc *bridgeConn) {
ticker := time.NewTicker(pingInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
bc.mu.Lock()
bc.conn.SetWriteDeadline(time.Now().Add(writeWait))
err := bc.conn.WriteMessage(websocket.BinaryMessage, EncodeFrame(FramePing, nil))
bc.mu.Unlock()
if err != nil {
return
}
case <-bc.done:
return
}
}
}
// handleEnumerate responds with the current list of USB devices known to this
// session. In a full implementation, this would forward the enumerate request
// to the client-side USB agent and await its response. Here we return the
// cached device list.
func (b *Bridge) handleEnumerate(bc *bridgeConn) {
bc.mu.Lock()
devices := bc.devices
bc.mu.Unlock()
if devices == nil {
devices = []USBDevice{}
}
payload := EncodeDeviceList(devices)
b.sendFrame(bc, FrameEnumResult, payload)
}
// handleOpen processes a device open request. The payload contains
// [deviceID:2] identifying which device to claim.
func (b *Bridge) handleOpen(bc *bridgeConn, payload []byte) {
if len(payload) < 2 {
b.sendError(bc, 0x0010, "open: payload too short")
return
}
deviceID := uint16(payload[0])<<8 | uint16(payload[1])
// Verify the device exists in our known list
bc.mu.Lock()
found := false
for _, dev := range bc.devices {
if dev.DeviceID == deviceID {
found = true
break
}
}
bc.mu.Unlock()
if !found {
b.sendError(bc, 0x0011, "open: device not found")
return
}
// In a real implementation, this would claim the USB device via the host agent.
// For now, acknowledge the open request.
result := make([]byte, 3)
result[0] = payload[0]
result[1] = payload[1]
result[2] = 0x00 // success
b.sendFrame(bc, FrameOpenResult, result)
log.Printf("[float/bridge] session %s opened device 0x%04X", bc.sessionID, deviceID)
}
// handleClose processes a device close request. Payload: [deviceID:2].
func (b *Bridge) handleClose(bc *bridgeConn, payload []byte) {
if len(payload) < 2 {
b.sendError(bc, 0x0020, "close: payload too short")
return
}
deviceID := uint16(payload[0])<<8 | uint16(payload[1])
// Acknowledge close
result := make([]byte, 3)
result[0] = payload[0]
result[1] = payload[1]
result[2] = 0x00 // success
b.sendFrame(bc, FrameCloseResult, result)
log.Printf("[float/bridge] session %s closed device 0x%04X", bc.sessionID, deviceID)
}
// handleTransfer forwards a bulk/interrupt OUT transfer to the USB device.
func (b *Bridge) handleTransfer(bc *bridgeConn, payload []byte) {
deviceID, endpoint, transferData, err := DecodeTransfer(payload)
if err != nil {
b.sendError(bc, 0x0030, "transfer: "+err.Error())
return
}
// In a real implementation, the transfer data would be sent to the USB device
// via the host agent, and the response would be sent back. Here we acknowledge
// receipt of the transfer request.
log.Printf("[float/bridge] session %s transfer to device 0x%04X endpoint 0x%02X: %d bytes",
bc.sessionID, deviceID, endpoint, len(transferData))
// Build transfer result: [deviceID:2][endpoint:1][status:1]
result := make([]byte, 4)
result[0] = byte(deviceID >> 8)
result[1] = byte(deviceID)
result[2] = endpoint
result[3] = 0x00 // success
b.sendFrame(bc, FrameTransferResult, result)
}
// handleInterrupt processes an interrupt transfer request.
func (b *Bridge) handleInterrupt(bc *bridgeConn, payload []byte) {
if len(payload) < 3 {
b.sendError(bc, 0x0040, "interrupt: payload too short")
return
}
deviceID := uint16(payload[0])<<8 | uint16(payload[1])
endpoint := payload[2]
log.Printf("[float/bridge] session %s interrupt on device 0x%04X endpoint 0x%02X",
bc.sessionID, deviceID, endpoint)
// Acknowledge interrupt request
result := make([]byte, 4)
result[0] = payload[0]
result[1] = payload[1]
result[2] = endpoint
result[3] = 0x00 // success
b.sendFrame(bc, FrameInterruptResult, result)
}
// sendFrame writes a binary frame to the WebSocket connection.
func (b *Bridge) sendFrame(bc *bridgeConn, frameType byte, payload []byte) {
bc.mu.Lock()
defer bc.mu.Unlock()
bc.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := bc.conn.WriteMessage(websocket.BinaryMessage, EncodeFrame(frameType, payload)); err != nil {
log.Printf("[float/bridge] session %s write error: %v", bc.sessionID, err)
}
}
// sendError writes an error frame to the WebSocket connection.
func (b *Bridge) sendError(bc *bridgeConn, code uint16, message string) {
b.sendFrame(bc, FrameError, EncodeError(code, message))
}
// cleanup removes a connection from the active sessions and cleans up resources.
func (b *Bridge) cleanup(bc *bridgeConn) {
b.mu.Lock()
if current, ok := b.sessions[bc.sessionID]; ok && current == bc {
delete(b.sessions, bc.sessionID)
}
b.mu.Unlock()
close(bc.done)
bc.conn.Close()
log.Printf("[float/bridge] session %s disconnected", bc.sessionID)
}
// ActiveSessions returns the number of currently connected WebSocket sessions.
func (b *Bridge) ActiveSessions() int {
b.mu.RLock()
defer b.mu.RUnlock()
return len(b.sessions)
}
// DisconnectSession forcibly closes the WebSocket connection for a given session.
func (b *Bridge) DisconnectSession(sessionID string) {
b.mu.Lock()
bc, ok := b.sessions[sessionID]
if ok {
delete(b.sessions, sessionID)
}
b.mu.Unlock()
if ok {
close(bc.done)
bc.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "session terminated"),
time.Now().Add(writeWait),
)
bc.conn.Close()
log.Printf("[float/bridge] session %s forcibly disconnected", sessionID)
}
}
// UpdateDeviceList sets the known device list for a session (called when the
// client-side USB agent reports its attached devices).
func (b *Bridge) UpdateDeviceList(sessionID string, devices []USBDevice) {
b.mu.RLock()
bc, ok := b.sessions[sessionID]
b.mu.RUnlock()
if ok {
bc.mu.Lock()
bc.devices = devices
bc.mu.Unlock()
}
}

View File

@ -0,0 +1,225 @@
package float
import (
"encoding/binary"
"fmt"
)
// Frame type constants define the binary protocol for USB passthrough over WebSocket.
const (
FrameEnumerate byte = 0x01
FrameEnumResult byte = 0x02
FrameOpen byte = 0x03
FrameOpenResult byte = 0x04
FrameClose byte = 0x05
FrameCloseResult byte = 0x06
FrameTransferOut byte = 0x10
FrameTransferIn byte = 0x11
FrameTransferResult byte = 0x12
FrameInterrupt byte = 0x20
FrameInterruptResult byte = 0x21
FramePing byte = 0xFE
FramePong byte = 0xFF
FrameError byte = 0xE0
)
// frameHeaderSize is the fixed size of a frame header: 1 byte type + 4 bytes length.
const frameHeaderSize = 5
// USBDevice represents a USB device detected on the client host.
type USBDevice struct {
VendorID uint16 `json:"vendor_id"`
ProductID uint16 `json:"product_id"`
DeviceID uint16 `json:"device_id"`
Manufacturer string `json:"manufacturer"`
Product string `json:"product"`
SerialNumber string `json:"serial_number"`
Class byte `json:"class"`
SubClass byte `json:"sub_class"`
}
// deviceFixedSize is the fixed portion of a serialized USBDevice:
// VendorID(2) + ProductID(2) + DeviceID(2) + Class(1) + SubClass(1) + 3 string lengths (2 each) = 14
const deviceFixedSize = 14
// EncodeFrame builds a binary frame: [type:1][length:4 big-endian][payload:N].
func EncodeFrame(frameType byte, payload []byte) []byte {
frame := make([]byte, frameHeaderSize+len(payload))
frame[0] = frameType
binary.BigEndian.PutUint32(frame[1:5], uint32(len(payload)))
copy(frame[frameHeaderSize:], payload)
return frame
}
// DecodeFrame parses a binary frame into its type and payload.
func DecodeFrame(data []byte) (frameType byte, payload []byte, err error) {
if len(data) < frameHeaderSize {
return 0, nil, fmt.Errorf("frame too short: need at least %d bytes, got %d", frameHeaderSize, len(data))
}
frameType = data[0]
length := binary.BigEndian.Uint32(data[1:5])
if uint32(len(data)-frameHeaderSize) < length {
return 0, nil, fmt.Errorf("frame payload truncated: header says %d bytes, have %d", length, len(data)-frameHeaderSize)
}
payload = make([]byte, length)
copy(payload, data[frameHeaderSize:frameHeaderSize+int(length)])
return frameType, payload, nil
}
// encodeString writes a length-prefixed string (2-byte big-endian length + bytes).
func encodeString(buf []byte, offset int, s string) int {
b := []byte(s)
binary.BigEndian.PutUint16(buf[offset:], uint16(len(b)))
offset += 2
copy(buf[offset:], b)
return offset + len(b)
}
// decodeString reads a length-prefixed string from the buffer.
func decodeString(data []byte, offset int) (string, int, error) {
if offset+2 > len(data) {
return "", 0, fmt.Errorf("string length truncated at offset %d", offset)
}
slen := int(binary.BigEndian.Uint16(data[offset:]))
offset += 2
if offset+slen > len(data) {
return "", 0, fmt.Errorf("string data truncated at offset %d: need %d bytes", offset, slen)
}
s := string(data[offset : offset+slen])
return s, offset + slen, nil
}
// serializeDevice serializes a single USBDevice into bytes.
func serializeDevice(dev USBDevice) []byte {
mfr := []byte(dev.Manufacturer)
prod := []byte(dev.Product)
ser := []byte(dev.SerialNumber)
size := deviceFixedSize + len(mfr) + len(prod) + len(ser)
buf := make([]byte, size)
binary.BigEndian.PutUint16(buf[0:], dev.VendorID)
binary.BigEndian.PutUint16(buf[2:], dev.ProductID)
binary.BigEndian.PutUint16(buf[4:], dev.DeviceID)
buf[6] = dev.Class
buf[7] = dev.SubClass
off := 8
off = encodeString(buf, off, dev.Manufacturer)
off = encodeString(buf, off, dev.Product)
_ = encodeString(buf, off, dev.SerialNumber)
return buf
}
// EncodeDeviceList serializes a slice of USBDevices for a FrameEnumResult payload.
// Format: [count:2 big-endian][device...]
func EncodeDeviceList(devices []USBDevice) []byte {
// First pass: serialize each device to compute total size
serialized := make([][]byte, len(devices))
totalSize := 2 // 2 bytes for count
for i, dev := range devices {
serialized[i] = serializeDevice(dev)
totalSize += len(serialized[i])
}
buf := make([]byte, totalSize)
binary.BigEndian.PutUint16(buf[0:], uint16(len(devices)))
off := 2
for _, s := range serialized {
copy(buf[off:], s)
off += len(s)
}
return buf
}
// DecodeDeviceList deserializes a FrameEnumResult payload into a slice of USBDevices.
func DecodeDeviceList(data []byte) ([]USBDevice, error) {
if len(data) < 2 {
return nil, fmt.Errorf("device list too short: need at least 2 bytes")
}
count := int(binary.BigEndian.Uint16(data[0:]))
off := 2
devices := make([]USBDevice, 0, count)
for i := 0; i < count; i++ {
if off+8 > len(data) {
return nil, fmt.Errorf("device %d: fixed fields truncated at offset %d", i, off)
}
dev := USBDevice{
VendorID: binary.BigEndian.Uint16(data[off:]),
ProductID: binary.BigEndian.Uint16(data[off+2:]),
DeviceID: binary.BigEndian.Uint16(data[off+4:]),
Class: data[off+6],
SubClass: data[off+7],
}
off += 8
var err error
dev.Manufacturer, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d manufacturer: %w", i, err)
}
dev.Product, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d product: %w", i, err)
}
dev.SerialNumber, off, err = decodeString(data, off)
if err != nil {
return nil, fmt.Errorf("device %d serial: %w", i, err)
}
devices = append(devices, dev)
}
return devices, nil
}
// EncodeTransfer serializes a USB transfer payload.
// Format: [deviceID:2][endpoint:1][data:N]
func EncodeTransfer(deviceID uint16, endpoint byte, data []byte) []byte {
buf := make([]byte, 3+len(data))
binary.BigEndian.PutUint16(buf[0:], deviceID)
buf[2] = endpoint
copy(buf[3:], data)
return buf
}
// DecodeTransfer deserializes a USB transfer payload.
func DecodeTransfer(data []byte) (deviceID uint16, endpoint byte, transferData []byte, err error) {
if len(data) < 3 {
return 0, 0, nil, fmt.Errorf("transfer payload too short: need at least 3 bytes, got %d", len(data))
}
deviceID = binary.BigEndian.Uint16(data[0:])
endpoint = data[2]
transferData = make([]byte, len(data)-3)
copy(transferData, data[3:])
return deviceID, endpoint, transferData, nil
}
// EncodeError serializes an error response payload.
// Format: [code:2 big-endian][message:UTF-8 bytes]
func EncodeError(code uint16, message string) []byte {
msg := []byte(message)
buf := make([]byte, 2+len(msg))
binary.BigEndian.PutUint16(buf[0:], code)
copy(buf[2:], msg)
return buf
}
// DecodeError deserializes an error response payload.
func DecodeError(data []byte) (code uint16, message string) {
if len(data) < 2 {
return 0, ""
}
code = binary.BigEndian.Uint16(data[0:])
message = string(data[2:])
return code, message
}

View File

@ -0,0 +1,248 @@
package float
import (
"fmt"
"log"
"sync"
"time"
"setec-manager/internal/db"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
// Session represents an active Float Mode session, combining database state
// with the live WebSocket connection reference.
type Session struct {
ID string `json:"id"`
UserID int64 `json:"user_id"`
ClientIP string `json:"client_ip"`
ClientAgent string `json:"client_agent"`
USBBridge bool `json:"usb_bridge"`
ConnectedAt time.Time `json:"connected_at"`
ExpiresAt time.Time `json:"expires_at"`
LastPing *time.Time `json:"last_ping,omitempty"`
conn *websocket.Conn
}
// SessionManager provides in-memory + database-backed session lifecycle
// management for Float Mode connections.
type SessionManager struct {
sessions map[string]*Session
mu sync.RWMutex
db *db.DB
}
// NewSessionManager creates a new SessionManager backed by the given database.
func NewSessionManager(database *db.DB) *SessionManager {
return &SessionManager{
sessions: make(map[string]*Session),
db: database,
}
}
// Create generates a new Float session with a random UUID, storing it in both
// the in-memory map and the database.
func (sm *SessionManager) Create(userID int64, clientIP, agent string, ttl time.Duration) (string, error) {
id := uuid.New().String()
now := time.Now()
expiresAt := now.Add(ttl)
session := &Session{
ID: id,
UserID: userID,
ClientIP: clientIP,
ClientAgent: agent,
ConnectedAt: now,
ExpiresAt: expiresAt,
}
// Persist to database first
if err := sm.db.CreateFloatSession(id, userID, clientIP, agent, expiresAt); err != nil {
return "", fmt.Errorf("create session: db insert: %w", err)
}
// Store in memory
sm.mu.Lock()
sm.sessions[id] = session
sm.mu.Unlock()
log.Printf("[float/session] created session %s for user %d from %s (expires %s)",
id, userID, clientIP, expiresAt.Format(time.RFC3339))
return id, nil
}
// Get retrieves a session by ID, checking the in-memory cache first, then
// falling back to the database. Returns nil and an error if not found.
func (sm *SessionManager) Get(id string) (*Session, error) {
// Check memory first
sm.mu.RLock()
if sess, ok := sm.sessions[id]; ok {
sm.mu.RUnlock()
// Check if expired
if time.Now().After(sess.ExpiresAt) {
sm.Delete(id)
return nil, fmt.Errorf("session %s has expired", id)
}
return sess, nil
}
sm.mu.RUnlock()
// Fall back to database
dbSess, err := sm.db.GetFloatSession(id)
if err != nil {
return nil, fmt.Errorf("get session: %w", err)
}
// Check if expired
if time.Now().After(dbSess.ExpiresAt) {
sm.db.DeleteFloatSession(id)
return nil, fmt.Errorf("session %s has expired", id)
}
// Hydrate into memory
session := &Session{
ID: dbSess.ID,
UserID: dbSess.UserID,
ClientIP: dbSess.ClientIP,
ClientAgent: dbSess.ClientAgent,
USBBridge: dbSess.USBBridge,
ConnectedAt: dbSess.ConnectedAt,
ExpiresAt: dbSess.ExpiresAt,
LastPing: dbSess.LastPing,
}
sm.mu.Lock()
sm.sessions[id] = session
sm.mu.Unlock()
return session, nil
}
// Delete removes a session from both the in-memory map and the database.
func (sm *SessionManager) Delete(id string) error {
sm.mu.Lock()
sess, ok := sm.sessions[id]
if ok {
// Close the WebSocket connection if it exists
if sess.conn != nil {
sess.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "session deleted"),
time.Now().Add(5*time.Second),
)
sess.conn.Close()
}
delete(sm.sessions, id)
}
sm.mu.Unlock()
if err := sm.db.DeleteFloatSession(id); err != nil {
return fmt.Errorf("delete session: db delete: %w", err)
}
log.Printf("[float/session] deleted session %s", id)
return nil
}
// Ping updates the last-ping timestamp for a session in both memory and DB.
func (sm *SessionManager) Ping(id string) error {
now := time.Now()
sm.mu.Lock()
if sess, ok := sm.sessions[id]; ok {
sess.LastPing = &now
}
sm.mu.Unlock()
if err := sm.db.PingFloatSession(id); err != nil {
return fmt.Errorf("ping session: %w", err)
}
return nil
}
// CleanExpired removes all sessions that have passed their expiry time.
// Returns the number of sessions removed.
func (sm *SessionManager) CleanExpired() (int, error) {
now := time.Now()
// Clean from memory
sm.mu.Lock()
var expiredIDs []string
for id, sess := range sm.sessions {
if now.After(sess.ExpiresAt) {
expiredIDs = append(expiredIDs, id)
if sess.conn != nil {
sess.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "session expired"),
now.Add(5*time.Second),
)
sess.conn.Close()
}
}
}
for _, id := range expiredIDs {
delete(sm.sessions, id)
}
sm.mu.Unlock()
// Clean from database
count, err := sm.db.CleanExpiredFloatSessions()
if err != nil {
return len(expiredIDs), fmt.Errorf("clean expired: db: %w", err)
}
total := int(count)
if total > 0 {
log.Printf("[float/session] cleaned %d expired sessions", total)
}
return total, nil
}
// ActiveCount returns the number of sessions currently in the in-memory map.
func (sm *SessionManager) ActiveCount() int {
sm.mu.RLock()
defer sm.mu.RUnlock()
return len(sm.sessions)
}
// SetConn associates a WebSocket connection with a session.
func (sm *SessionManager) SetConn(id string, conn *websocket.Conn) {
sm.mu.Lock()
if sess, ok := sm.sessions[id]; ok {
sess.conn = conn
sess.USBBridge = true
}
sm.mu.Unlock()
}
// List returns all active (non-expired) sessions from the database.
func (sm *SessionManager) List() ([]Session, error) {
dbSessions, err := sm.db.ListFloatSessions()
if err != nil {
return nil, fmt.Errorf("list sessions: %w", err)
}
sessions := make([]Session, 0, len(dbSessions))
for _, dbs := range dbSessions {
if time.Now().After(dbs.ExpiresAt) {
continue
}
sessions = append(sessions, Session{
ID: dbs.ID,
UserID: dbs.UserID,
ClientIP: dbs.ClientIP,
ClientAgent: dbs.ClientAgent,
USBBridge: dbs.USBBridge,
ConnectedAt: dbs.ConnectedAt,
ExpiresAt: dbs.ExpiresAt,
LastPing: dbs.LastPing,
})
}
return sessions, nil
}

View File

@ -0,0 +1,272 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"setec-manager/internal/deploy"
)
type autarchStatus struct {
Installed bool `json:"installed"`
InstallDir string `json:"install_dir"`
GitCommit string `json:"git_commit"`
VenvReady bool `json:"venv_ready"`
PipPackages int `json:"pip_packages"`
WebRunning bool `json:"web_running"`
WebStatus string `json:"web_status"`
DNSRunning bool `json:"dns_running"`
DNSStatus string `json:"dns_status"`
}
func (h *Handler) AutarchStatus(w http.ResponseWriter, r *http.Request) {
status := h.getAutarchStatus()
h.render(w, "autarch.html", status)
}
func (h *Handler) AutarchStatusAPI(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.getAutarchStatus())
}
func (h *Handler) getAutarchStatus() autarchStatus {
dir := h.Config.Autarch.InstallDir
status := autarchStatus{InstallDir: dir}
// Check if installed
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
status.Installed = true
}
// Git commit
if hash, message, err := deploy.CurrentCommit(dir); err == nil {
status.GitCommit = hash + " " + message
}
// Venv
status.VenvReady = deploy.VenvExists(dir)
// Pip packages
venvDir := filepath.Join(dir, "venv")
if pkgs, err := deploy.ListPackages(venvDir); err == nil {
status.PipPackages = len(pkgs)
}
// Web service
webActive, _ := deploy.IsActive("autarch-web")
status.WebRunning = webActive
if webActive {
status.WebStatus = "active"
} else {
status.WebStatus = "inactive"
}
// DNS service
dnsActive, _ := deploy.IsActive("autarch-dns")
status.DNSRunning = dnsActive
if dnsActive {
status.DNSStatus = "active"
} else {
status.DNSStatus = "inactive"
}
return status
}
func (h *Handler) AutarchInstall(w http.ResponseWriter, r *http.Request) {
dir := h.Config.Autarch.InstallDir
repo := h.Config.Autarch.GitRepo
branch := h.Config.Autarch.GitBranch
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
writeError(w, http.StatusConflict, "AUTARCH already installed at "+dir)
return
}
depID, _ := h.DB.CreateDeployment(nil, "autarch_install")
var output strings.Builder
steps := []struct {
label string
fn func() error
}{
{"Clone from GitHub", func() error {
os.MkdirAll(filepath.Dir(dir), 0755)
out, err := deploy.Clone(repo, branch, dir)
output.WriteString(out)
return err
}},
{"Create Python venv", func() error {
return deploy.CreateVenv(dir)
}},
{"Upgrade pip", func() error {
venvDir := filepath.Join(dir, "venv")
deploy.UpgradePip(venvDir)
return nil
}},
{"Install pip packages", func() error {
reqFile := filepath.Join(dir, "requirements.txt")
if _, err := os.Stat(reqFile); err != nil {
return nil
}
venvDir := filepath.Join(dir, "venv")
out, err := deploy.InstallRequirements(venvDir, reqFile)
output.WriteString(out)
return err
}},
{"Install npm packages", func() error {
out, _ := deploy.NpmInstall(dir)
output.WriteString(out)
return nil
}},
{"Set permissions", func() error {
exec.Command("chown", "-R", "root:root", dir).Run()
exec.Command("chmod", "-R", "755", dir).Run()
for _, d := range []string{"data", "data/certs", "data/dns", "results", "dossiers", "models"} {
os.MkdirAll(filepath.Join(dir, d), 0755)
}
confPath := filepath.Join(dir, "autarch_settings.conf")
if _, err := os.Stat(confPath); err == nil {
exec.Command("chmod", "600", confPath).Run()
}
return nil
}},
{"Install systemd units", func() error {
h.installAutarchUnits(dir)
return nil
}},
}
for _, step := range steps {
output.WriteString(fmt.Sprintf("\n=== %s ===\n", step.label))
if err := step.fn(); err != nil {
h.DB.FinishDeployment(depID, "failed", output.String())
writeError(w, http.StatusInternalServerError, fmt.Sprintf("%s failed: %v", step.label, err))
return
}
}
h.DB.FinishDeployment(depID, "success", output.String())
writeJSON(w, http.StatusOK, map[string]string{"status": "installed"})
}
func (h *Handler) AutarchUpdate(w http.ResponseWriter, r *http.Request) {
dir := h.Config.Autarch.InstallDir
depID, _ := h.DB.CreateDeployment(nil, "autarch_update")
var output strings.Builder
// Git pull
out, err := deploy.Pull(dir)
output.WriteString(out)
if err != nil {
h.DB.FinishDeployment(depID, "failed", output.String())
writeError(w, http.StatusInternalServerError, "git pull failed")
return
}
// Reinstall pip packages
reqFile := filepath.Join(dir, "requirements.txt")
if _, err := os.Stat(reqFile); err == nil {
venvDir := filepath.Join(dir, "venv")
pipOut, _ := deploy.InstallRequirements(venvDir, reqFile)
output.WriteString(pipOut)
}
// Restart services
deploy.Restart("autarch-web")
deploy.Restart("autarch-dns")
h.DB.FinishDeployment(depID, "success", output.String())
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
func (h *Handler) AutarchStart(w http.ResponseWriter, r *http.Request) {
deploy.Start("autarch-web")
deploy.Start("autarch-dns")
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
}
func (h *Handler) AutarchStop(w http.ResponseWriter, r *http.Request) {
deploy.Stop("autarch-web")
deploy.Stop("autarch-dns")
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
}
func (h *Handler) AutarchRestart(w http.ResponseWriter, r *http.Request) {
deploy.Restart("autarch-web")
deploy.Restart("autarch-dns")
writeJSON(w, http.StatusOK, map[string]string{"status": "restarted"})
}
func (h *Handler) AutarchConfig(w http.ResponseWriter, r *http.Request) {
confPath := filepath.Join(h.Config.Autarch.InstallDir, "autarch_settings.conf")
data, err := os.ReadFile(confPath)
if err != nil {
writeError(w, http.StatusNotFound, "config not found")
return
}
writeJSON(w, http.StatusOK, map[string]string{"config": string(data)})
}
func (h *Handler) AutarchConfigUpdate(w http.ResponseWriter, r *http.Request) {
var body struct {
Config string `json:"config"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid body")
return
}
confPath := filepath.Join(h.Config.Autarch.InstallDir, "autarch_settings.conf")
if err := os.WriteFile(confPath, []byte(body.Config), 0600); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "saved"})
}
func (h *Handler) AutarchDNSBuild(w http.ResponseWriter, r *http.Request) {
dnsDir := filepath.Join(h.Config.Autarch.InstallDir, "services", "dns-server")
depID, _ := h.DB.CreateDeployment(nil, "dns_build")
cmd := exec.Command("go", "build", "-o", "autarch-dns", ".")
cmd.Dir = dnsDir
out, err := cmd.CombinedOutput()
if err != nil {
h.DB.FinishDeployment(depID, "failed", string(out))
writeError(w, http.StatusInternalServerError, "build failed: "+string(out))
return
}
h.DB.FinishDeployment(depID, "success", string(out))
writeJSON(w, http.StatusOK, map[string]string{"status": "built"})
}
func (h *Handler) installAutarchUnits(dir string) {
webUnit := deploy.GenerateUnit(deploy.UnitConfig{
Name: "autarch-web",
Description: "AUTARCH Web Dashboard",
ExecStart: filepath.Join(dir, "venv", "bin", "python3") + " " + filepath.Join(dir, "autarch_web.py"),
WorkingDirectory: dir,
User: "root",
Environment: map[string]string{"PYTHONUNBUFFERED": "1"},
})
dnsUnit := deploy.GenerateUnit(deploy.UnitConfig{
Name: "autarch-dns",
Description: "AUTARCH DNS Server",
ExecStart: filepath.Join(dir, "services", "dns-server", "autarch-dns") + " --config " + filepath.Join(dir, "data", "dns", "config.json"),
WorkingDirectory: dir,
User: "root",
})
deploy.InstallUnit("autarch-web", webUnit)
deploy.InstallUnit("autarch-dns", dnsUnit)
}

View File

@ -0,0 +1,146 @@
package handlers
import (
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
)
func (h *Handler) BackupList(w http.ResponseWriter, r *http.Request) {
backups, err := h.DB.ListBackups()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, backups)
return
}
h.render(w, "backups.html", backups)
}
func (h *Handler) BackupSite(w http.ResponseWriter, r *http.Request) {
id, err := paramInt(r, "id")
if err != nil {
writeError(w, http.StatusBadRequest, "invalid id")
return
}
site, err := h.DB.GetSite(id)
if err != nil || site == nil {
writeError(w, http.StatusNotFound, "site not found")
return
}
// Create backup directory
backupDir := h.Config.Backups.Dir
os.MkdirAll(backupDir, 0755)
timestamp := time.Now().Format("20060102-150405")
filename := fmt.Sprintf("site-%s-%s.tar.gz", site.Domain, timestamp)
backupPath := filepath.Join(backupDir, filename)
// Create tar.gz
cmd := exec.Command("tar", "-czf", backupPath, "-C", filepath.Dir(site.AppRoot), filepath.Base(site.AppRoot))
out, err := cmd.CombinedOutput()
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("backup failed: %s", string(out)))
return
}
// Get file size
info, _ := os.Stat(backupPath)
size := int64(0)
if info != nil {
size = info.Size()
}
bID, _ := h.DB.CreateBackup(&id, "site", backupPath, size)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": bID,
"path": backupPath,
"size": size,
})
}
func (h *Handler) BackupFull(w http.ResponseWriter, r *http.Request) {
backupDir := h.Config.Backups.Dir
os.MkdirAll(backupDir, 0755)
timestamp := time.Now().Format("20060102-150405")
filename := fmt.Sprintf("full-system-%s.tar.gz", timestamp)
backupPath := filepath.Join(backupDir, filename)
// Backup key directories
dirs := []string{
h.Config.Nginx.Webroot,
"/etc/nginx",
"/opt/setec-manager/data",
}
args := []string{"-czf", backupPath}
for _, d := range dirs {
if _, err := os.Stat(d); err == nil {
args = append(args, d)
}
}
cmd := exec.Command("tar", args...)
out, err := cmd.CombinedOutput()
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("backup failed: %s", string(out)))
return
}
info, _ := os.Stat(backupPath)
size := int64(0)
if info != nil {
size = info.Size()
}
bID, _ := h.DB.CreateBackup(nil, "full", backupPath, size)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": bID,
"path": backupPath,
"size": size,
})
}
func (h *Handler) BackupDelete(w http.ResponseWriter, r *http.Request) {
id, err := paramInt(r, "id")
if err != nil {
writeError(w, http.StatusBadRequest, "invalid id")
return
}
// Get backup info to delete file
var filePath string
h.DB.Conn().QueryRow(`SELECT file_path FROM backups WHERE id=?`, id).Scan(&filePath)
if filePath != "" {
os.Remove(filePath)
}
h.DB.DeleteBackup(id)
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
func (h *Handler) BackupDownload(w http.ResponseWriter, r *http.Request) {
id, err := paramInt(r, "id")
if err != nil {
writeError(w, http.StatusBadRequest, "invalid id")
return
}
var filePath string
h.DB.Conn().QueryRow(`SELECT file_path FROM backups WHERE id=?`, id).Scan(&filePath)
if filePath == "" {
writeError(w, http.StatusNotFound, "backup not found")
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(filePath)))
http.ServeFile(w, r, filePath)
}

View File

@ -0,0 +1,151 @@
package handlers
import (
"fmt"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
"setec-manager/internal/deploy"
"setec-manager/internal/system"
)
type systemInfo struct {
Hostname string `json:"hostname"`
OS string `json:"os"`
Arch string `json:"arch"`
CPUs int `json:"cpus"`
Uptime string `json:"uptime"`
LoadAvg string `json:"load_avg"`
MemTotal string `json:"mem_total"`
MemUsed string `json:"mem_used"`
MemPercent float64 `json:"mem_percent"`
DiskTotal string `json:"disk_total"`
DiskUsed string `json:"disk_used"`
DiskPercent float64 `json:"disk_percent"`
SiteCount int `json:"site_count"`
Services []serviceInfo `json:"services"`
}
type serviceInfo struct {
Name string `json:"name"`
Status string `json:"status"`
Running bool `json:"running"`
}
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
info := h.gatherSystemInfo()
h.render(w, "dashboard.html", info)
}
func (h *Handler) SystemInfo(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.gatherSystemInfo())
}
func (h *Handler) gatherSystemInfo() systemInfo {
info := systemInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
CPUs: runtime.NumCPU(),
}
// Hostname — no wrapper, keep exec.Command
if out, err := exec.Command("hostname").Output(); err == nil {
info.Hostname = strings.TrimSpace(string(out))
}
// Uptime
if ut, err := system.GetUptime(); err == nil {
info.Uptime = "up " + ut.HumanReadable
}
// Load average
if la, err := system.GetLoadAvg(); err == nil {
info.LoadAvg = fmt.Sprintf("%.2f %.2f %.2f", la.Load1, la.Load5, la.Load15)
}
// Memory
if mem, err := system.GetMemory(); err == nil {
info.MemTotal = mem.Total
info.MemUsed = mem.Used
if mem.TotalBytes > 0 {
info.MemPercent = float64(mem.UsedBytes) / float64(mem.TotalBytes) * 100
}
}
// Disk — find the root mount from the disk list
if disks, err := system.GetDisk(); err == nil {
for _, d := range disks {
if d.MountPoint == "/" {
info.DiskTotal = d.Size
info.DiskUsed = d.Used
pct := strings.TrimSuffix(d.UsePercent, "%")
info.DiskPercent, _ = strconv.ParseFloat(pct, 64)
break
}
}
// If no root mount found but we have disks, use the first one
if info.DiskTotal == "" && len(disks) > 0 {
d := disks[0]
info.DiskTotal = d.Size
info.DiskUsed = d.Used
pct := strings.TrimSuffix(d.UsePercent, "%")
info.DiskPercent, _ = strconv.ParseFloat(pct, 64)
}
}
// Site count
if sites, err := h.DB.ListSites(); err == nil {
info.SiteCount = len(sites)
}
// Services
services := []struct{ name, unit string }{
{"Nginx", "nginx"},
{"AUTARCH Web", "autarch-web"},
{"AUTARCH DNS", "autarch-dns"},
{"Setec Manager", "setec-manager"},
}
for _, svc := range services {
si := serviceInfo{Name: svc.name}
active, err := deploy.IsActive(svc.unit)
if err == nil && active {
si.Status = "active"
si.Running = true
} else {
si.Status = "inactive"
si.Running = false
}
info.Services = append(info.Services, si)
}
return info
}
func formatBytes(b float64) string {
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for b >= 1024 && i < len(units)-1 {
b /= 1024
i++
}
return strconv.FormatFloat(b, 'f', 1, 64) + " " + units[i]
}
// uptimeSince returns a human-readable duration.
func uptimeSince(d time.Duration) string {
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
mins := int(d.Minutes()) % 60
if days > 0 {
return strconv.Itoa(days) + "d " + strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
}
if hours > 0 {
return strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
}
return strconv.Itoa(mins) + "m"
}

View File

@ -0,0 +1,184 @@
package handlers
import (
"encoding/json"
"net/http"
"setec-manager/internal/system"
)
type firewallRule struct {
ID int64 `json:"id"`
Direction string `json:"direction"`
Protocol string `json:"protocol"`
Port string `json:"port"`
Source string `json:"source"`
Action string `json:"action"`
Comment string `json:"comment"`
}
type firewallStatus struct {
Enabled bool `json:"enabled"`
Rules []firewallRule `json:"rules"`
UFWOut string `json:"ufw_output"`
}
func (h *Handler) FirewallList(w http.ResponseWriter, r *http.Request) {
status := h.getFirewallStatus()
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, status)
return
}
h.render(w, "firewall.html", status)
}
func (h *Handler) FirewallStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.getFirewallStatus())
}
func (h *Handler) FirewallAddRule(w http.ResponseWriter, r *http.Request) {
var rule firewallRule
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
rule.Port = r.FormValue("port")
rule.Protocol = r.FormValue("protocol")
rule.Source = r.FormValue("source")
rule.Action = r.FormValue("action")
rule.Comment = r.FormValue("comment")
}
if rule.Port == "" {
writeError(w, http.StatusBadRequest, "port is required")
return
}
if rule.Protocol == "" {
rule.Protocol = "tcp"
}
if rule.Action == "" {
rule.Action = "allow"
}
if rule.Source == "" {
rule.Source = "any"
}
ufwRule := system.UFWRule{
Port: rule.Port,
Protocol: rule.Protocol,
Source: rule.Source,
Action: rule.Action,
Comment: rule.Comment,
}
if err := system.FirewallAddRule(ufwRule); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Save to DB
h.DB.Conn().Exec(`INSERT INTO firewall_rules (direction, protocol, port, source, action, comment)
VALUES (?, ?, ?, ?, ?, ?)`, "in", rule.Protocol, rule.Port, rule.Source, rule.Action, rule.Comment)
writeJSON(w, http.StatusCreated, map[string]string{"status": "rule added"})
}
func (h *Handler) FirewallDeleteRule(w http.ResponseWriter, r *http.Request) {
id, err := paramInt(r, "id")
if err != nil {
writeError(w, http.StatusBadRequest, "invalid id")
return
}
// Get rule from DB to build delete command
var port, protocol, action string
err = h.DB.Conn().QueryRow(`SELECT port, protocol, action FROM firewall_rules WHERE id=?`, id).
Scan(&port, &protocol, &action)
if err != nil {
writeError(w, http.StatusNotFound, "rule not found")
return
}
system.FirewallDeleteRule(system.UFWRule{
Port: port,
Protocol: protocol,
Action: action,
})
h.DB.Conn().Exec(`DELETE FROM firewall_rules WHERE id=?`, id)
writeJSON(w, http.StatusOK, map[string]string{"status": "rule deleted"})
}
func (h *Handler) FirewallEnable(w http.ResponseWriter, r *http.Request) {
if err := system.FirewallEnable(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
}
func (h *Handler) FirewallDisable(w http.ResponseWriter, r *http.Request) {
if err := system.FirewallDisable(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
}
func (h *Handler) getFirewallStatus() firewallStatus {
status := firewallStatus{}
enabled, _, raw, _ := system.FirewallStatus()
status.UFWOut = raw
status.Enabled = enabled
// Load rules from DB
rows, err := h.DB.Conn().Query(`SELECT id, direction, protocol, port, source, action, comment
FROM firewall_rules WHERE enabled=TRUE ORDER BY id`)
if err == nil {
defer rows.Close()
for rows.Next() {
var rule firewallRule
rows.Scan(&rule.ID, &rule.Direction, &rule.Protocol, &rule.Port,
&rule.Source, &rule.Action, &rule.Comment)
status.Rules = append(status.Rules, rule)
}
}
return status
}
func (h *Handler) InstallDefaultFirewall() error {
// Set default policies
system.FirewallSetDefaults("deny", "allow")
// Add default rules
defaultRules := []system.UFWRule{
{Port: "22", Protocol: "tcp", Action: "allow", Comment: "SSH"},
{Port: "80", Protocol: "tcp", Action: "allow", Comment: "HTTP"},
{Port: "443", Protocol: "tcp", Action: "allow", Comment: "HTTPS"},
{Port: "9090", Protocol: "tcp", Action: "allow", Comment: "Setec Manager"},
{Port: "8181", Protocol: "tcp", Action: "allow", Comment: "AUTARCH Web"},
{Port: "53", Protocol: "", Action: "allow", Comment: "AUTARCH DNS"},
}
for _, rule := range defaultRules {
system.FirewallAddRule(rule)
}
// Enable the firewall
system.FirewallEnable()
// Record in DB
dbRules := []firewallRule{
{Port: "22", Protocol: "tcp", Action: "allow", Comment: "SSH"},
{Port: "80", Protocol: "tcp", Action: "allow", Comment: "HTTP"},
{Port: "443", Protocol: "tcp", Action: "allow", Comment: "HTTPS"},
{Port: "9090", Protocol: "tcp", Action: "allow", Comment: "Setec Manager"},
{Port: "8181", Protocol: "tcp", Action: "allow", Comment: "AUTARCH Web"},
{Port: "53", Protocol: "tcp", Action: "allow", Comment: "AUTARCH DNS"},
}
for _, rule := range dbRules {
h.DB.Conn().Exec(`INSERT OR IGNORE INTO firewall_rules (direction, protocol, port, source, action, comment)
VALUES ('in', ?, ?, 'any', ?, ?)`, rule.Protocol, rule.Port, rule.Action, rule.Comment)
}
return nil
}

View File

@ -0,0 +1,66 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
"github.com/google/uuid"
)
func (h *Handler) FloatRegister(w http.ResponseWriter, r *http.Request) {
if !h.Config.Float.Enabled {
writeError(w, http.StatusServiceUnavailable, "Float Mode is disabled")
return
}
var body struct {
UserAgent string `json:"user_agent"`
}
json.NewDecoder(r.Body).Decode(&body)
// Parse TTL
ttl, err := time.ParseDuration(h.Config.Float.SessionTTL)
if err != nil {
ttl = 24 * time.Hour
}
sessionID := uuid.New().String()
clientIP := r.RemoteAddr
if err := h.DB.CreateFloatSession(sessionID, 0, clientIP, body.UserAgent, time.Now().Add(ttl)); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]string{
"session_id": sessionID,
"expires_in": h.Config.Float.SessionTTL,
})
}
func (h *Handler) FloatSessions(w http.ResponseWriter, r *http.Request) {
// Clean expired sessions first
h.DB.CleanExpiredFloatSessions()
sessions, err := h.DB.ListFloatSessions()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, sessions)
return
}
h.render(w, "float.html", sessions)
}
func (h *Handler) FloatDisconnect(w http.ResponseWriter, r *http.Request) {
id := paramStr(r, "id")
if err := h.DB.DeleteFloatSession(id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "disconnected"})
}

View File

@ -0,0 +1,103 @@
package handlers
import (
"encoding/json"
"html/template"
"io/fs"
"log"
"net/http"
"strconv"
"sync"
"setec-manager/internal/config"
"setec-manager/internal/db"
"setec-manager/internal/hosting"
"setec-manager/web"
"github.com/go-chi/chi/v5"
)
type Handler struct {
Config *config.Config
DB *db.DB
HostingConfigs *hosting.ProviderConfigStore
tmpl *template.Template
once sync.Once
}
func New(cfg *config.Config, database *db.DB, hostingConfigs *hosting.ProviderConfigStore) *Handler {
return &Handler{
Config: cfg,
DB: database,
HostingConfigs: hostingConfigs,
}
}
func (h *Handler) getTemplates() *template.Template {
h.once.Do(func() {
funcMap := template.FuncMap{
"eq": func(a, b interface{}) bool { return a == b },
"ne": func(a, b interface{}) bool { return a != b },
"default": func(val, def interface{}) interface{} {
if val == nil || val == "" || val == 0 || val == false {
return def
}
return val
},
}
var err error
h.tmpl, err = template.New("").Funcs(funcMap).ParseFS(web.TemplateFS, "templates/*.html")
if err != nil {
log.Fatalf("Failed to parse templates: %v", err)
}
// Also parse from the static FS to make sure it's available
_ = fs.WalkDir(web.StaticFS, ".", func(path string, d fs.DirEntry, err error) error {
return nil
})
})
return h.tmpl
}
type pageData struct {
Title string
Data interface{}
Config *config.Config
}
func (h *Handler) render(w http.ResponseWriter, name string, data interface{}) {
pd := pageData{
Data: data,
Config: h.Config,
}
t := h.getTemplates().Lookup(name)
if t == nil {
http.Error(w, "Template not found: "+name, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.Execute(w, pd); err != nil {
log.Printf("[template] %s: %v", name, err)
}
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
func paramInt(r *http.Request, name string) (int64, error) {
return strconv.ParseInt(chi.URLParam(r, name), 10, 64)
}
func paramStr(r *http.Request, name string) string {
return chi.URLParam(r, name)
}

View File

@ -0,0 +1,697 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"setec-manager/internal/hosting"
)
// providerInfo is the view model sent to the hosting template and JSON responses.
type providerInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Connected bool `json:"connected"`
HasConfig bool `json:"has_config"`
}
// listProviderInfo builds a summary of every registered provider and its config status.
func (h *Handler) listProviderInfo() []providerInfo {
names := hosting.List()
out := make([]providerInfo, 0, len(names))
for _, name := range names {
p, ok := hosting.Get(name)
if !ok {
continue
}
pi := providerInfo{
Name: p.Name(),
DisplayName: p.DisplayName(),
}
if h.HostingConfigs != nil {
cfg, err := h.HostingConfigs.Load(name)
if err == nil && cfg != nil {
pi.HasConfig = true
if cfg.APIKey != "" {
pi.Connected = true
}
}
}
out = append(out, pi)
}
return out
}
// getProvider retrieves the provider from the URL and returns it. On error it
// writes an HTTP error and returns nil.
func (h *Handler) getProvider(w http.ResponseWriter, r *http.Request) hosting.Provider {
name := paramStr(r, "provider")
if name == "" {
writeError(w, http.StatusBadRequest, "missing provider name")
return nil
}
p, ok := hosting.Get(name)
if !ok {
writeError(w, http.StatusNotFound, "hosting provider "+name+" not registered")
return nil
}
return p
}
// configureProvider loads saved credentials for a provider and calls Configure
// on it so it is ready for API calls. Returns false if no config is saved.
func (h *Handler) configureProvider(p hosting.Provider) bool {
if h.HostingConfigs == nil {
return false
}
cfg, err := h.HostingConfigs.Load(p.Name())
if err != nil || cfg == nil || cfg.APIKey == "" {
return false
}
if err := p.Configure(*cfg); err != nil {
log.Printf("[hosting] configure %s: %v", p.Name(), err)
return false
}
return true
}
// ─── Page Handlers ───────────────────────────────────────────────────────────
// HostingProviders renders the hosting management page (GET /hosting).
func (h *Handler) HostingProviders(w http.ResponseWriter, r *http.Request) {
providers := h.listProviderInfo()
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, providers)
return
}
h.render(w, "hosting.html", map[string]interface{}{
"Providers": providers,
})
}
// HostingProviderConfig returns the config page/detail for a single provider.
func (h *Handler) HostingProviderConfig(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
var savedCfg *hosting.ProviderConfig
if h.HostingConfigs != nil {
savedCfg, _ = h.HostingConfigs.Load(p.Name())
}
data := map[string]interface{}{
"Provider": providerInfo{Name: p.Name(), DisplayName: p.DisplayName()},
"Config": savedCfg,
"Providers": h.listProviderInfo(),
}
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, data)
return
}
h.render(w, "hosting.html", data)
}
// ─── Configuration ───────────────────────────────────────────────────────────
// HostingProviderSave saves API credentials and tests the connection.
func (h *Handler) HostingProviderSave(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
var body struct {
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
Extra map[string]string `json:"extra"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.APIKey == "" {
writeError(w, http.StatusBadRequest, "api_key is required")
return
}
cfg := hosting.ProviderConfig{
APIKey: body.APIKey,
APISecret: body.APISecret,
Extra: body.Extra,
}
// Configure the provider to validate credentials.
if err := p.Configure(cfg); err != nil {
writeError(w, http.StatusBadRequest, "configure: "+err.Error())
return
}
// Test the connection.
connected := true
if err := p.TestConnection(); err != nil {
log.Printf("[hosting] test %s failed: %v", p.Name(), err)
connected = false
}
// Persist.
if h.HostingConfigs != nil {
if err := h.HostingConfigs.Save(p.Name(), cfg); err != nil {
writeError(w, http.StatusInternalServerError, "save config: "+err.Error())
return
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "saved",
"connected": connected,
})
}
// HostingProviderTest tests the connection to a provider without saving.
func (h *Handler) HostingProviderTest(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured — save credentials first")
return
}
if err := p.TestConnection(); err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"connected": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"connected": true,
})
}
// ─── DNS ─────────────────────────────────────────────────────────────────────
// HostingDNSList returns DNS records for a domain.
func (h *Handler) HostingDNSList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
records, err := p.ListDNSRecords(domain)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, records)
}
// HostingDNSUpdate replaces DNS records for a domain.
func (h *Handler) HostingDNSUpdate(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Records []hosting.DNSRecord `json:"records"`
Overwrite bool `json:"overwrite"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if err := p.UpdateDNSRecords(domain, body.Records, body.Overwrite); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// HostingDNSDelete deletes DNS records matching name+type for a domain.
func (h *Handler) HostingDNSDelete(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Name string `json:"name"`
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
filter := hosting.DNSRecordFilter{Name: body.Name, Type: body.Type}
if err := p.DeleteDNSRecord(domain, filter); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// HostingDNSReset resets DNS records for a domain to provider defaults.
func (h *Handler) HostingDNSReset(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
if err := p.ResetDNSRecords(domain); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "reset"})
}
// ─── Domains ─────────────────────────────────────────────────────────────────
// HostingDomainsList returns all domains registered with the provider.
func (h *Handler) HostingDomainsList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domains, err := p.ListDomains()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, domains)
}
// HostingDomainsCheck checks availability of a domain across TLDs.
func (h *Handler) HostingDomainsCheck(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var body struct {
Domain string `json:"domain"`
TLDs []string `json:"tlds"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
results, err := p.CheckDomainAvailability(body.Domain, body.TLDs)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, results)
}
// HostingDomainsPurchase purchases a domain.
func (h *Handler) HostingDomainsPurchase(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var req hosting.DomainPurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if req.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
if req.Years <= 0 {
req.Years = 1
}
result, err := p.PurchaseDomain(req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, result)
}
// HostingDomainNameservers updates nameservers for a domain.
func (h *Handler) HostingDomainNameservers(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Nameservers []string `json:"nameservers"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if len(body.Nameservers) == 0 {
writeError(w, http.StatusBadRequest, "nameservers list is empty")
return
}
if err := p.SetNameservers(domain, body.Nameservers); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// HostingDomainLock toggles the registrar lock on a domain.
func (h *Handler) HostingDomainLock(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Locked bool `json:"locked"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
var err error
if body.Locked {
err = p.EnableDomainLock(domain)
} else {
err = p.DisableDomainLock(domain)
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "locked": body.Locked})
}
// HostingDomainPrivacy toggles privacy protection on a domain.
func (h *Handler) HostingDomainPrivacy(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Privacy bool `json:"privacy"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
var err error
if body.Privacy {
err = p.EnablePrivacyProtection(domain)
} else {
err = p.DisablePrivacyProtection(domain)
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "privacy": body.Privacy})
}
// ─── VMs / VPS ───────────────────────────────────────────────────────────────
// HostingVMsList lists all VMs for a provider.
func (h *Handler) HostingVMsList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
vms, err := p.ListVMs()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, vms)
}
// HostingVMGet returns details for a single VM.
func (h *Handler) HostingVMGet(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
id := paramStr(r, "id")
vm, err := p.GetVM(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if vm == nil {
writeError(w, http.StatusNotFound, "VM not found")
return
}
writeJSON(w, http.StatusOK, vm)
}
// HostingVMCreate creates a new VM.
func (h *Handler) HostingVMCreate(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var req hosting.VMCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if req.Plan == "" {
writeError(w, http.StatusBadRequest, "plan is required")
return
}
if req.DataCenterID == "" {
writeError(w, http.StatusBadRequest, "data_center_id is required")
return
}
result, err := p.CreateVM(req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, result)
}
// HostingDataCenters lists available data centers.
func (h *Handler) HostingDataCenters(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
dcs, err := p.ListDataCenters()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, dcs)
}
// ─── SSH Keys ────────────────────────────────────────────────────────────────
// HostingSSHKeys lists SSH keys for the provider account.
func (h *Handler) HostingSSHKeys(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
keys, err := p.ListSSHKeys()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, keys)
}
// HostingSSHKeyAdd adds an SSH key.
func (h *Handler) HostingSSHKeyAdd(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var body struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Name == "" || body.PublicKey == "" {
writeError(w, http.StatusBadRequest, "name and public_key are required")
return
}
key, err := p.AddSSHKey(body.Name, body.PublicKey)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, key)
}
// HostingSSHKeyDelete deletes an SSH key.
func (h *Handler) HostingSSHKeyDelete(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
id := paramStr(r, "id")
if err := p.DeleteSSHKey(id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// ─── Billing ─────────────────────────────────────────────────────────────────
// HostingSubscriptions lists billing subscriptions.
func (h *Handler) HostingSubscriptions(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
subs, err := p.ListSubscriptions()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, subs)
}
// HostingCatalog returns the product catalog.
func (h *Handler) HostingCatalog(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
category := r.URL.Query().Get("category")
items, err := p.GetCatalog(category)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, items)
}

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