resolve ip

This commit is contained in:
Mateusz Gruszczyński
2026-05-20 21:59:25 +02:00
parent f4d8611240
commit af20e55539
11 changed files with 364 additions and 159 deletions

View File

@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS user_preferences (
footer_items_json TEXT, footer_items_json TEXT,
title_speed_enabled INTEGER DEFAULT 0, title_speed_enabled INTEGER DEFAULT 0,
tracker_favicons_enabled INTEGER DEFAULT 0, tracker_favicons_enabled INTEGER DEFAULT 0,
reverse_dns_enabled INTEGER DEFAULT 0,
automation_toasts_enabled INTEGER DEFAULT 1, automation_toasts_enabled INTEGER DEFAULT 1,
smart_queue_toasts_enabled INTEGER DEFAULT 1, smart_queue_toasts_enabled INTEGER DEFAULT 1,
disk_monitor_paths_json TEXT, disk_monitor_paths_json TEXT,
@@ -510,6 +511,7 @@ MIGRATIONS = [
"ALTER TABLE user_preferences ADD COLUMN footer_items_json TEXT", "ALTER TABLE user_preferences ADD COLUMN footer_items_json TEXT",
"ALTER TABLE user_preferences ADD COLUMN title_speed_enabled INTEGER DEFAULT 0", "ALTER TABLE user_preferences ADD COLUMN title_speed_enabled INTEGER DEFAULT 0",
"ALTER TABLE user_preferences ADD COLUMN tracker_favicons_enabled INTEGER DEFAULT 0", "ALTER TABLE user_preferences ADD COLUMN tracker_favicons_enabled INTEGER DEFAULT 0",
"ALTER TABLE user_preferences ADD COLUMN reverse_dns_enabled INTEGER DEFAULT 0",
"ALTER TABLE user_preferences ADD COLUMN interface_scale INTEGER DEFAULT 100", "ALTER TABLE user_preferences ADD COLUMN interface_scale INTEGER DEFAULT 100",
"ALTER TABLE user_preferences ADD COLUMN detail_panel_height INTEGER DEFAULT 255", "ALTER TABLE user_preferences ADD COLUMN detail_panel_height INTEGER DEFAULT 255",
"ALTER TABLE user_preferences ADD COLUMN torrent_sort_json TEXT", "ALTER TABLE user_preferences ADD COLUMN torrent_sort_json TEXT",

View File

@@ -139,22 +139,22 @@
"type": "object" "type": "object"
}, },
"AutoBackupSettings": { "AutoBackupSettings": {
"type": "object", "additionalProperties": true,
"properties": { "properties": {
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"interval_hours": { "interval_hours": {
"type": "integer", "minimum": 1,
"minimum": 1 "type": "integer"
},
"retention_days": {
"type": "integer",
"minimum": 1
}, },
"last_run_at": { "last_run_at": {
"type": "string", "nullable": true,
"nullable": true "type": "string"
},
"retention_days": {
"minimum": 1,
"type": "integer"
} }
}, },
"required": [ "required": [
@@ -162,28 +162,28 @@
"interval_hours", "interval_hours",
"retention_days" "retention_days"
], ],
"additionalProperties": true "type": "object"
}, },
"AutoBackupSettingsRequest": { "AutoBackupSettingsRequest": {
"type": "object", "additionalProperties": true,
"properties": { "properties": {
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"interval_hours": { "interval_hours": {
"type": "integer", "minimum": 1,
"minimum": 1 "type": "integer"
},
"retention_days": {
"type": "integer",
"minimum": 1
}, },
"last_run_at": { "last_run_at": {
"type": "string", "nullable": true,
"nullable": true "type": "string"
},
"retention_days": {
"minimum": 1,
"type": "integer"
} }
}, },
"additionalProperties": true "type": "object"
}, },
"AutoBackupSettingsResponse": { "AutoBackupSettingsResponse": {
"allOf": [ "allOf": [
@@ -191,7 +191,6 @@
"$ref": "#/components/schemas/ApiOk" "$ref": "#/components/schemas/ApiOk"
}, },
{ {
"type": "object",
"properties": { "properties": {
"settings": { "settings": {
"$ref": "#/components/schemas/AutoBackupSettings" "$ref": "#/components/schemas/AutoBackupSettings"
@@ -199,7 +198,8 @@
}, },
"required": [ "required": [
"settings" "settings"
] ],
"type": "object"
} }
] ]
}, },
@@ -353,31 +353,31 @@
] ]
}, },
"BackupPreview": { "BackupPreview": {
"type": "object", "additionalProperties": true,
"properties": { "properties": {
"version": {
"type": "integer",
"nullable": true
},
"created_at": {
"type": "string",
"nullable": true
},
"automatic": { "automatic": {
"type": "boolean" "type": "boolean"
}, },
"created_at": {
"nullable": true,
"type": "string"
},
"tables": { "tables": {
"type": "array",
"items": { "items": {
"$ref": "#/components/schemas/BackupPreviewTable" "$ref": "#/components/schemas/BackupPreviewTable"
} },
"type": "array"
},
"version": {
"nullable": true,
"type": "integer"
} }
}, },
"required": [ "required": [
"automatic", "automatic",
"tables" "tables"
], ],
"additionalProperties": true "type": "object"
}, },
"BackupPreviewResponse": { "BackupPreviewResponse": {
"allOf": [ "allOf": [
@@ -385,7 +385,6 @@
"$ref": "#/components/schemas/ApiOk" "$ref": "#/components/schemas/ApiOk"
}, },
{ {
"type": "object",
"properties": { "properties": {
"preview": { "preview": {
"$ref": "#/components/schemas/BackupPreview" "$ref": "#/components/schemas/BackupPreview"
@@ -393,31 +392,31 @@
}, },
"required": [ "required": [
"preview" "preview"
] ],
"type": "object"
} }
] ]
}, },
"BackupPreviewTable": { "BackupPreviewTable": {
"type": "object",
"properties": { "properties": {
"columns": {
"items": {
"type": "string"
},
"type": "array"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"rows": { "rows": {
"type": "integer" "type": "integer"
}, },
"columns": {
"type": "array",
"items": {
"type": "string"
}
},
"sample": { "sample": {
"type": "array",
"items": { "items": {
"type": "object", "additionalProperties": true,
"additionalProperties": true "type": "object"
} },
"type": "array"
} }
}, },
"required": [ "required": [
@@ -425,7 +424,8 @@
"rows", "rows",
"columns", "columns",
"sample" "sample"
] ],
"type": "object"
}, },
"CleanupCacheSummary": { "CleanupCacheSummary": {
"properties": { "properties": {
@@ -1129,11 +1129,22 @@
"font_family": { "font_family": {
"enum": [ "enum": [
"default", "default",
"adwaita-mono",
"inter",
"system-ui", "system-ui",
"figtree",
"inter",
"geist",
"manrope",
"dm-sans",
"source-sans-3", "source-sans-3",
"jetbrains-mono" "open-sans",
"roboto",
"lato",
"nunito-sans",
"poppins",
"montserrat",
"ibm-plex-sans",
"jetbrains-mono",
"adwaita-mono"
], ],
"type": "string" "type": "string"
}, },
@@ -1150,6 +1161,10 @@
"port_check_enabled": { "port_check_enabled": {
"type": "boolean" "type": "boolean"
}, },
"reverse_dns_enabled": {
"description": "Enables cached reverse DNS lookups for the Peers tab.",
"type": "boolean"
},
"table_columns_json": { "table_columns_json": {
"description": "JSON-encoded TableColumnsPreference stored in user preferences.", "description": "JSON-encoded TableColumnsPreference stored in user preferences.",
"type": "string" "type": "string"
@@ -1542,23 +1557,23 @@
"type": "object" "type": "object"
}, },
"TorrentChunkActionRequest": { "TorrentChunkActionRequest": {
"type": "object", "additionalProperties": true,
"properties": { "properties": {
"first_chunk": { "first_chunk": {
"type": "integer", "minimum": 0,
"minimum": 0 "type": "integer"
}, },
"last_chunk": { "last_chunk": {
"type": "integer", "minimum": 0,
"minimum": 0 "type": "integer"
}, },
"priority": { "priority": {
"type": "integer", "maximum": 3,
"minimum": 0, "minimum": 0,
"maximum": 3 "type": "integer"
} }
}, },
"additionalProperties": true "type": "object"
}, },
"TorrentChunkActionResponse": { "TorrentChunkActionResponse": {
"allOf": [ "allOf": [
@@ -1566,59 +1581,58 @@
"$ref": "#/components/schemas/ApiOk" "$ref": "#/components/schemas/ApiOk"
}, },
{ {
"type": "object",
"properties": { "properties": {
"result": {
"type": "object",
"additionalProperties": true
},
"message": { "message": {
"type": "string" "type": "string"
},
"result": {
"additionalProperties": true,
"type": "object"
} }
}, },
"required": [ "required": [
"result", "result",
"message" "message"
] ],
"type": "object"
} }
] ]
}, },
"TorrentChunkCell": { "TorrentChunkCell": {
"type": "object",
"properties": { "properties": {
"index": { "completed": {
"type": "integer" "type": "integer"
}, },
"first_chunk": { "first_chunk": {
"type": "integer" "type": "integer"
}, },
"grouped": {
"type": "boolean"
},
"index": {
"type": "integer"
},
"last_chunk": { "last_chunk": {
"type": "integer" "type": "integer"
}, },
"completed": {
"type": "integer"
},
"total": {
"type": "integer"
},
"percent": { "percent": {
"type": "number", "format": "float",
"format": "float" "type": "number"
}, },
"seen": { "seen": {
"type": "boolean" "type": "boolean"
}, },
"status": { "status": {
"type": "string",
"enum": [ "enum": [
"complete", "complete",
"partial", "partial",
"missing", "missing",
"seen" "seen"
] ],
"type": "string"
}, },
"grouped": { "total": {
"type": "boolean" "type": "integer"
}, },
"unit_count": { "unit_count": {
"type": "integer" "type": "integer"
@@ -1635,13 +1649,19 @@
"status", "status",
"grouped", "grouped",
"unit_count" "unit_count"
] ],
"type": "object"
}, },
"TorrentChunks": { "TorrentChunks": {
"type": "object",
"properties": { "properties": {
"hash": { "bitfield_units": {
"type": "string" "type": "integer"
},
"cells": {
"items": {
"$ref": "#/components/schemas/TorrentChunkCell"
},
"type": "array"
}, },
"chunk_size": { "chunk_size": {
"type": "integer" "type": "integer"
@@ -1649,50 +1669,44 @@
"chunk_size_h": { "chunk_size_h": {
"type": "string" "type": "string"
}, },
"size_chunks": { "chunks_hashed": {
"type": "integer" "type": "integer"
}, },
"completed_chunks": { "completed_chunks": {
"type": "integer" "type": "integer"
}, },
"chunks_hashed": {
"type": "integer"
},
"bitfield_units": {
"type": "integer"
},
"visual_cells": {
"type": "integer"
},
"grouped": { "grouped": {
"type": "boolean" "type": "boolean"
}, },
"cells": { "hash": {
"type": "array", "type": "string"
"items": { },
"$ref": "#/components/schemas/TorrentChunkCell" "size_chunks": {
} "type": "integer"
}, },
"summary": { "summary": {
"type": "object", "additionalProperties": {
"type": "integer"
},
"properties": { "properties": {
"complete": { "complete": {
"type": "integer" "type": "integer"
}, },
"partial": { "missing": {
"type": "integer" "type": "integer"
}, },
"missing": { "partial": {
"type": "integer" "type": "integer"
}, },
"seen": { "seen": {
"type": "integer" "type": "integer"
} }
}, },
"additionalProperties": { "type": "object"
},
"visual_cells": {
"type": "integer" "type": "integer"
} }
}
}, },
"required": [ "required": [
"hash", "hash",
@@ -1706,7 +1720,8 @@
"grouped", "grouped",
"cells", "cells",
"summary" "summary"
] ],
"type": "object"
}, },
"TorrentChunksResponse": { "TorrentChunksResponse": {
"allOf": [ "allOf": [
@@ -1714,7 +1729,6 @@
"$ref": "#/components/schemas/ApiOk" "$ref": "#/components/schemas/ApiOk"
}, },
{ {
"type": "object",
"properties": { "properties": {
"chunks": { "chunks": {
"$ref": "#/components/schemas/TorrentChunks" "$ref": "#/components/schemas/TorrentChunks"
@@ -1722,7 +1736,8 @@
}, },
"required": [ "required": [
"chunks" "chunks"
] ],
"type": "object"
} }
] ]
}, },
@@ -1840,6 +1855,68 @@
} }
] ]
}, },
"TorrentPeer": {
"additionalProperties": true,
"properties": {
"banned": {
"type": "boolean"
},
"city": {
"type": "string"
},
"client": {
"type": "string"
},
"completed": {
"type": "integer"
},
"country": {
"type": "string"
},
"country_iso": {
"type": "string"
},
"down_rate": {
"type": "integer"
},
"down_rate_h": {
"type": "string"
},
"encrypted": {
"type": "boolean"
},
"host": {
"description": "Reverse DNS PTR hostname when reverse_dns_enabled is enabled and a hostname is cached or resolved.",
"type": "string"
},
"host_pending": {
"description": "True when a lightweight background PTR lookup was started but did not finish within the request budget.",
"type": "boolean"
},
"incoming": {
"type": "boolean"
},
"index": {
"type": "integer"
},
"ip": {
"type": "string"
},
"port": {
"type": "integer"
},
"snubbed": {
"type": "boolean"
},
"up_rate": {
"type": "integer"
},
"up_rate_h": {
"type": "string"
}
},
"type": "object"
},
"TorrentPeersResponse": { "TorrentPeersResponse": {
"allOf": [ "allOf": [
{ {
@@ -1849,8 +1926,7 @@
"properties": { "properties": {
"peers": { "peers": {
"items": { "items": {
"additionalProperties": true, "$ref": "#/components/schemas/TorrentPeer"
"type": "object"
}, },
"type": "array" "type": "array"
} }
@@ -3082,64 +3158,64 @@
}, },
"/api/backup/settings": { "/api/backup/settings": {
"get": { "get": {
"summary": "Get automatic backup settings",
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/AutoBackupSettingsResponse" "$ref": "#/components/schemas/AutoBackupSettingsResponse"
} }
} }
} },
"description": "OK"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Get automatic backup settings"
}, },
"post": { "post": {
"summary": "Save automatic backup settings",
"requestBody": { "requestBody": {
"required": false,
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/AutoBackupSettingsRequest" "$ref": "#/components/schemas/AutoBackupSettingsRequest"
} }
} }
} },
"required": false
}, },
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/AutoBackupSettingsResponse" "$ref": "#/components/schemas/AutoBackupSettingsResponse"
} }
} }
} },
"description": "OK"
}, },
"400": { "400": {
"description": "Error",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiError" "$ref": "#/components/schemas/ApiError"
} }
} }
} },
"description": "Error"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Save automatic backup settings"
} }
}, },
"/api/backup/{backup_id}": { "/api/backup/{backup_id}": {
@@ -3246,7 +3322,6 @@
}, },
"/api/backup/{backup_id}/preview": { "/api/backup/{backup_id}/preview": {
"get": { "get": {
"summary": "Preview backup",
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@@ -3259,31 +3334,32 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/BackupPreviewResponse" "$ref": "#/components/schemas/BackupPreviewResponse"
} }
} }
} },
"description": "OK"
}, },
"400": { "400": {
"description": "Error",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiError" "$ref": "#/components/schemas/ApiError"
} }
} }
} },
"description": "Error"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Preview backup"
} }
}, },
"/api/backup/{backup_id}/restore": { "/api/backup/{backup_id}/restore": {
@@ -5523,35 +5599,35 @@
}, },
"/api/rtorrent-config/reset": { "/api/rtorrent-config/reset": {
"post": { "post": {
"summary": "Reset startup rTorrent config overrides",
"description": "Clear pyTorrent-saved rTorrent config overrides and reload live rTorrent values.", "description": "Clear pyTorrent-saved rTorrent config overrides and reload live rTorrent values.",
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/RtorrentConfigResponse" "$ref": "#/components/schemas/RtorrentConfigResponse"
} }
} }
} },
"description": "OK"
}, },
"400": { "400": {
"description": "Error",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiError" "$ref": "#/components/schemas/ApiError"
} }
} }
} },
"description": "Error"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Reset startup rTorrent config overrides"
} }
}, },
"/api/smart-queue": { "/api/smart-queue": {
@@ -6291,7 +6367,6 @@
}, },
"/api/torrents/{torrent_hash}/chunks": { "/api/torrents/{torrent_hash}/chunks": {
"get": { "get": {
"summary": "Torrent chunks",
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@@ -6306,45 +6381,45 @@
"name": "max_cells", "name": "max_cells",
"required": false, "required": false,
"schema": { "schema": {
"type": "integer", "default": 2048,
"minimum": 64,
"maximum": 10000, "maximum": 10000,
"default": 2048 "minimum": 64,
"type": "integer"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/TorrentChunksResponse" "$ref": "#/components/schemas/TorrentChunksResponse"
} }
} }
} },
"description": "OK"
}, },
"400": { "400": {
"description": "Error",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiError" "$ref": "#/components/schemas/ApiError"
} }
} }
} },
"description": "Error"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Torrent chunks"
} }
}, },
"/api/torrents/{torrent_hash}/chunks/{action_name}": { "/api/torrents/{torrent_hash}/chunks/{action_name}": {
"post": { "post": {
"summary": "Run torrent chunk action",
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@@ -6359,51 +6434,52 @@
"name": "action_name", "name": "action_name",
"required": true, "required": true,
"schema": { "schema": {
"type": "string",
"enum": [ "enum": [
"recheck", "recheck",
"prioritize_files" "prioritize_files"
] ],
"type": "string"
} }
} }
], ],
"requestBody": { "requestBody": {
"required": false,
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/TorrentChunkActionRequest" "$ref": "#/components/schemas/TorrentChunkActionRequest"
} }
} }
} },
"required": false
}, },
"responses": { "responses": {
"200": { "200": {
"description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/TorrentChunkActionResponse" "$ref": "#/components/schemas/TorrentChunkActionResponse"
} }
} }
} },
"description": "OK"
}, },
"400": { "400": {
"description": "Error",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiError" "$ref": "#/components/schemas/ApiError"
} }
} }
} },
"description": "Error"
} }
}, },
"security": [ "security": [
{ {
"sessionCookie": [] "sessionCookie": []
} }
] ],
"summary": "Run torrent chunk action"
} }
}, },
"/api/torrents/{torrent_hash}/files": { "/api/torrents/{torrent_hash}/files": {
@@ -6685,7 +6761,7 @@
"sessionCookie": [] "sessionCookie": []
} }
], ],
"summary": "Torrent peers with GeoIP" "summary": "Torrent peers with GeoIP and optional reverse DNS"
} }
}, },
"/api/torrents/{torrent_hash}/torrent-file": { "/api/torrents/{torrent_hash}/torrent-file": {

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from ._shared import * from ._shared import *
from ..services import torrent_creator from ..services import torrent_creator
from ..services.reverse_dns import attach_reverse_dns
@bp.get("/torrents") @bp.get("/torrents")
def torrents(): def torrents():
@@ -386,6 +387,10 @@ def torrent_peers(torrent_hash: str):
peers = rtorrent.torrent_peers(profile, torrent_hash) peers = rtorrent.torrent_peers(profile, torrent_hash)
for peer in peers: for peer in peers:
peer.update(lookup_ip(peer.get("ip", ""))) peer.update(lookup_ip(peer.get("ip", "")))
prefs = preferences.get_preferences(profile_id=profile.get("id"))
if int(prefs.get("reverse_dns_enabled") or 0):
# Note: PTR hostnames are attached only when the user enables the lightweight cached resolver.
attach_reverse_dns(peers)
return ok({"peers": peers}) return ok({"peers": peers})

View File

@@ -347,6 +347,7 @@ def save_preferences(data: dict, user_id: int | None = None):
footer_items_json = data.get("footer_items_json") footer_items_json = data.get("footer_items_json")
title_speed_enabled = data.get("title_speed_enabled") title_speed_enabled = data.get("title_speed_enabled")
tracker_favicons_enabled = data.get("tracker_favicons_enabled") tracker_favicons_enabled = data.get("tracker_favicons_enabled")
reverse_dns_enabled = data.get("reverse_dns_enabled")
automation_toasts_enabled = data.get("automation_toasts_enabled") automation_toasts_enabled = data.get("automation_toasts_enabled")
smart_queue_toasts_enabled = data.get("smart_queue_toasts_enabled") smart_queue_toasts_enabled = data.get("smart_queue_toasts_enabled")
disk_monitor_paths_json = data.get("disk_monitor_paths_json") disk_monitor_paths_json = data.get("disk_monitor_paths_json")
@@ -387,6 +388,9 @@ def save_preferences(data: dict, user_id: int | None = None):
conn.execute("UPDATE user_preferences SET title_speed_enabled=?, updated_at=? WHERE user_id=?", (1 if title_speed_enabled else 0, now, user_id)) conn.execute("UPDATE user_preferences SET title_speed_enabled=?, updated_at=? WHERE user_id=?", (1 if title_speed_enabled else 0, now, user_id))
if tracker_favicons_enabled is not None: if tracker_favicons_enabled is not None:
conn.execute("UPDATE user_preferences SET tracker_favicons_enabled=?, updated_at=? WHERE user_id=?", (1 if tracker_favicons_enabled else 0, now, user_id)) conn.execute("UPDATE user_preferences SET tracker_favicons_enabled=?, updated_at=? WHERE user_id=?", (1 if tracker_favicons_enabled else 0, now, user_id))
if reverse_dns_enabled is not None:
# Note: Reverse DNS is optional because peer PTR lookups can add latency on busy swarms.
conn.execute("UPDATE user_preferences SET reverse_dns_enabled=?, updated_at=? WHERE user_id=?", (1 if reverse_dns_enabled else 0, now, user_id))
if automation_toasts_enabled is not None: if automation_toasts_enabled is not None:
# Note: Lets users silence automation-created toast noise without hiding job/history data. # Note: Lets users silence automation-created toast noise without hiding job/history data.
conn.execute("UPDATE user_preferences SET automation_toasts_enabled=?, updated_at=? WHERE user_id=?", (1 if automation_toasts_enabled else 0, now, user_id)) conn.execute("UPDATE user_preferences SET automation_toasts_enabled=?, updated_at=? WHERE user_id=?", (1 if automation_toasts_enabled else 0, now, user_id))

View File

@@ -0,0 +1,99 @@
from __future__ import annotations
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor, wait
from threading import Lock
from typing import Any
_CACHE_TTL_SECONDS = 24 * 60 * 60
_NEGATIVE_TTL_SECONDS = 60 * 60
_CACHE_LIMIT = 2048
_LOOKUP_LIMIT_PER_REQUEST = 24
_LOOKUP_TIMEOUT_SECONDS = 0.8
_cache: dict[str, tuple[str, float]] = {}
_pending: dict[str, Any] = {}
_lock = Lock()
_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="reverse-dns")
def _is_resolvable_ip(value: str) -> bool:
try:
ipaddress.ip_address(str(value or "").strip())
return True
except ValueError:
return False
def _lookup_host(ip: str) -> str:
try:
host = socket.gethostbyaddr(ip)[0]
return str(host or "").rstrip(".")
except Exception:
return ""
def _trim_cache(now: float) -> None:
expired = [ip for ip, (_, expires_at) in _cache.items() if expires_at <= now]
for ip in expired:
_cache.pop(ip, None)
if len(_cache) <= _CACHE_LIMIT:
return
for ip, _ in sorted(_cache.items(), key=lambda item: item[1][1])[: len(_cache) - _CACHE_LIMIT]:
_cache.pop(ip, None)
def _store(ip: str, host: str, now: float | None = None) -> None:
now = now or time.monotonic()
ttl = _CACHE_TTL_SECONDS if host else _NEGATIVE_TTL_SECONDS
_cache[ip] = (host, now + ttl)
def attach_reverse_dns(peers: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Attach cached or newly resolved PTR hostnames to peer rows with a small request budget."""
now = time.monotonic()
missing: list[str] = []
with _lock:
_trim_cache(now)
for peer in peers:
ip = str(peer.get("ip") or "").strip()
if not ip or not _is_resolvable_ip(ip):
peer["host"] = ""
continue
cached = _cache.get(ip)
if cached and cached[1] > now:
peer["host"] = cached[0]
continue
peer["host"] = ""
if ip not in _pending and ip not in missing and len(missing) < _LOOKUP_LIMIT_PER_REQUEST:
missing.append(ip)
for ip in missing:
_pending[ip] = _executor.submit(_lookup_host, ip)
futures = list(_pending.items())
if futures:
wait([future for _, future in futures], timeout=_LOOKUP_TIMEOUT_SECONDS)
done_hosts: dict[str, str] = {}
with _lock:
now = time.monotonic()
for ip, future in list(_pending.items()):
if not future.done():
continue
try:
host = str(future.result() or "")
except Exception:
host = ""
_store(ip, host, now)
done_hosts[ip] = host
_pending.pop(ip, None)
for peer in peers:
ip = str(peer.get("ip") or "").strip()
if ip in done_hosts:
peer["host"] = done_hosts[ip]
elif not peer.get("host") and ip in _pending:
peer["host_pending"] = True
return peers

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4358,3 +4358,22 @@ body,
width: 100%; width: 100%;
} }
} }
.peers-table {
table-layout: auto;
width: 100%;
}
.peers-table .peer-progress-wide {
min-width: 108px;
width: clamp(108px, 12vw, 126px);
}
.peer-host {
display: inline-block;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}

File diff suppressed because one or more lines are too long