feat: add ComponentGrid and ComponentTable components for displaying components in grid and table formats
feat: implement HealthBadge and RoleBadge components for displaying health status and roles feat: create LogViewer component for streaming logs of services feat: develop SecretsEditor for managing secrets with CRUD operations feat: introduce ToolCard component for displaying tool information style: add global CSS variables and styles for consistent theming feat: set up API client for handling requests to the backend feat: implement hooks for fetching components, statuses, and tools feat: create routes for dashboard, component details, and tools feat: add service management endpoints for starting, stopping, and restarting services feat: implement event bus for handling real-time updates via SSE feat: create health check and logs endpoints for monitoring services feat: add tests for health and tools endpoints to ensure functionality chore: update project configuration files and dependencies for better development experience
This commit is contained in:
0
dashboard/.gitignore → app/.gitignore
vendored
0
dashboard/.gitignore → app/.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
31
castle-api/CLAUDE.md
Normal file
31
castle-api/CLAUDE.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working
|
||||
with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
castle-api is a FastAPI service. Castle API.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
uv sync # Install dependencies
|
||||
uv run castle-api # Run service (port 9020)
|
||||
uv run pytest tests/ -v # Run tests
|
||||
uv run ruff check . # Lint
|
||||
uv run ruff format . # Format
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/castle_api/config.py` — Settings via pydantic-settings, env prefix `CASTLE_API_`
|
||||
- `src/castle_api/main.py` — FastAPI app, lifespan, health endpoint
|
||||
- `tests/` — pytest with TestClient fixtures
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables with `CASTLE_API_` prefix:
|
||||
- `CASTLE_API_DATA_DIR` — Data directory (default: ./data)
|
||||
- `CASTLE_API_HOST` — Bind host (default: 0.0.0.0)
|
||||
- `CASTLE_API_PORT` — Port (default: 9020)
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "dashboard-api"
|
||||
name = "castle-api"
|
||||
version = "0.1.0"
|
||||
description = "Castle dashboard API"
|
||||
description = "Castle API"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi>=0.115.0",
|
||||
@@ -15,14 +15,14 @@ dependencies = [
|
||||
castle-cli = { path = "../cli", editable = true }
|
||||
|
||||
[project.scripts]
|
||||
dashboard-api = "dashboard_api.main:run"
|
||||
castle-api = "castle_api.main:run"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/dashboard_api"]
|
||||
packages = ["src/castle_api"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
@@ -33,4 +33,4 @@ dev = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["dashboard_api"]
|
||||
known-first-party = ["castle_api"]
|
||||
3
castle-api/src/castle_api/__init__.py
Normal file
3
castle-api/src/castle_api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Castle API."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Configuration for dashboard-api."""
|
||||
"""Configuration for castle-api."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
|
||||
port: int = 9020
|
||||
|
||||
model_config = {
|
||||
"env_prefix": "DASHBOARD_API_",
|
||||
"env_prefix": "CASTLE_API_",
|
||||
"env_file": ".env",
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ from pydantic import BaseModel
|
||||
from castle_cli.config import load_config, save_config
|
||||
from castle_cli.manifest import ComponentManifest
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.stream import broadcast
|
||||
from castle_api.config import settings
|
||||
from castle_api.stream import broadcast
|
||||
|
||||
router = APIRouter(prefix="/config", tags=["config"])
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from dashboard_api.bus import bus
|
||||
from castle_api.bus import bus
|
||||
|
||||
router = APIRouter(prefix="/events", tags=["events"])
|
||||
|
||||
@@ -9,7 +9,7 @@ import httpx
|
||||
|
||||
from castle_cli.config import CastleConfig
|
||||
|
||||
from dashboard_api.models import HealthStatus
|
||||
from castle_api.models import HealthStatus
|
||||
|
||||
|
||||
async def check_all_health(config: CastleConfig) -> list[HealthStatus]:
|
||||
@@ -10,7 +10,7 @@ from starlette.responses import StreamingResponse
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from castle_api.config import settings
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
@@ -11,16 +11,16 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from dashboard_api.bus import bus
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.config_editor import router as config_router
|
||||
from dashboard_api.events import router as events_router
|
||||
from dashboard_api.logs import router as logs_router
|
||||
from dashboard_api.routes import router as dashboard_router
|
||||
from dashboard_api.secrets import router as secrets_router
|
||||
from dashboard_api.services import router as services_router
|
||||
from dashboard_api.stream import close_all_subscribers, health_poll_loop, subscribe, unsubscribe
|
||||
from dashboard_api.tools import router as tools_router
|
||||
from castle_api.bus import bus
|
||||
from castle_api.config import settings
|
||||
from castle_api.config_editor import router as config_router
|
||||
from castle_api.events import router as events_router
|
||||
from castle_api.logs import router as logs_router
|
||||
from castle_api.routes import router as dashboard_router
|
||||
from castle_api.secrets import router as secrets_router
|
||||
from castle_api.services import router as services_router
|
||||
from castle_api.stream import close_all_subscribers, health_poll_loop, subscribe, unsubscribe
|
||||
from castle_api.tools import router as tools_router
|
||||
|
||||
# Set by _watch_shutdown when uvicorn begins its shutdown sequence.
|
||||
_shutting_down = False
|
||||
@@ -53,8 +53,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Castle API",
|
||||
description="Castle dashboard API, event bus, and service management",
|
||||
title="castle-api",
|
||||
description="Castle API, event bus, and service management",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
@@ -109,7 +109,7 @@ async def sse_stream() -> StreamingResponse:
|
||||
def run() -> None:
|
||||
"""Run the application with uvicorn."""
|
||||
config = uvicorn.Config(
|
||||
"dashboard_api.main:app",
|
||||
"castle_api.main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=False,
|
||||
@@ -9,9 +9,9 @@ from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.health import check_all_health
|
||||
from dashboard_api.models import (
|
||||
from castle_api.config import settings
|
||||
from castle_api.health import check_all_health
|
||||
from castle_api.models import (
|
||||
ComponentDetail,
|
||||
ComponentSummary,
|
||||
GatewayInfo,
|
||||
@@ -10,15 +10,15 @@ from starlette.responses import JSONResponse
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.health import check_all_health
|
||||
from dashboard_api.models import HealthStatus
|
||||
from dashboard_api.stream import broadcast
|
||||
from castle_api.config import settings
|
||||
from castle_api.health import check_all_health
|
||||
from castle_api.models import HealthStatus
|
||||
from castle_api.stream import broadcast
|
||||
|
||||
router = APIRouter(prefix="/services", tags=["services"])
|
||||
|
||||
UNIT_PREFIX = "castle-"
|
||||
SELF_NAME = "dashboard-api"
|
||||
SELF_NAME = "castle-api"
|
||||
|
||||
|
||||
async def _systemctl(action: str, unit: str) -> tuple[bool, str]:
|
||||
@@ -9,8 +9,8 @@ import time
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.health import check_all_health
|
||||
from castle_api.config import settings
|
||||
from castle_api.health import check_all_health
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,8 +10,8 @@ from fastapi import APIRouter, HTTPException, status
|
||||
from castle_cli.config import load_config
|
||||
from castle_cli.manifest import ComponentManifest
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.models import ToolCategory, ToolDetail, ToolSummary
|
||||
from castle_api.config import settings
|
||||
from castle_api.models import ToolCategory, ToolDetail, ToolSummary
|
||||
|
||||
router = APIRouter(tags=["tools"])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test fixtures for dashboard-api."""
|
||||
"""Test fixtures for castle-api."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
@@ -7,8 +7,8 @@ import pytest
|
||||
import yaml
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from dashboard_api.config import settings
|
||||
from dashboard_api.main import app
|
||||
from castle_api.config import settings
|
||||
from castle_api.main import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for dashboard-api health endpoint."""
|
||||
"""Tests for castle-api health endpoint."""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
74
dashboard-api/uv.lock → castle-api/uv.lock
generated
74
dashboard-api/uv.lock → castle-api/uv.lock
generated
@@ -32,6 +32,43 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castle-api"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "castle-cli" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "castle-cli", editable = "../cli" },
|
||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.0.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.34.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "pytest", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castle-cli"
|
||||
version = "0.1.0"
|
||||
@@ -86,43 +123,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashboard-api"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "castle-cli" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "castle-cli", editable = "../cli" },
|
||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.0.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.34.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "pytest", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.129.0"
|
||||
32
castle.yaml
32
castle.yaml
@@ -1,8 +1,7 @@
|
||||
gateway:
|
||||
port: 9000
|
||||
components:
|
||||
# ── Infrastructure ──────────────────────────────────────
|
||||
gateway:
|
||||
castle-gateway:
|
||||
description: Caddy reverse proxy gateway
|
||||
run:
|
||||
runner: command
|
||||
@@ -14,16 +13,15 @@ components:
|
||||
- /home/payne/.castle/generated/Caddyfile
|
||||
- --adapter
|
||||
- caddyfile
|
||||
manage:
|
||||
systemd:
|
||||
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile
|
||||
--adapter caddyfile
|
||||
expose:
|
||||
http:
|
||||
internal:
|
||||
port: 9000
|
||||
health_path: /
|
||||
manage:
|
||||
systemd:
|
||||
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile --adapter caddyfile
|
||||
|
||||
# ── Services ──────────────────────────────────────────────
|
||||
central-context:
|
||||
description: Content storage API
|
||||
run:
|
||||
@@ -44,7 +42,9 @@ components:
|
||||
caddy:
|
||||
path_prefix: /central-context
|
||||
notification-bridge:
|
||||
description: Desktop notification forwarder
|
||||
description: Desktop notification forwarder. This taps into your notifications
|
||||
system on the desktop and will forward all notifications the the central context
|
||||
server.
|
||||
run:
|
||||
runner: python_uv_tool
|
||||
cwd: notification-bridge
|
||||
@@ -63,14 +63,14 @@ components:
|
||||
proxy:
|
||||
caddy:
|
||||
path_prefix: /notifications
|
||||
dashboard-api:
|
||||
description: Castle dashboard API
|
||||
castle-api:
|
||||
description: Castle API
|
||||
run:
|
||||
runner: python_uv_tool
|
||||
cwd: dashboard-api
|
||||
cwd: castle-api
|
||||
env:
|
||||
DASHBOARD_API_CASTLE_ROOT: /data/repos/castle
|
||||
tool: dashboard-api
|
||||
CASTLE_API_CASTLE_ROOT: /data/repos/castle
|
||||
tool: castle-api
|
||||
manage:
|
||||
systemd: {}
|
||||
expose:
|
||||
@@ -137,10 +137,10 @@ components:
|
||||
manage:
|
||||
systemd: {}
|
||||
tool: {}
|
||||
dashboard:
|
||||
description: Castle web dashboard
|
||||
castle-app:
|
||||
description: Castle web app
|
||||
build:
|
||||
working_dir: dashboard
|
||||
working_dir: app
|
||||
commands:
|
||||
- - pnpm
|
||||
- build
|
||||
|
||||
@@ -8,13 +8,13 @@ import subprocess
|
||||
from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config
|
||||
|
||||
|
||||
GATEWAY_COMPONENT = "gateway"
|
||||
GATEWAY_COMPONENT = "castle-gateway"
|
||||
GATEWAY_UNIT = "castle-gateway.service"
|
||||
|
||||
|
||||
def _find_dashboard_dist(config: CastleConfig) -> str | None:
|
||||
"""Find the dashboard dist/ directory if it exists."""
|
||||
dist = config.root / "dashboard" / "dist"
|
||||
def _find_app_dist(config: CastleConfig) -> str | None:
|
||||
"""Find the app dist/ directory if it exists."""
|
||||
dist = config.root / "app" / "dist"
|
||||
if dist.exists() and (dist / "index.html").exists():
|
||||
return str(dist)
|
||||
return None
|
||||
@@ -42,17 +42,17 @@ def _generate_caddyfile(config: CastleConfig) -> str:
|
||||
lines.append(" }")
|
||||
lines.append("")
|
||||
|
||||
# Dashboard SPA at root (must come after more-specific handle_path rules)
|
||||
dashboard_dist = _find_dashboard_dist(config)
|
||||
if dashboard_dist:
|
||||
# App SPA at root (must come after more-specific handle_path rules)
|
||||
app_dist = _find_app_dist(config)
|
||||
if app_dist:
|
||||
lines.append(" handle {")
|
||||
lines.append(f" root * {dashboard_dist}")
|
||||
lines.append(f" root * {app_dist}")
|
||||
lines.append(" try_files {path} /index.html")
|
||||
lines.append(" file_server")
|
||||
lines.append(" }")
|
||||
else:
|
||||
# Fallback: serve from generated directory
|
||||
fallback = GENERATED_DIR / "dashboard"
|
||||
fallback = GENERATED_DIR / "app"
|
||||
lines.append(" handle / {")
|
||||
lines.append(f" root * {fallback}")
|
||||
lines.append(" file_server")
|
||||
@@ -70,11 +70,11 @@ def _write_generated_files(config: CastleConfig) -> None:
|
||||
caddyfile_path.write_text(_generate_caddyfile(config))
|
||||
print(f" Generated {caddyfile_path}")
|
||||
|
||||
dashboard_dist = _find_dashboard_dist(config)
|
||||
if dashboard_dist:
|
||||
print(f" Dashboard: {dashboard_dist}")
|
||||
app_dist = _find_app_dist(config)
|
||||
if app_dist:
|
||||
print(f" App: {app_dist}")
|
||||
else:
|
||||
print(" Dashboard: dist/ not found, using fallback")
|
||||
print(" App: dist/ not found, using fallback")
|
||||
|
||||
|
||||
def run_gateway(args: argparse.Namespace) -> int:
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working
|
||||
with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
dashboard-api is a FastAPI service. Castle dashboard API.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
uv sync # Install dependencies
|
||||
uv run dashboard-api # Run service (port 9020)
|
||||
uv run pytest tests/ -v # Run tests
|
||||
uv run ruff check . # Lint
|
||||
uv run ruff format . # Format
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/dashboard_api/config.py` — Settings via pydantic-settings, env prefix `DASHBOARD_API_`
|
||||
- `src/dashboard_api/main.py` — FastAPI app, lifespan, health endpoint
|
||||
- `tests/` — pytest with TestClient fixtures
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables with `DASHBOARD_API_` prefix:
|
||||
- `DASHBOARD_API_DATA_DIR` — Data directory (default: ./data)
|
||||
- `DASHBOARD_API_HOST` — Bind host (default: 0.0.0.0)
|
||||
- `DASHBOARD_API_PORT` — Port (default: 9020)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Castle dashboard API."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,23 +1,20 @@
|
||||
# Web Frontends in Castle
|
||||
|
||||
How to build, serve, and manage web frontends as castle components. Based on
|
||||
the stack used in [wild-cloud/web](https://github.com/civilsociety-dev/wild-cloud).
|
||||
How to build, serve, and manage web frontends as castle components.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Choice |
|
||||
|-------|--------|
|
||||
| **Build** | Vite 6 |
|
||||
| **Language** | TypeScript 5.8 (strict) |
|
||||
| **Framework** | React 19 |
|
||||
| **Routing** | React Router 7 |
|
||||
| **Styling** | Tailwind CSS 4 (`@tailwindcss/vite` plugin) |
|
||||
| **Components** | shadcn/ui (new-york style) + Radix UI primitives |
|
||||
| **Icons** | Lucide React |
|
||||
| **Server state** | TanStack React Query 5 |
|
||||
| **Forms** | React Hook Form + Zod validation |
|
||||
| **Testing** | Vitest + Testing Library |
|
||||
| **Package manager** | pnpm |
|
||||
- Build: Vite 6
|
||||
- Language: TypeScript 5.8 (strict)
|
||||
- Framework: React 19
|
||||
- Routing: React Router 7
|
||||
- Styling: Tailwind CSS 4 (`@tailwindcss/vite` plugin)
|
||||
- Components: shadcn/ui (new-york style) + Radix UI primitives
|
||||
- Icons: Lucide React
|
||||
- Server state: TanStack React Query 5
|
||||
- Forms: React Hook Form + Zod validation
|
||||
- Testing: Vitest + Testing Library
|
||||
- Package manager: pnpm
|
||||
|
||||
## Scaffolding a new frontend
|
||||
|
||||
|
||||
Reference in New Issue
Block a user