refactor: Simplify variable names and improve readability in metrics and lifecycle modules
CI / lint-test-build (push) Failing after 11s
CI / lint-test-build (push) Failing after 11s
This commit is contained in:
+97
-88
@@ -86,7 +86,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]:
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 5
|
||||
""").fetchall()
|
||||
pnl_total_row = conn.execute("""
|
||||
rpnl = conn.execute("""
|
||||
SELECT COALESCE(SUM(COALESCE(realized_pnl, 0)), 0)
|
||||
FROM trades
|
||||
""").fetchone()
|
||||
@@ -131,7 +131,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]:
|
||||
"total_value": total_value,
|
||||
"open_trade_count": len(open_trade_rows),
|
||||
"open_trades": open_trade_rows,
|
||||
"realized_pnl_total": f"{float(pnl_total_row[0]):.2f} USD" if pnl_total_row else "—",
|
||||
"realized_pnl_total": f"{float(rpnl[0]):.2f} USD" if rpnl else "—",
|
||||
"opportunities": opportunity_rows,
|
||||
}
|
||||
|
||||
@@ -146,21 +146,23 @@ def _dashboard_charts(request: Request) -> dict[str, object]:
|
||||
LIMIT 10
|
||||
""").fetchall()
|
||||
|
||||
chart_rows = list(reversed(opportunity_rows))
|
||||
labels = [
|
||||
row[0].isoformat() if isinstance(row[0], datetime) else f"opportunity-{index + 1}"
|
||||
for index, row in enumerate(chart_rows)
|
||||
]
|
||||
net_pct_values = [float(row[2]) if row[2] is not None else 0.0 for row in chart_rows]
|
||||
est_profit_values = [float(row[3]) if row[3] is not None else 0.0 for row in chart_rows]
|
||||
cycles = [str(row[1]) for row in chart_rows]
|
||||
cr = list(reversed(opportunity_rows))
|
||||
labels = []
|
||||
for index, row in enumerate(cr):
|
||||
if isinstance(row[0], datetime):
|
||||
labels.append(row[0].isoformat())
|
||||
else:
|
||||
labels.append(f"opportunity-{index + 1}")
|
||||
np = [float(row[2]) if row[2] is not None else 0.0 for row in cr]
|
||||
ep = [float(row[3]) if row[3] is not None else 0.0 for row in cr]
|
||||
cycles = [str(row[1]) for row in cr]
|
||||
|
||||
return {
|
||||
"labels": labels,
|
||||
"net_pct_values": net_pct_values,
|
||||
"est_profit_values": est_profit_values,
|
||||
"net_pct_values": np,
|
||||
"est_profit_values": ep,
|
||||
"cycles": cycles,
|
||||
"has_chart_data": bool(chart_rows),
|
||||
"has_chart_data": bool(cr),
|
||||
"generated_at": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
@@ -186,13 +188,17 @@ def _record_audit(
|
||||
if repository is None:
|
||||
return
|
||||
correlation_id = request.headers.get("x-request-id")
|
||||
if payload is not None:
|
||||
ret_pl = {str(key): payload[key] for key in payload}
|
||||
else:
|
||||
ret_pl = None
|
||||
repository.insert(
|
||||
AuditRecord(
|
||||
occurred_at=datetime.now(UTC),
|
||||
actor=actor,
|
||||
event_type=event_type,
|
||||
decision=decision,
|
||||
payload=None if payload is None else {str(key): payload[key] for key in payload},
|
||||
payload=ret_pl,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
)
|
||||
@@ -254,8 +260,8 @@ def _alert_status_snapshot(request: Request) -> dict[str, object]:
|
||||
|
||||
|
||||
def _dashboard_controls(request: Request) -> dict[str, object]:
|
||||
controls = _dashboard_controls_state(request)
|
||||
settings = request.app.state.settings
|
||||
ctl = _dashboard_controls_state(request)
|
||||
rs = request.app.state.settings
|
||||
alert_status = _alert_status_snapshot(request)
|
||||
last_event = alert_status.get("last_event")
|
||||
last_event_title = "—"
|
||||
@@ -264,44 +270,47 @@ def _dashboard_controls(request: Request) -> dict[str, object]:
|
||||
if isinstance(title_value, str):
|
||||
last_event_title = title_value
|
||||
|
||||
configured_channels = alert_status.get("configured_channels")
|
||||
channels_display = "—"
|
||||
if isinstance(configured_channels, list) and configured_channels:
|
||||
channels_display = ", ".join(str(channel) for channel in configured_channels)
|
||||
cc = alert_status.get("configured_channels")
|
||||
cd = "—"
|
||||
if isinstance(cc, list) and cc:
|
||||
cd = ", ".join(str(channel) for channel in cc)
|
||||
|
||||
dedup_seconds_raw = alert_status.get("dedup_seconds", 0.0)
|
||||
dedup_seconds = float(dedup_seconds_raw) if isinstance(dedup_seconds_raw, int | float) else 0.0
|
||||
tradable_pairs_display = (
|
||||
", ".join(controls.tradable_pairs) if controls.tradable_pairs else "All"
|
||||
ddsr = alert_status.get("dedup_seconds", 0.0)
|
||||
dds = float(ddsr) if isinstance(ddsr, int | float) else 0.0
|
||||
tpd = ", ".join(ctl.tradable_pairs) if ctl.tradable_pairs else "All"
|
||||
max_trade_capital_usd = (
|
||||
f"{float(rs.max_trade_capital_usd):.2f} USD"
|
||||
if rs.max_trade_capital_usd is not None
|
||||
else "—"
|
||||
)
|
||||
max_trade_capital_usd_value = (
|
||||
f"{float(rs.max_trade_capital_usd):.2f}" if rs.max_trade_capital_usd is not None else ""
|
||||
)
|
||||
max_concurrent_trades = (
|
||||
str(rs.max_concurrent_trades) if rs.max_concurrent_trades is not None else "—"
|
||||
)
|
||||
max_concurrent_trades_value = (
|
||||
str(rs.max_concurrent_trades) if rs.max_concurrent_trades is not None else ""
|
||||
)
|
||||
alerts_last_channel_results = [
|
||||
str(item) for item in cast(list[object], alert_status.get("last_channel_results", []))
|
||||
]
|
||||
|
||||
return {
|
||||
"execution_status": "running" if controls.is_running else "stopped",
|
||||
"kill_switch_status": "active" if controls.kill_switch.is_active else "inactive",
|
||||
"kill_switch_reason": controls.kill_switch.reason or "—",
|
||||
"paper_trading_mode": "enabled" if settings.paper_trading_mode else "disabled",
|
||||
"trade_capital_usd": f"{float(settings.trade_capital_usd):.2f} USD",
|
||||
"trade_capital_usd_value": f"{float(settings.trade_capital_usd):.2f}",
|
||||
"max_trade_capital_usd": (
|
||||
"—"
|
||||
if settings.max_trade_capital_usd is None
|
||||
else f"{float(settings.max_trade_capital_usd):.2f} USD"
|
||||
),
|
||||
"max_trade_capital_usd_value": (
|
||||
""
|
||||
if settings.max_trade_capital_usd is None
|
||||
else f"{float(settings.max_trade_capital_usd):.2f}"
|
||||
),
|
||||
"max_concurrent_trades": (
|
||||
"—" if settings.max_concurrent_trades is None else str(settings.max_concurrent_trades)
|
||||
),
|
||||
"max_concurrent_trades_value": (
|
||||
"" if settings.max_concurrent_trades is None else str(settings.max_concurrent_trades)
|
||||
),
|
||||
"execution_status": "running" if ctl.is_running else "stopped",
|
||||
"kill_switch_status": "active" if ctl.kill_switch.is_active else "inactive",
|
||||
"kill_switch_reason": ctl.kill_switch.reason or "—",
|
||||
"paper_trading_mode": "enabled" if rs.paper_trading_mode else "disabled",
|
||||
"trade_capital_usd": f"{float(rs.trade_capital_usd):.2f} USD",
|
||||
"trade_capital_usd_value": f"{float(rs.trade_capital_usd):.2f}",
|
||||
"max_trade_capital_usd": max_trade_capital_usd,
|
||||
"max_trade_capital_usd_value": max_trade_capital_usd_value,
|
||||
"max_concurrent_trades": max_concurrent_trades,
|
||||
"max_concurrent_trades_value": max_concurrent_trades_value,
|
||||
"alerts_enabled": "enabled" if bool(alert_status.get("enabled", False)) else "disabled",
|
||||
"alerts_channels": channels_display,
|
||||
"alerts_channels": cd,
|
||||
"alerts_min_severity": str(alert_status.get("min_severity", "—")),
|
||||
"alerts_dedup_seconds": f"{dedup_seconds:.0f}",
|
||||
"alerts_dedup_seconds": f"{dds:.0f}",
|
||||
"alerts_last_result": str(alert_status.get("last_result", "unavailable")),
|
||||
"alerts_last_attempted_at": str(alert_status.get("last_attempted_at") or "—"),
|
||||
"alerts_last_success_at": str(alert_status.get("last_success_at") or "—"),
|
||||
@@ -310,12 +319,12 @@ def _dashboard_controls(request: Request) -> dict[str, object]:
|
||||
"alerts_last_channel_results": [
|
||||
str(item) for item in cast(list[object], alert_status.get("last_channel_results", []))
|
||||
],
|
||||
"tradable_pairs_display": tradable_pairs_display,
|
||||
"tradable_pairs_value": ", ".join(controls.tradable_pairs),
|
||||
"strategy_mode": controls.strategy_mode,
|
||||
"strategy_profit_threshold": f"{controls.strategy_profit_threshold:.6f}",
|
||||
"strategy_max_depth_levels": str(controls.strategy_max_depth_levels),
|
||||
"updated_at": controls.updated_at.isoformat(),
|
||||
"tradable_pairs_display": tpd,
|
||||
"tradable_pairs_value": ", ".join(ctl.tradable_pairs),
|
||||
"strategy_mode": ctl.strategy_mode,
|
||||
"strategy_profit_threshold": f"{ctl.strategy_profit_threshold:.6f}",
|
||||
"strategy_max_depth_levels": str(ctl.strategy_max_depth_levels),
|
||||
"updated_at": ctl.updated_at.isoformat(),
|
||||
"start_endpoint": "/dashboard/control/start",
|
||||
"stop_endpoint": "/dashboard/control/stop",
|
||||
"kill_switch_endpoint": "/dashboard/control/kill-switch",
|
||||
@@ -519,34 +528,38 @@ async def dashboard_control_kill_switch(request: Request) -> HTMLResponse:
|
||||
|
||||
@router.post("/dashboard/control/config", response_class=HTMLResponse)
|
||||
async def dashboard_control_config(request: Request) -> HTMLResponse:
|
||||
controls = _dashboard_controls_state(request)
|
||||
settings = request.app.state.settings
|
||||
ctl = _dashboard_controls_state(request)
|
||||
rs = request.app.state.settings
|
||||
form = _parse_form_body(await request.body())
|
||||
|
||||
if "trade_capital_usd" in form and form["trade_capital_usd"]:
|
||||
settings.trade_capital_usd = float(form["trade_capital_usd"])
|
||||
rs.trade_capital_usd = float(form["trade_capital_usd"])
|
||||
if "max_trade_capital_usd" in form:
|
||||
max_trade_capital_value = form["max_trade_capital_usd"].strip()
|
||||
settings.max_trade_capital_usd = (
|
||||
float(max_trade_capital_value) if max_trade_capital_value else None
|
||||
)
|
||||
mtcv = form["max_trade_capital_usd"].strip()
|
||||
rs.max_trade_capital_usd = float(mtcv) if mtcv else None
|
||||
if "max_concurrent_trades" in form:
|
||||
max_concurrent_value = form["max_concurrent_trades"].strip()
|
||||
settings.max_concurrent_trades = int(max_concurrent_value) if max_concurrent_value else None
|
||||
mcv = form["max_concurrent_trades"].strip()
|
||||
rs.max_concurrent_trades = int(mcv) if mcv else None
|
||||
|
||||
controls.tradable_pairs = _parse_comma_separated_list(form.get("tradable_pairs"))
|
||||
form_pairs = form.get("tradable_pairs")
|
||||
ctl.tradable_pairs = _parse_comma_separated_list(form_pairs)
|
||||
if "strategy_mode" in form and form["strategy_mode"].strip():
|
||||
strategy_mode = form["strategy_mode"].strip().lower()
|
||||
if strategy_mode not in {"incremental", "paper", "live"}:
|
||||
raise ValueError("strategy_mode must be one of: incremental, paper, live")
|
||||
controls.strategy_mode = strategy_mode
|
||||
if "strategy_profit_threshold" in form and form["strategy_profit_threshold"].strip():
|
||||
controls.strategy_profit_threshold = float(form["strategy_profit_threshold"])
|
||||
if "strategy_max_depth_levels" in form and form["strategy_max_depth_levels"].strip():
|
||||
controls.strategy_max_depth_levels = int(form["strategy_max_depth_levels"])
|
||||
e = "strategy_mode must be one of: incremental, paper, live"
|
||||
raise ValueError(e)
|
||||
ctl.strategy_mode = strategy_mode
|
||||
if "strategy_profit_threshold" in form:
|
||||
if form["strategy_profit_threshold"].strip():
|
||||
spt = float(form["strategy_profit_threshold"])
|
||||
ctl.strategy_profit_threshold = spt
|
||||
if "strategy_max_depth_levels" in form:
|
||||
if form["strategy_max_depth_levels"].strip():
|
||||
smdl = int(form["strategy_max_depth_levels"])
|
||||
ctl.strategy_max_depth_levels = smdl
|
||||
|
||||
settings.paper_trading_mode = _form_bool(form.get("paper_trading_mode"))
|
||||
controls.mark_updated()
|
||||
rs.paper_trading_mode = _form_bool(form.get("paper_trading_mode"))
|
||||
ctl.mark_updated()
|
||||
|
||||
notifier = _alert_notifier(request)
|
||||
if notifier is not None:
|
||||
@@ -556,18 +569,14 @@ async def dashboard_control_config(request: Request) -> HTMLResponse:
|
||||
title="Runtime config updated",
|
||||
message="Dashboard control updated runtime risk and execution settings.",
|
||||
details={
|
||||
"trade_capital_usd": f"{settings.trade_capital_usd}",
|
||||
"trade_capital_usd": f"{rs.trade_capital_usd}",
|
||||
"max_trade_capital_usd": (
|
||||
"none"
|
||||
if settings.max_trade_capital_usd is None
|
||||
else f"{settings.max_trade_capital_usd}"
|
||||
"none" if rs.max_trade_capital_usd is None else f"{rs.max_trade_capital_usd}"
|
||||
),
|
||||
"max_concurrent_trades": (
|
||||
"none"
|
||||
if settings.max_concurrent_trades is None
|
||||
else f"{settings.max_concurrent_trades}"
|
||||
"none" if rs.max_concurrent_trades is None else f"{rs.max_concurrent_trades}"
|
||||
),
|
||||
"paper_trading_mode": "true" if settings.paper_trading_mode else "false",
|
||||
"paper_trading_mode": "true" if rs.paper_trading_mode else "false",
|
||||
},
|
||||
)
|
||||
_record_audit(
|
||||
@@ -576,14 +585,14 @@ async def dashboard_control_config(request: Request) -> HTMLResponse:
|
||||
event_type="dashboard.control.config",
|
||||
decision="approved",
|
||||
payload={
|
||||
"trade_capital_usd": settings.trade_capital_usd,
|
||||
"max_trade_capital_usd": settings.max_trade_capital_usd,
|
||||
"max_concurrent_trades": settings.max_concurrent_trades,
|
||||
"paper_trading_mode": settings.paper_trading_mode,
|
||||
"tradable_pairs": controls.tradable_pairs,
|
||||
"strategy_mode": controls.strategy_mode,
|
||||
"strategy_profit_threshold": controls.strategy_profit_threshold,
|
||||
"strategy_max_depth_levels": controls.strategy_max_depth_levels,
|
||||
"trade_capital_usd": rs.trade_capital_usd,
|
||||
"max_trade_capital_usd": rs.max_trade_capital_usd,
|
||||
"max_concurrent_trades": rs.max_concurrent_trades,
|
||||
"paper_trading_mode": rs.paper_trading_mode,
|
||||
"tradable_pairs": ctl.tradable_pairs,
|
||||
"strategy_mode": ctl.strategy_mode,
|
||||
"strategy_profit_threshold": ctl.strategy_profit_threshold,
|
||||
"strategy_max_depth_levels": ctl.strategy_max_depth_levels,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+25
-57
@@ -24,7 +24,7 @@ class MetricsCalculator:
|
||||
|
||||
def compute(self) -> PerformanceMetrics:
|
||||
with self._store.connect() as conn:
|
||||
trade_metrics = conn.execute("""
|
||||
tm = conn.execute("""
|
||||
SELECT
|
||||
COALESCE(SUM(COALESCE(realized_pnl, 0)), 0) AS realized_pnl_usd,
|
||||
COUNT(*) AS total_trades,
|
||||
@@ -46,7 +46,7 @@ class MetricsCalculator:
|
||||
WHERE finished_at IS NOT NULL
|
||||
""").fetchone()
|
||||
|
||||
opportunity_metrics = conn.execute("""
|
||||
om = conn.execute("""
|
||||
SELECT
|
||||
COUNT(*) AS opportunity_count,
|
||||
MIN(detected_at) AS first_detected_at,
|
||||
@@ -54,79 +54,47 @@ class MetricsCalculator:
|
||||
FROM opportunities
|
||||
""").fetchone()
|
||||
|
||||
fill_metrics = conn.execute("""
|
||||
fm = conn.execute("""
|
||||
SELECT AVG(filled_volume / volume) AS fill_rate
|
||||
FROM orders
|
||||
WHERE volume > 0 AND filled_volume IS NOT NULL
|
||||
""").fetchone()
|
||||
|
||||
realized_pnl_usd = (
|
||||
float(trade_metrics[0]) if trade_metrics and trade_metrics[0] is not None else 0.0
|
||||
)
|
||||
total_trades = (
|
||||
int(trade_metrics[1]) if trade_metrics and trade_metrics[1] is not None else 0
|
||||
)
|
||||
winning_trades = (
|
||||
int(trade_metrics[2]) if trade_metrics and trade_metrics[2] is not None else 0
|
||||
)
|
||||
win_rate = winning_trades / total_trades if total_trades > 0 else None
|
||||
r_pnl_usd = float(tm[0]) if tm and tm[0] is not None else 0.0
|
||||
tt = int(tm[1]) if tm and tm[1] is not None else 0
|
||||
wt = int(tm[2]) if tm and tm[2] is not None else 0
|
||||
wr = wt / tt if tt > 0 else None
|
||||
|
||||
avg_trade_duration_seconds = (
|
||||
float(trade_metrics[3]) if trade_metrics and trade_metrics[3] is not None else None
|
||||
)
|
||||
atd = float(tm[3]) if tm and tm[3] is not None else None
|
||||
|
||||
opportunity_count = (
|
||||
int(opportunity_metrics[0])
|
||||
if opportunity_metrics is not None and opportunity_metrics[0] is not None
|
||||
else 0
|
||||
)
|
||||
first_detected_at = (
|
||||
opportunity_metrics[1]
|
||||
if opportunity_metrics is not None and isinstance(opportunity_metrics[1], datetime)
|
||||
else None
|
||||
)
|
||||
last_detected_at = (
|
||||
opportunity_metrics[2]
|
||||
if opportunity_metrics is not None and isinstance(opportunity_metrics[2], datetime)
|
||||
else None
|
||||
)
|
||||
oc = int(om[0]) if om is not None and om[0] is not None else 0
|
||||
fo = om[1] if om is not None and isinstance(om[1], datetime) else None
|
||||
lo = om[2] if om is not None and isinstance(om[2], datetime) else None
|
||||
|
||||
opportunities_per_minute: float | None
|
||||
if (
|
||||
opportunity_count >= 2
|
||||
and first_detected_at is not None
|
||||
and last_detected_at is not None
|
||||
):
|
||||
span_seconds = (last_detected_at - first_detected_at).total_seconds()
|
||||
if oc >= 2 and fo is not None and lo is not None:
|
||||
span_seconds = (lo - fo).total_seconds()
|
||||
opportunities_per_minute = (
|
||||
opportunity_count / (span_seconds / 60.0)
|
||||
if span_seconds > 0.0
|
||||
else float(opportunity_count)
|
||||
oc / (span_seconds / 60.0) if span_seconds > 0.0 else float(oc)
|
||||
)
|
||||
elif opportunity_count == 1:
|
||||
elif oc == 1:
|
||||
opportunities_per_minute = 60.0
|
||||
else:
|
||||
opportunities_per_minute = None
|
||||
|
||||
fill_rate = float(fill_metrics[0]) if fill_metrics and fill_metrics[0] is not None else None
|
||||
fill_rate = float(fm[0]) if fm and fm[0] is not None else None
|
||||
|
||||
latency_p50_seconds = (
|
||||
float(trade_metrics[4]) if trade_metrics and trade_metrics[4] is not None else None
|
||||
)
|
||||
latency_p95_seconds = (
|
||||
float(trade_metrics[5]) if trade_metrics and trade_metrics[5] is not None else None
|
||||
)
|
||||
latency_p99_seconds = (
|
||||
float(trade_metrics[6]) if trade_metrics and trade_metrics[6] is not None else None
|
||||
)
|
||||
lp50 = float(tm[4]) if tm and tm[4] is not None else None
|
||||
lp95 = float(tm[5]) if tm and tm[5] is not None else None
|
||||
lp99 = float(tm[6]) if tm and tm[6] is not None else None
|
||||
|
||||
return PerformanceMetrics(
|
||||
realized_pnl_usd=realized_pnl_usd,
|
||||
win_rate=win_rate,
|
||||
avg_trade_duration_seconds=avg_trade_duration_seconds,
|
||||
realized_pnl_usd=r_pnl_usd,
|
||||
win_rate=wr,
|
||||
avg_trade_duration_seconds=atd,
|
||||
opportunities_per_minute=opportunities_per_minute,
|
||||
fill_rate=fill_rate,
|
||||
latency_p50_seconds=latency_p50_seconds,
|
||||
latency_p95_seconds=latency_p95_seconds,
|
||||
latency_p99_seconds=latency_p99_seconds,
|
||||
latency_p50_seconds=lp50,
|
||||
latency_p95_seconds=lp95,
|
||||
latency_p99_seconds=lp99,
|
||||
)
|
||||
|
||||
@@ -127,31 +127,32 @@ def persist_runtime_snapshot(app: FastAPI, *, note: str | None = None) -> Runtim
|
||||
|
||||
|
||||
async def restore_runtime_state(app: FastAPI) -> RuntimeRecoveryReport:
|
||||
controls = _controls(app)
|
||||
ctl = _controls(app)
|
||||
store = _store(app)
|
||||
runtime_repository = _runtime_repository(app)
|
||||
repo = _runtime_repository(app)
|
||||
|
||||
restored_from_snapshot = False
|
||||
snapshot_at: str | None = None
|
||||
|
||||
latest = runtime_repository.latest() if runtime_repository is not None else None
|
||||
latest = repo.latest() if repo is not None else None
|
||||
if latest is not None:
|
||||
restored_from_snapshot = True
|
||||
snapshot_at = latest.snapshot_at.isoformat()
|
||||
controls.is_running = latest.is_running
|
||||
ctl.is_running = latest.is_running
|
||||
if latest.kill_switch_active:
|
||||
controls.kill_switch.activate(reason=latest.kill_switch_reason or "recovered")
|
||||
r = latest.kill_switch_reason or "recovered"
|
||||
ctl.kill_switch.activate(reason=r)
|
||||
else:
|
||||
controls.kill_switch.deactivate()
|
||||
controls.mark_updated()
|
||||
ctl.kill_switch.deactivate()
|
||||
ctl.mark_updated()
|
||||
|
||||
open_trades = _open_trade_count(store)
|
||||
restart_guard_active = False
|
||||
if open_trades > 0:
|
||||
controls.is_running = False
|
||||
if not controls.kill_switch.is_active:
|
||||
controls.kill_switch.activate(reason="recovery_open_trades_detected")
|
||||
controls.mark_updated()
|
||||
ctl.is_running = False
|
||||
if not ctl.kill_switch.is_active:
|
||||
ctl.kill_switch.activate(reason="recovery_open_trades_detected")
|
||||
ctl.mark_updated()
|
||||
restart_guard_active = True
|
||||
|
||||
report = RuntimeRecoveryReport(
|
||||
|
||||
Reference in New Issue
Block a user