Autarch Is Setting The Internet Free
This commit is contained in:
parent
559f447753
commit
32842d9873
Binary file not shown.
@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
source "$(dirname "$(realpath "$0")")/venv/bin/activate"
|
|
||||||
650
autarch_float.md
Normal file
650
autarch_float.md
Normal 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.
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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-----
|
|
||||||
@ -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-----
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
Site,URL,Category,Status,Confidence
|
|
||||||
GitHub,https://github.com/test,,good,85
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -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": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -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
|
|
||||||
@ -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": []
|
|
||||||
}
|
|
||||||
@ -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": []
|
|
||||||
}
|
|
||||||
@ -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": []
|
|
||||||
}
|
|
||||||
@ -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": []
|
|
||||||
}
|
|
||||||
@ -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 "Assist colleagues by answering questions and sharing information about work methods, processes and procedures" what you could put in your group health job description is something like "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", 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 "Tell me about a time you didn't meet a deadline and what did you learn." 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 "safety" 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 "rec" 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&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&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&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&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 "seasons pass" 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&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 "pull the pol-19 red card" 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 "done" 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
76365
data/sites/dh.json
76365
data/sites/dh.json
File diff suppressed because it is too large
Load Diff
35922
data/sites/maigret.json
35922
data/sites/maigret.json
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
63308
data/sites/snoop.json
63308
data/sites/snoop.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
63748
data/sites/test.json
63748
data/sites/test.json
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
@ -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")
|
|
||||||
@ -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'
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"username": "admin",
|
|
||||||
"password": "admin",
|
|
||||||
"force_change": true
|
|
||||||
}
|
|
||||||
586
docs/install.sh
Normal file
586
docs/install.sh
Normal 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
669
docs/setec_manager_plan.md
Normal 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
|
||||||
|
```
|
||||||
BIN
services/server-manager/autarch-server-manager
Normal file
BIN
services/server-manager/autarch-server-manager
Normal file
Binary file not shown.
51
services/server-manager/build.sh
Normal file
51
services/server-manager/build.sh
Normal 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 "══════════════════════════════════════════════════════"
|
||||||
BIN
services/server-manager/cmd.exe
Normal file
BIN
services/server-manager/cmd.exe
Normal file
Binary file not shown.
25
services/server-manager/cmd/main.go
Normal file
25
services/server-manager/cmd/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
services/server-manager/go.mod
Normal file
32
services/server-manager/go.mod
Normal 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
|
||||||
|
)
|
||||||
51
services/server-manager/go.sum
Normal file
51
services/server-manager/go.sum
Normal 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=
|
||||||
132
services/server-manager/internal/config/ini.go
Normal file
132
services/server-manager/internal/config/ini.go
Normal 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)
|
||||||
|
}
|
||||||
826
services/server-manager/internal/tui/app.go
Normal file
826
services/server-manager/internal/tui/app.go
Normal 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
|
||||||
|
}
|
||||||
48
services/server-manager/internal/tui/helpers.go
Normal file
48
services/server-manager/internal/tui/helpers.go
Normal 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"
|
||||||
|
}
|
||||||
99
services/server-manager/internal/tui/inputs.go
Normal file
99
services/server-manager/internal/tui/inputs.go
Normal 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)
|
||||||
|
}
|
||||||
161
services/server-manager/internal/tui/streaming.go
Normal file
161
services/server-manager/internal/tui/streaming.go
Normal 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
|
||||||
493
services/server-manager/internal/tui/view_deploy.go
Normal file
493
services/server-manager/internal/tui/view_deploy.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
416
services/server-manager/internal/tui/view_deps.go
Normal file
416
services/server-manager/internal/tui/view_deps.go
Normal 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
|
||||||
|
}
|
||||||
761
services/server-manager/internal/tui/view_dns.go
Normal file
761
services/server-manager/internal/tui/view_dns.go
Normal 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."}}
|
||||||
|
}
|
||||||
52
services/server-manager/internal/tui/view_main.go
Normal file
52
services/server-manager/internal/tui/view_main.go
Normal 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
|
||||||
|
}
|
||||||
273
services/server-manager/internal/tui/view_modules.go
Normal file
273
services/server-manager/internal/tui/view_modules.go
Normal 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
|
||||||
|
}
|
||||||
380
services/server-manager/internal/tui/view_service.go
Normal file
380
services/server-manager/internal/tui/view_service.go
Normal 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)
|
||||||
|
}
|
||||||
249
services/server-manager/internal/tui/view_settings.go
Normal file
249
services/server-manager/internal/tui/view_settings.go
Normal 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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
services/server-manager/internal/tui/view_users.go
Normal file
225
services/server-manager/internal/tui/view_users.go
Normal 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."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
services/server-manager/internal/users/credentials.go
Normal file
114
services/server-manager/internal/users/credentials.go
Normal 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)
|
||||||
|
}
|
||||||
12
services/setec-manager/build.sh
Normal file
12
services/setec-manager/build.sh
Normal 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'"
|
||||||
Binary file not shown.
258
services/setec-manager/cmd/main.go
Normal file
258
services/setec-manager/cmd/main.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
44
services/setec-manager/config.yaml
Normal file
44
services/setec-manager/config.yaml
Normal 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
|
||||||
1660
services/setec-manager/docs/api-reference.md
Normal file
1660
services/setec-manager/docs/api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
790
services/setec-manager/docs/custom-provider-guide.md
Normal file
790
services/setec-manager/docs/custom-provider-guide.md
Normal 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.
|
||||||
859
services/setec-manager/docs/hosting-providers.md
Normal file
859
services/setec-manager/docs/hosting-providers.md
Normal 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.
|
||||||
365
services/setec-manager/docs/hostinger-setup.md
Normal file
365
services/setec-manager/docs/hostinger-setup.md
Normal 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.
|
||||||
25
services/setec-manager/go.mod
Normal file
25
services/setec-manager/go.mod
Normal 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
|
||||||
|
)
|
||||||
65
services/setec-manager/go.sum
Normal file
65
services/setec-manager/go.sum
Normal 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=
|
||||||
361
services/setec-manager/internal/acme/acme.go
Normal file
361
services/setec-manager/internal/acme/acme.go
Normal 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
|
||||||
|
}
|
||||||
146
services/setec-manager/internal/config/config.go
Normal file
146
services/setec-manager/internal/config/config.go
Normal 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)
|
||||||
|
}
|
||||||
46
services/setec-manager/internal/db/backups.go
Normal file
46
services/setec-manager/internal/db/backups.go
Normal 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
|
||||||
|
}
|
||||||
163
services/setec-manager/internal/db/db.go
Normal file
163
services/setec-manager/internal/db/db.go
Normal 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
|
||||||
|
);`
|
||||||
60
services/setec-manager/internal/db/deployments.go
Normal file
60
services/setec-manager/internal/db/deployments.go
Normal 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()
|
||||||
|
}
|
||||||
70
services/setec-manager/internal/db/float.go
Normal file
70
services/setec-manager/internal/db/float.go
Normal 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()
|
||||||
|
}
|
||||||
107
services/setec-manager/internal/db/sites.go
Normal file
107
services/setec-manager/internal/db/sites.go
Normal 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
|
||||||
|
}
|
||||||
124
services/setec-manager/internal/db/users.go
Normal file
124
services/setec-manager/internal/db/users.go
Normal 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
|
||||||
|
}
|
||||||
144
services/setec-manager/internal/deploy/git.go
Normal file
144
services/setec-manager/internal/deploy/git.go
Normal 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
|
||||||
|
}
|
||||||
100
services/setec-manager/internal/deploy/node.go
Normal file
100
services/setec-manager/internal/deploy/node.go
Normal 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
|
||||||
|
}
|
||||||
93
services/setec-manager/internal/deploy/python.go
Normal file
93
services/setec-manager/internal/deploy/python.go
Normal 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()
|
||||||
|
}
|
||||||
246
services/setec-manager/internal/deploy/systemd.go
Normal file
246
services/setec-manager/internal/deploy/systemd.go
Normal 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
|
||||||
|
}
|
||||||
366
services/setec-manager/internal/float/bridge.go
Normal file
366
services/setec-manager/internal/float/bridge.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
225
services/setec-manager/internal/float/protocol.go
Normal file
225
services/setec-manager/internal/float/protocol.go
Normal 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
|
||||||
|
}
|
||||||
248
services/setec-manager/internal/float/session.go
Normal file
248
services/setec-manager/internal/float/session.go
Normal 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
|
||||||
|
}
|
||||||
272
services/setec-manager/internal/handlers/autarch.go
Normal file
272
services/setec-manager/internal/handlers/autarch.go
Normal 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)
|
||||||
|
}
|
||||||
146
services/setec-manager/internal/handlers/backups.go
Normal file
146
services/setec-manager/internal/handlers/backups.go
Normal 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)
|
||||||
|
}
|
||||||
151
services/setec-manager/internal/handlers/dashboard.go
Normal file
151
services/setec-manager/internal/handlers/dashboard.go
Normal 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"
|
||||||
|
}
|
||||||
184
services/setec-manager/internal/handlers/firewall.go
Normal file
184
services/setec-manager/internal/handlers/firewall.go
Normal 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
|
||||||
|
}
|
||||||
66
services/setec-manager/internal/handlers/float.go
Normal file
66
services/setec-manager/internal/handlers/float.go
Normal 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"})
|
||||||
|
}
|
||||||
103
services/setec-manager/internal/handlers/handlers.go
Normal file
103
services/setec-manager/internal/handlers/handlers.go
Normal 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)
|
||||||
|
}
|
||||||
697
services/setec-manager/internal/handlers/hosting.go
Normal file
697
services/setec-manager/internal/handlers/hosting.go
Normal 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
Loading…
x
Reference in New Issue
Block a user