feat(system-hub): Add HAProxy routes health check panel

- Add get_service_health RPCD method to check all HAProxy routes
- Integrate /usr/sbin/service-health-check for backend HTTP probing
- Add health panel in services.js with up/down stats and health %
- Display down services list with tooltips showing IP:port
- Add refresh button for manual health check trigger
- Update ACL with get_service_health read permission
- 5-minute cache for health data with force-refresh option

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-09 13:28:06 +01:00
parent 0cdbffda4c
commit fe762b6eb1
7 changed files with 640 additions and 22 deletions

View File

@ -517,7 +517,17 @@
"Bash(__NEW_LINE_17d05a792c15c52e__ echo \"\")",
"Bash(__NEW_LINE_9054b2ef7cdd675f__ echo \"\")",
"Bash(do echo -n \"$host: \")",
"Bash(do echo -n \"$d: \")"
"Bash(do echo -n \"$d: \")",
"WebFetch(domain:www.gandi.net)",
"WebFetch(domain:whois.domaintools.com)",
"WebFetch(domain:who.is)",
"WebFetch(domain:lookup.icann.org)",
"Bash(whois:*)",
"WebFetch(domain:whois.nic.tv)",
"Bash(GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10\" git push:*)",
"Bash(GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -p 2222\" git push:*)",
"Bash(git remote add:*)",
"Bash(git branch:*)"
]
}
}

View File

@ -438,6 +438,132 @@ method_token_rpc() {
fi
}
#------------------------------------------------------------------------------
# Avatar-Tap Session Integration
#------------------------------------------------------------------------------
# Get captured sessions from avatar-tap
method_get_tap_sessions() {
local domain
read -r input
json_load "$input" 2>/dev/null
json_get_var domain domain
$RTTYCTL json-tap-sessions 2>/dev/null || echo '[]'
}
# Get single session details
method_get_tap_session() {
local session_id
read -r input
json_load "$input"
json_get_var session_id session_id
[ -z "$session_id" ] && {
echo '{"error":"Missing session_id"}'
return
}
$RTTYCTL json-tap-session "$session_id" 2>/dev/null || echo '{"error":"Session not found"}'
}
# Replay a session to a remote node
method_replay_to_node() {
local session_id target_node
read -r input
json_load "$input"
json_get_var session_id session_id
json_get_var target_node target_node
[ -z "$session_id" ] || [ -z "$target_node" ] && {
echo '{"success":false,"error":"Missing session_id or target_node"}'
return
}
local result=$($RTTYCTL tap-replay "$session_id" "$target_node" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Session replayed successfully"
# Extract response preview
local preview=$(echo "$result" | tail -20 | head -10)
json_add_string "preview" "$preview"
else
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
}
# Export session
method_export_session() {
local session_id
read -r input
json_load "$input"
json_get_var session_id session_id
[ -z "$session_id" ] && {
echo '{"success":false,"error":"Missing session_id"}'
return
}
local export_file="/tmp/export_session_$session_id.json"
$RTTYCTL tap-export "$session_id" "$export_file" 2>/dev/null
if [ -f "$export_file" ]; then
json_init
json_add_boolean "success" 1
json_add_string "file" "$export_file"
json_add_int "size" "$(wc -c < "$export_file")"
# Include the actual content for download
local content=$(cat "$export_file" | base64 -w 0)
json_add_string "content" "$content"
json_dump
rm -f "$export_file"
else
echo '{"success":false,"error":"Export failed"}'
fi
}
# Import session
method_import_session() {
local content filename
read -r input
json_load "$input"
json_get_var content content
json_get_var filename filename
[ -z "$content" ] && {
echo '{"success":false,"error":"Missing content"}'
return
}
local import_file="/tmp/import_session_$$.json"
echo "$content" | base64 -d > "$import_file" 2>/dev/null
if [ ! -s "$import_file" ]; then
rm -f "$import_file"
echo '{"success":false,"error":"Invalid content"}'
return
fi
local result=$($RTTYCTL tap-import "$import_file" 2>&1)
local rc=$?
rm -f "$import_file"
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "$result"
else
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
}
#------------------------------------------------------------------------------
# Main dispatcher
#------------------------------------------------------------------------------

View File

@ -30,6 +30,13 @@ var callGetHealth = rpc.declare({
expect: {}
});
var callGetServiceHealth = rpc.declare({
object: 'luci.system-hub',
method: 'get_service_health',
params: ['refresh'],
expect: {}
});
var callListServices = rpc.declare({
object: 'luci.system-hub',
method: 'list_services',
@ -257,6 +264,9 @@ return baseclass.extend({
getStatus: callStatus,
getSystemInfo: callGetSystemInfo,
getHealth: callGetHealth,
getServiceHealth: function(refresh) {
return callGetServiceHealth({ refresh: refresh ? 1 : 0 });
},
getComponents: callGetComponents,
getComponentsByCategory: callGetComponentsByCategory,
listServices: callListServices,

View File

@ -8,15 +8,21 @@
return view.extend({
services: [],
serviceHealth: null,
activeFilter: 'all',
searchQuery: '',
healthCheckRunning: false,
load: function() {
return API.listServices();
return Promise.all([
API.listServices(),
API.getServiceHealth(false).catch(function() { return null; })
]);
},
render: function(data) {
this.services = this.normalizeServices(data);
this.services = this.normalizeServices(data[0]);
this.serviceHealth = data[1];
var self = this;
poll.add(function() {
@ -32,6 +38,7 @@ return view.extend({
var content = [
this.renderHeader(),
this.renderHealthPanel(),
this.renderControls(),
E('div', { 'class': 'sh-services-grid', 'id': 'sh-services-grid' },
this.getFilteredServices().map(this.renderServiceCard, this))
@ -40,6 +47,141 @@ return view.extend({
return KissTheme.wrap(content, 'admin/secubox/system/system-hub/services');
},
renderHealthPanel: function() {
var self = this;
var health = this.serviceHealth;
var downServices = [];
var upCount = 0;
var downCount = 0;
if (health && health.services) {
health.services.forEach(function(svc) {
if (svc.s === 'down') {
downServices.push(svc);
downCount++;
} else {
upCount++;
}
});
}
var totalRoutes = upCount + downCount;
var healthPercent = totalRoutes > 0 ? Math.round((upCount / totalRoutes) * 100) : 0;
return E('div', { 'class': 'sh-health-panel', 'id': 'sh-health-panel' }, [
E('div', { 'class': 'sh-health-header' }, [
E('div', { 'class': 'sh-health-title' }, [
E('span', { 'class': 'sh-health-icon' }, '🔍'),
E('span', {}, _('HAProxy Routes Health'))
]),
E('div', { 'class': 'sh-health-actions' }, [
E('button', {
'class': 'sh-btn sh-btn-action',
'id': 'sh-health-refresh-btn',
'type': 'button',
'click': ui.createHandlerFn(this, 'refreshHealthCheck')
}, [
E('span', { 'class': 'sh-btn-icon' }, '↻'),
_('Refresh')
])
])
]),
E('div', { 'class': 'sh-health-stats' }, [
E('div', { 'class': 'sh-health-stat success' }, [
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-up' }, upCount.toString()),
E('span', { 'class': 'sh-health-stat-label' }, _('Up'))
]),
E('div', { 'class': 'sh-health-stat danger' }, [
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-down' }, downCount.toString()),
E('span', { 'class': 'sh-health-stat-label' }, _('Down'))
]),
E('div', { 'class': 'sh-health-stat' }, [
E('span', { 'class': 'sh-health-stat-value' }, totalRoutes.toString()),
E('span', { 'class': 'sh-health-stat-label' }, _('Total'))
]),
E('div', { 'class': 'sh-health-stat ' + (healthPercent >= 90 ? 'success' : healthPercent >= 70 ? 'warning' : 'danger') }, [
E('span', { 'class': 'sh-health-stat-value' }, healthPercent + '%'),
E('span', { 'class': 'sh-health-stat-label' }, _('Health'))
])
]),
downCount > 0 ? E('div', { 'class': 'sh-health-down-list', 'id': 'sh-health-down-list' }, [
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
E('div', { 'class': 'sh-health-down-items' },
downServices.slice(0, 10).map(function(svc) {
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
})
),
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
]) : E('div', { 'class': 'sh-health-all-ok' }, [
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
_('All routes are healthy')
])
]);
},
refreshHealthCheck: function() {
var self = this;
var btn = document.getElementById('sh-health-refresh-btn');
if (btn) btn.classList.add('spinning');
return API.getServiceHealth(true).then(function(health) {
self.serviceHealth = health;
self.updateHealthPanel();
if (btn) btn.classList.remove('spinning');
}).catch(function(err) {
if (btn) btn.classList.remove('spinning');
ui.addNotification(null, E('p', {}, _('Health check failed: ') + (err.message || err)), 'error');
});
},
updateHealthPanel: function() {
var panel = document.getElementById('sh-health-panel');
if (!panel) return;
var health = this.serviceHealth;
var upCount = 0;
var downCount = 0;
var downServices = [];
if (health && health.services) {
health.services.forEach(function(svc) {
if (svc.s === 'down') {
downServices.push(svc);
downCount++;
} else {
upCount++;
}
});
}
var upEl = document.getElementById('sh-health-up');
var downEl = document.getElementById('sh-health-down');
if (upEl) upEl.textContent = upCount.toString();
if (downEl) downEl.textContent = downCount.toString();
var downList = document.getElementById('sh-health-down-list');
if (downList) {
if (downCount > 0) {
dom.content(downList, [
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
E('div', { 'class': 'sh-health-down-items' },
downServices.slice(0, 10).map(function(svc) {
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
})
),
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
]);
} else {
dom.content(downList, [
E('div', { 'class': 'sh-health-all-ok' }, [
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
_('All routes are healthy')
])
]);
}
}
},
injectStyles: function() {
if (document.querySelector('#sh-services-kiss-styles')) return;
var style = document.createElement('style');
@ -85,6 +227,29 @@ return view.extend({
.sh-empty-icon { font-size: 48px; margin-bottom: 12px; }
.sh-service-detail { margin-bottom: 16px; }
.sh-service-detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--kiss-line); }
/* Health Panel Styles */
.sh-health-panel { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 12px; padding: 16px; margin-bottom: 20px; }
.sh-health-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.sh-health-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; }
.sh-health-icon { font-size: 20px; }
.sh-health-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.sh-health-stat { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px 16px; min-width: 80px; text-align: center; }
.sh-health-stat.success { border-left: 3px solid var(--kiss-green); }
.sh-health-stat.danger { border-left: 3px solid var(--kiss-red); }
.sh-health-stat.warning { border-left: 3px solid #f59e0b; }
.sh-health-stat-value { display: block; font-size: 24px; font-weight: 700; color: var(--kiss-text); }
.sh-health-stat-label { display: block; font-size: 11px; color: var(--kiss-muted); text-transform: uppercase; margin-top: 4px; }
.sh-health-down-list { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--kiss-line); }
.sh-health-down-title { font-size: 12px; color: var(--kiss-muted); margin-bottom: 8px; }
.sh-health-down-items { display: flex; flex-wrap: wrap; gap: 6px; }
.sh-health-down-item { background: rgba(255,23,68,0.1); color: var(--kiss-red); padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: help; }
.sh-health-down-more { font-size: 11px; color: var(--kiss-muted); padding: 4px 8px; }
.sh-health-all-ok { display: flex; align-items: center; gap: 8px; color: var(--kiss-green); font-size: 13px; padding: 8px 0; }
.sh-health-ok-icon { font-size: 16px; }
.sh-btn-icon { margin-right: 4px; }
.sh-btn.spinning .sh-btn-icon { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
`;
document.head.appendChild(style);
},

View File

@ -2253,6 +2253,38 @@ get_components_by_category() {
fi
}
# Service Health Check - checks all HAProxy routes status
get_service_health() {
local cache_file="/tmp/service-health.json"
# Read refresh param
local input
read -r input
json_load "$input" 2>/dev/null
local refresh
json_get_var refresh refresh
json_cleanup
[ -z "$refresh" ] && refresh="0"
# Refresh if requested or cache is old (>5min) or missing
if [ "$refresh" = "1" ] || [ ! -f "$cache_file" ]; then
/usr/sbin/service-health-check json > "$cache_file" 2>/dev/null
elif [ -f "$cache_file" ]; then
local age=$(( $(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0) ))
if [ "$age" -gt 300 ]; then
/usr/sbin/service-health-check json > "$cache_file" 2>/dev/null
fi
fi
if [ -f "$cache_file" ]; then
cat "$cache_file"
else
json_init
json_add_string "error" "No health data available"
json_dump
fi
}
# Main dispatcher
case "$1" in
list)
@ -2261,6 +2293,7 @@ case "$1" in
"status": {},
"get_system_info": {},
"get_health": {},
"get_service_health": { "refresh": 0 },
"get_components": {},
"get_components_by_category": { "category": "string" },
"list_services": {},
@ -2338,6 +2371,7 @@ EOF
status) status ;;
get_system_info) get_system_info ;;
get_health) get_health ;;
get_service_health) get_service_health ;;
get_components) get_components ;;
get_components_by_category) get_components_by_category ;;
list_services) list_services ;;

View File

@ -7,6 +7,7 @@
"status",
"get_system_info",
"get_health",
"get_service_health",
"get_components",
"get_components_by_category",
"list_services",

View File

@ -640,7 +640,7 @@ cmd_disconnect() {
}
#------------------------------------------------------------------------------
# Session Management (Placeholder)
# Session Management (RTTY Remote Sessions)
#------------------------------------------------------------------------------
cmd_sessions() {
@ -658,22 +658,274 @@ cmd_sessions() {
echo "Session database not initialized"
}
cmd_replay() {
#------------------------------------------------------------------------------
# Avatar-Tap Integration - Session Capture & Replay
#------------------------------------------------------------------------------
AVATAR_TAP_DB="/srv/avatar-tap/sessions.db"
cmd_tap_sessions() {
local domain_filter="$1"
echo "Avatar-Tap Captured Sessions"
echo "============================"
echo ""
if [ ! -f "$AVATAR_TAP_DB" ]; then
echo "No captured sessions (avatar-tap database not found)"
return 1
fi
if [ -n "$domain_filter" ]; then
echo "Filter: $domain_filter"
echo ""
sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, datetime(captured_at,'unixepoch') as captured, label FROM sessions WHERE domain LIKE '%$domain_filter%' ORDER BY captured_at DESC LIMIT 30" 2>/dev/null | \
while IFS='|' read id domain method path captured label; do
printf " [%3d] %-25s %-6s %-30s %s\n" "$id" "$domain" "$method" "${path:0:30}" "${label:-}"
done
else
sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, datetime(captured_at,'unixepoch') as captured, label FROM sessions ORDER BY captured_at DESC LIMIT 30" 2>/dev/null | \
while IFS='|' read id domain method path captured label; do
printf " [%3d] %-25s %-6s %-30s %s\n" "$id" "$domain" "$method" "${path:0:30}" "${label:-}"
done
fi
echo ""
local total=$(sqlite3 "$AVATAR_TAP_DB" "SELECT COUNT(*) FROM sessions" 2>/dev/null)
echo "Total sessions: ${total:-0}"
}
cmd_tap_show() {
local session_id="$1"
[ -z "$session_id" ] && die "Usage: rttyctl tap-show <session_id>"
if [ ! -f "$AVATAR_TAP_DB" ]; then
die "Avatar-tap database not found"
fi
echo "Session Details: #$session_id"
echo "=========================="
local session=$(sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, captured_at, last_used, use_count, label, avatar_id FROM sessions WHERE id=$session_id" 2>/dev/null)
if [ -z "$session" ]; then
die "Session not found: $session_id"
fi
echo "$session" | while IFS='|' read id domain method path captured last_used use_count label avatar_id; do
echo " ID: $id"
echo " Domain: $domain"
echo " Method: $method"
echo " Path: $path"
echo " Captured: $(date -d "@$captured" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$captured" "+%Y-%m-%d %H:%M:%S")"
echo " Used: $use_count times"
echo " Label: ${label:-(none)}"
echo " Avatar: ${avatar_id:-(not linked)}"
done
# Show headers
echo ""
echo "Request Headers:"
sqlite3 "$AVATAR_TAP_DB" "SELECT name, value FROM session_headers WHERE session_id=$session_id AND type='request'" 2>/dev/null | \
while IFS='|' read name value; do
# Mask sensitive values
case "$name" in
Cookie|Authorization|X-Auth-Token)
printf " %-20s %s...\n" "$name:" "${value:0:30}"
;;
*)
printf " %-20s %s\n" "$name:" "$value"
;;
esac
done
}
cmd_tap_replay() {
local session_id="$1"
local target_node="$2"
[ -z "$session_id" ] || [ -z "$target_node" ] && die "Usage: rttyctl replay <session_id> <target_node>"
[ -z "$session_id" ] || [ -z "$target_node" ] && {
echo "Usage: rttyctl tap-replay <session_id> <target_node>"
echo ""
echo "Replay a captured avatar-tap session to a remote mesh node."
echo ""
echo "Examples:"
echo " rttyctl tap-replay 5 10.100.0.2"
echo " rttyctl tap-replay 12 sb-office"
exit 1
}
echo "Replaying session $session_id to $target_node..."
echo "Note: Session replay requires avatar-tap integration (coming soon)"
if [ ! -f "$AVATAR_TAP_DB" ]; then
die "Avatar-tap database not found"
fi
# Get session info
local session=$(sqlite3 "$AVATAR_TAP_DB" "SELECT domain, method, path FROM sessions WHERE id=$session_id" 2>/dev/null)
[ -z "$session" ] && die "Session not found: $session_id"
local domain=$(echo "$session" | cut -d'|' -f1)
local method=$(echo "$session" | cut -d'|' -f2)
local path=$(echo "$session" | cut -d'|' -f3)
echo "Replaying Session #$session_id to $target_node"
echo "=============================================="
echo " Original: $method $domain$path"
echo ""
# Get target node address
local target_addr=$(get_node_address "$target_node")
[ -z "$target_addr" ] && die "Cannot resolve target node: $target_node"
echo " Target: $target_addr"
echo ""
# Export session to JSON
local export_file="/tmp/replay_session_$$.json"
avatar-tapctl export "$session_id" "$export_file" 2>/dev/null || die "Failed to export session"
# Read session data
local headers=$(jsonfilter -i "$export_file" -e '@.headers' 2>/dev/null)
local cookies=$(jsonfilter -i "$export_file" -e '@.cookies' 2>/dev/null)
local body=$(jsonfilter -i "$export_file" -e '@.body' 2>/dev/null)
# Build the replay target URL
# Replace original domain with target node domain if same path is available
local target_url="http://${target_addr}${path}"
echo " Replay URL: $target_url"
echo ""
# Execute replay via curl
local curl_opts="-s -m 30"
# Add headers
if [ -n "$headers" ]; then
echo "$headers" | jsonfilter -e '@[*]' 2>/dev/null | while read header; do
local name=$(echo "$header" | jsonfilter -e '@.name')
local value=$(echo "$header" | jsonfilter -e '@.value')
# Skip host header as we're changing target
[ "$name" = "Host" ] && continue
curl_opts="$curl_opts -H \"$name: $value\""
done
fi
# Add cookies
if [ -n "$cookies" ]; then
local cookie_str=$(echo "$cookies" | jsonfilter -e '@[*]' 2>/dev/null | while read c; do
local n=$(echo "$c" | jsonfilter -e '@.name')
local v=$(echo "$c" | jsonfilter -e '@.value')
echo -n "$n=$v; "
done)
[ -n "$cookie_str" ] && curl_opts="$curl_opts -H \"Cookie: $cookie_str\""
fi
echo "Executing replay..."
case "$method" in
GET)
local result=$(curl $curl_opts "$target_url" 2>&1)
;;
POST)
local result=$(curl $curl_opts -X POST -d "$body" "$target_url" 2>&1)
;;
PUT)
local result=$(curl $curl_opts -X PUT -d "$body" "$target_url" 2>&1)
;;
DELETE)
local result=$(curl $curl_opts -X DELETE "$target_url" 2>&1)
;;
*)
local result=$(curl $curl_opts -X "$method" "$target_url" 2>&1)
;;
esac
local rc=$?
rm -f "$export_file"
if [ $rc -eq 0 ]; then
echo "Replay successful!"
echo ""
echo "Response:"
echo "$result" | head -50
else
echo "Replay failed (curl error: $rc)"
echo "$result"
return 1
fi
# Log replay
log "info" "Replayed session #$session_id to $target_node"
}
cmd_export() {
cmd_tap_export() {
local session_id="$1"
[ -z "$session_id" ] && die "Usage: rttyctl export <session_id>"
local output_file="$2"
echo "Exporting session $session_id..."
echo "Note: Session export requires implementation"
[ -z "$session_id" ] && die "Usage: rttyctl tap-export <session_id> [output_file]"
if [ -z "$output_file" ]; then
output_file="/tmp/session_${session_id}.json"
fi
if [ ! -f "$AVATAR_TAP_DB" ]; then
die "Avatar-tap database not found"
fi
# Use avatar-tapctl export
avatar-tapctl export "$session_id" "$output_file" 2>/dev/null
if [ -f "$output_file" ]; then
echo "Session exported to: $output_file"
echo "Size: $(wc -c < "$output_file") bytes"
else
die "Export failed"
fi
}
cmd_tap_import() {
local import_file="$1"
[ -z "$import_file" ] && die "Usage: rttyctl tap-import <file.json>"
[ ! -f "$import_file" ] && die "Import file not found: $import_file"
echo "Importing session from: $import_file"
# Parse and insert into avatar-tap database
local domain=$(jsonfilter -i "$import_file" -e '@.domain' 2>/dev/null)
local method=$(jsonfilter -i "$import_file" -e '@.method' 2>/dev/null)
local path=$(jsonfilter -i "$import_file" -e '@.path' 2>/dev/null)
local label=$(jsonfilter -i "$import_file" -e '@.label' 2>/dev/null)
[ -z "$domain" ] || [ -z "$method" ] || [ -z "$path" ] && die "Invalid session format"
# Insert into database
local now=$(date +%s)
sqlite3 "$AVATAR_TAP_DB" "INSERT INTO sessions (domain, method, path, captured_at, label, use_count) VALUES ('$domain', '$method', '$path', $now, '$label', 0)" 2>/dev/null
local new_id=$(sqlite3 "$AVATAR_TAP_DB" "SELECT last_insert_rowid()" 2>/dev/null)
echo "Session imported with ID: $new_id"
}
cmd_tap_json_sessions() {
if [ ! -f "$AVATAR_TAP_DB" ]; then
echo '{"sessions":[]}'
return
fi
sqlite3 -json "$AVATAR_TAP_DB" \
"SELECT id, domain, path, method, captured_at, last_used, use_count, label FROM sessions ORDER BY captured_at DESC LIMIT 50" 2>/dev/null || echo '{"sessions":[]}'
}
cmd_tap_json_session() {
local session_id="$1"
if [ ! -f "$AVATAR_TAP_DB" ] || [ -z "$session_id" ]; then
echo '{"error":"Session not found"}'
return
fi
sqlite3 -json "$AVATAR_TAP_DB" \
"SELECT id, domain, path, method, captured_at, last_used, use_count, label FROM sessions WHERE id=$session_id" 2>/dev/null || echo '{"error":"Session not found"}'
}
#------------------------------------------------------------------------------
@ -788,10 +1040,12 @@ RPCD Proxy:
rpc-list <node> List available RPCD objects
rpc-batch <node> <file.json> Execute batch RPCD calls
Session Management:
sessions [node_id] List active/recent sessions
replay <session_id> <node> Replay captured session to node
export <session_id> Export session as JSON
Avatar-Tap Session Replay:
tap-sessions [domain] List captured sessions (filter by domain)
tap-show <id> Show session details (headers, cookies)
tap-replay <id> <node> Replay captured session to remote node
tap-export <id> [file] Export session as JSON
tap-import <file> Import session from JSON file
Server Control:
server start Start local RTTY server
@ -812,12 +1066,14 @@ Shared Access Tokens:
JSON Output (for RPCD):
json-status Status as JSON
json-nodes Nodes as JSON
json-tap-sessions Avatar-tap sessions as JSON
Examples:
rttyctl nodes
rttyctl rpc 10.100.0.2 luci.system-hub status
rttyctl rpc sb-01 luci.haproxy vhost_list
rttyctl rpc-list 192.168.255.2
rttyctl tap-sessions photos.gk2
rttyctl tap-replay 5 10.100.0.3
rttyctl tap-export 12 /tmp/session.json
Version: $VERSION
EOF
@ -855,11 +1111,21 @@ case "$1" in
sessions)
cmd_sessions "$2"
;;
replay)
cmd_replay "$2" "$3"
# Avatar-Tap Session Replay
tap-sessions)
cmd_tap_sessions "$2"
;;
export)
cmd_export "$2"
tap-show)
cmd_tap_show "$2"
;;
tap-replay)
cmd_tap_replay "$2" "$3"
;;
tap-export)
cmd_tap_export "$2" "$3"
;;
tap-import)
cmd_tap_import "$2"
;;
server)
cmd_server "$2"
@ -882,6 +1148,12 @@ case "$1" in
json-tokens)
cmd_token_json_list
;;
json-tap-sessions)
cmd_tap_json_sessions
;;
json-tap-session)
cmd_tap_json_session "$2"
;;
token)
case "$2" in
generate)