Improve admin UI setup flow

This commit is contained in:
Alishahryar1
2026-05-10 15:57:56 -07:00
parent 33de002e04
commit e386a3c8aa
8 changed files with 430 additions and 56 deletions
+29 -30
View File
@@ -14,7 +14,7 @@ Use Claude Code CLI, VS Code, JetBrains ACP, or chat bots through your own Anthr
Free Claude Code routes Anthropic Messages API traffic from Claude Code to NVIDIA NIM, Kimi, Wafer, OpenRouter, DeepSeek, LM Studio, llama.cpp, or Ollama. It keeps Claude Code's client-side protocol stable while letting you choose free, paid, or local models.
[Quick Start](#quick-start) · [Local Admin UI](#local-admin-ui) · [Providers](#choose-a-provider) · [Clients](#connect-claude-code) · [Configuration](#configuration-reference) · [Troubleshooting](#troubleshooting) · [Development](#development)
[Quick Start](#quick-start) · [Providers](#choose-a-provider) · [Clients](#connect-claude-code) · [Configuration](#configuration-reference) · [Troubleshooting](#troubleshooting) · [Development](#development)
</div>
@@ -42,14 +42,21 @@ Free Claude Code routes Anthropic Messages API traffic from Claude Code to NVIDI
- Native Claude Code `/model` picker support through the proxy's `/v1/models` endpoint (Claude Code must opt in to Gateway model discovery; see [Model Picker](#model-picker)).
- Streaming, tool use, reasoning/thinking block handling, and local request optimizations.
- Optional Discord or Telegram bot wrapper for remote coding sessions.
- Optional Usage through the VSCode extension.
- Optional voice-note transcription through local Whisper or NVIDIA NIM.
- Local **Admin UI** at `/admin` to edit supported proxy settings, validate changes, and check providers (loopback access only).
## Quick Start
### 1. Install Requirements
### 1. Install the latest version of [Claude Code](https://code.claude.com/docs/en/overview)
Install [Claude Code](https://github.com/anthropics/claude-code), then install `uv` and Python 3.14.
```bash
npm install -g @anthropic-ai/claude-code
```
### 2. Install Runtime Requirements
Install the latest version of [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.14.
macOS/Linux:
@@ -67,7 +74,7 @@ uv self update
uv python install 3.14
```
### 2. Install The Proxy
### 3. Install The Proxy
```bash
uv tool install git+https://github.com/Alishahryar1/free-claude-code.git
@@ -75,10 +82,10 @@ uv tool install git+https://github.com/Alishahryar1/free-claude-code.git
Use the same command to update the proxy.
### 3. Start The Proxy
### 4. Start The Proxy
```bash
free-claude-code
fcc-server
```
After startup, the terminal prints the proxy and admin URLs:
@@ -90,7 +97,7 @@ Admin UI: http://127.0.0.1:8082/admin
Many terminals make these clickable. Use your configured `PORT` if it is not `8082`.
### 4. Open The Admin UI
### 5. Open The Admin UI
Open the **Admin UI** URL from the terminal output.
@@ -98,35 +105,21 @@ Open the **Admin UI** URL from the terminal output.
<img src="assets/admin-page.png" alt="Local admin UI for proxy settings" width="700">
</div>
### 5. Configure Provider And Model
### 6. Configure Provider And Model
In the Admin UI, paste your provider key, set `MODEL`, then click **Validate** and **Apply**. Values are saved to `~/.config/free-claude-code/.env`.
### 6. Run Claude Code
Point `ANTHROPIC_BASE_URL` at the proxy root. Do not append `/v1`. Use the same `ANTHROPIC_AUTH_TOKEN` you configured in the Admin UI.
PowerShell:
```powershell
$env:ANTHROPIC_AUTH_TOKEN="freecc"; $env:ANTHROPIC_BASE_URL="http://localhost:8082"; $env:CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="1"; claude
```
Bash:
### 7. Run Claude Code
```bash
ANTHROPIC_AUTH_TOKEN="freecc" ANTHROPIC_BASE_URL="http://localhost:8082" CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 claude
fcc-claude
```
## Local Admin UI
The Admin UI is local-only: non-loopback clients, or requests with a non-local `Origin`, receive **403**.
It can load masked config values, validate/apply changes, show runtime status, probe local backends (LM Studio, llama.cpp, Ollama), test providers, list models, and refresh the model cache.
`fcc-claude` reads the current configured port and auth token each time it starts, sets the Claude Code environment variables, and then launches the real `claude` command.
## Choose A Provider
Use these examples when setting `MODEL` in the [Admin UI](#local-admin-ui) or in `.env`.
Use these examples when setting `MODEL` in the Admin UI or in `.env`.
Model values use this format:
@@ -303,10 +296,14 @@ MODEL="wafer/DeepSeek-V4-Pro"
### Claude Code CLI
For terminal use, prefer the installed launcher:
```bash
ANTHROPIC_AUTH_TOKEN="freecc" ANTHROPIC_BASE_URL="http://localhost:8082" CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 claude
fcc-claude
```
Keep `fcc-server` running while you work. The Admin UI manages proxy config, restarts the server when runtime settings change, and `fcc-claude` reads the current Admin UI-managed port and auth token every time it starts.
### VS Code Extension
Open Settings, search for `claude-code.environmentVariables`, choose **Edit in settings.json**, and add:
@@ -344,13 +341,13 @@ Restart the IDE after changing the file.
Claude Code 2.1.126 or later can populate `/model` from this proxy's Gateway `/v1/models` response when `ANTHROPIC_BASE_URL` points here. In **2.1.1262.1.128** that discovery was automatic; **newer releases** require **`CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`** in the same environment as `ANTHROPIC_*`. Omit the flag if you only set models via proxy config and never use `/model` discovery.
Start Claude Code with that variable set (see [Quick Start](#4-run-claude-code)), run `/model`, and choose any discovered provider model.
Start Claude Code with that variable set (see [Run Claude Code](#7-run-claude-code)), run `/model`, and choose any discovered provider model.
<div align="center">
<img src="assets/cc-model-picker.png" alt="Claude Code model picker showing gateway models" width="700">
</div>
The proxy lists models for configured provider keys and referenced local providers. Picker-safe IDs are routed back to the real provider/model automatically, so no `.env` edit or separate launcher script is needed after startup.
The proxy lists models for configured provider keys and referenced local providers. Picker-safe IDs are routed back to the real provider/model automatically, so no `.env` edit is needed after startup.
Each provider model also has a `(no thinking)` picker variant. Use it when a model does not support Claude Code thinking or fails with adaptive-thinking requests. It routes to the same upstream model while asking Claude Code to send a non-thinking request.
@@ -596,8 +593,10 @@ Run them in that order before pushing. CI enforces the same checks.
`pyproject.toml` installs:
- `free-claude-code`: starts the proxy with configured host and port.
- `fcc-server`: starts the proxy with configured host and port.
- `fcc-init`: optional file-based config scaffold at `~/.config/free-claude-code/.env`.
- `fcc-claude`: launches Claude Code with the configured local proxy URL, auth token, and model discovery flag.
- `free-claude-code`: compatibility alias for `fcc-server`.
### Extending
+2 -1
View File
@@ -995,6 +995,7 @@ def write_managed_env(updates: Mapping[str, Any]) -> dict[str, Any]:
return validation | {"applied": False, "pending_fields": []}
target_values = _target_values_with_updates(updates)
pending_fields = changed_pending_fields(updates)
path = managed_env_path()
path.parent.mkdir(parents=True, exist_ok=True)
temp_path = path.with_suffix(path.suffix + ".tmp")
@@ -1006,7 +1007,7 @@ def write_managed_env(updates: Mapping[str, Any]) -> dict[str, Any]:
"errors": [],
"env_preview": render_env_file(target_values, mask_secrets=True),
"path": str(path),
"pending_fields": changed_pending_fields(updates),
"pending_fields": pending_fields,
}
+45 -2
View File
@@ -2,16 +2,18 @@
from __future__ import annotations
import inspect
import ipaddress
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
import httpx
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from config.settings import Settings
from config.settings import get_settings as get_cached_settings
from providers.registry import ProviderRegistry
@@ -22,6 +24,7 @@ from .admin_config import (
validate_updates,
write_managed_env,
)
from .admin_urls import local_admin_url
router = APIRouter()
@@ -104,13 +107,25 @@ async def validate_admin_config(payload: AdminConfigPayload, request: Request):
@router.post("/admin/api/config/apply")
async def apply_admin_config(payload: AdminConfigPayload, request: Request):
async def apply_admin_config(
payload: AdminConfigPayload,
request: Request,
background_tasks: BackgroundTasks,
):
require_loopback_admin(request)
result = write_managed_env(_filtered_values(payload.values))
if not result["applied"]:
return result
get_cached_settings.cache_clear()
restart = _restart_metadata(result["pending_fields"], request)
result["restart"] = restart
if restart["required"] and restart["automatic"]:
callback = request.app.state.admin_restart_callback
background_tasks.add_task(_invoke_admin_restart_callback, callback)
request.app.state.admin_pending_fields = []
return result
old_registry = getattr(request.app.state, "provider_registry", None)
if isinstance(old_registry, ProviderRegistry):
await old_registry.cleanup()
@@ -200,6 +215,34 @@ def _filtered_values(values: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in values.items() if key in FIELD_BY_KEY}
async def _invoke_admin_restart_callback(callback: Any) -> None:
result = callback()
if inspect.isawaitable(result):
await result
def _restart_metadata(fields: list[str], request: Request) -> dict[str, Any]:
callback = getattr(request.app.state, "admin_restart_callback", None)
automatic = bool(fields and callable(callback))
return {
"required": bool(fields),
"automatic": automatic,
"admin_url": _next_admin_url() if automatic else None,
"fields": fields,
}
def _next_admin_url() -> str:
fields = {
field["key"]: field["value"] for field in load_config_response()["fields"]
}
settings = Settings.model_construct(
host=fields.get("HOST") or "0.0.0.0",
port=int(fields.get("PORT") or 8082),
)
return local_admin_url(settings)
def _local_provider_url(provider_id: str, values: dict[str, str]) -> str:
if provider_id == "lmstudio":
return values.get("LM_STUDIO_BASE_URL", "")
+11 -2
View File
@@ -339,11 +339,20 @@ async function apply() {
showValidationResult(result);
return;
}
const pending = result.pending_fields || [];
const restart = result.restart || {};
if (restart.required && restart.automatic) {
showMessage("Applied. Restarting server...", "ok");
byId("applyButton").disabled = true;
setTimeout(() => {
window.location.href = restart.admin_url || "/admin";
}, 1600);
return;
}
const pending = restart.required ? restart.fields || [] : result.pending_fields || [];
await load();
showMessage(
pending.length
? `Applied. Pending manual runtime action: ${pending.join(", ")}`
? `Applied. Restart fcc-server to use: ${pending.join(", ")}`
: "Applied",
"ok",
);
+121 -18
View File
@@ -2,7 +2,24 @@
from __future__ import annotations
import os
import subprocess
import sys
from collections.abc import Mapping, Sequence
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
import uvicorn
from api.admin_urls import local_proxy_root_url
from api.app import GracefulLifespanApp, create_app
from cli.process_registry import kill_all_best_effort
from config.settings import Settings, get_settings
PROXY_PREFLIGHT_PATH = "/health"
PROXY_PREFLIGHT_TIMEOUT_SECONDS = 1.5
SERVER_GRACEFUL_SHUTDOWN_SECONDS = 5
def _load_env_template() -> str:
@@ -21,26 +38,45 @@ def _load_env_template() -> str:
def serve() -> None:
"""Start the FastAPI server (registered as `free-claude-code` script)."""
import uvicorn
from cli.process_registry import kill_all_best_effort
from config.settings import get_settings
settings = get_settings()
"""Start the FastAPI server (registered as `fcc-server` script)."""
try:
uvicorn.run(
"api.app:create_asgi_app",
factory=True,
host=settings.host,
port=settings.port,
log_level="debug",
timeout_graceful_shutdown=5,
)
while True:
settings = get_settings()
if not _run_supervised_server(settings):
return
get_settings.cache_clear()
finally:
kill_all_best_effort()
def _run_supervised_server(settings: Settings) -> bool:
"""Run one uvicorn server instance; return whether admin requested restart."""
restart_requested = False
server_holder: dict[str, uvicorn.Server] = {}
def request_restart() -> None:
nonlocal restart_requested
restart_requested = True
if server := server_holder.get("server"):
server.should_exit = True
app = create_app(lifespan_enabled=False)
app.state.admin_restart_callback = request_restart
asgi_app = GracefulLifespanApp(app)
config = uvicorn.Config(
asgi_app,
host=settings.host,
port=settings.port,
log_level="debug",
timeout_graceful_shutdown=SERVER_GRACEFUL_SHUTDOWN_SECONDS,
)
server = uvicorn.Server(config)
server_holder["server"] = server
server.run()
return restart_requested
def init() -> None:
"""Scaffold config at ~/.config/free-claude-code/.env (registered as `fcc-init`)."""
config_dir = Path.home() / ".config" / "free-claude-code"
@@ -55,6 +91,73 @@ def init() -> None:
template = _load_env_template()
env_file.write_text(template, encoding="utf-8")
print(f"Config created at {env_file}")
print(
"Edit it to set your API keys and model preferences, then run: free-claude-code"
)
print("Edit it to set your API keys and model preferences, then run: fcc-server")
def _claude_child_env(
settings: Settings, base_env: Mapping[str, str]
) -> dict[str, str]:
"""Return a Claude Code environment that targets this proxy."""
env = {
key: value
for key, value in base_env.items()
if not key.startswith("ANTHROPIC_")
}
env["ANTHROPIC_BASE_URL"] = local_proxy_root_url(settings)
env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1"
if token := settings.anthropic_auth_token.strip():
env["ANTHROPIC_AUTH_TOKEN"] = token
return env
def _preflight_proxy(proxy_root_url: str) -> str | None:
"""Return an error message when the local proxy health check is unreachable."""
url = f"{proxy_root_url.rstrip('/')}{PROXY_PREFLIGHT_PATH}"
request = Request(url, method="GET")
try:
with urlopen(request, timeout=PROXY_PREFLIGHT_TIMEOUT_SECONDS) as response:
status_code = response.getcode()
except HTTPError as exc:
return f"returned HTTP {exc.code}"
except URLError as exc:
return str(exc.reason)
except OSError as exc:
return str(exc)
if not 200 <= status_code < 300:
return f"returned HTTP {status_code}"
return None
def launch_claude(argv: Sequence[str] | None = None) -> None:
"""Launch Claude Code with Free Claude Code proxy environment variables."""
settings = get_settings()
proxy_root_url = local_proxy_root_url(settings)
if error := _preflight_proxy(proxy_root_url):
print(
f"Free Claude Code proxy is not reachable at {proxy_root_url}: {error}",
file=sys.stderr,
)
print("Start it in another terminal with: fcc-server", file=sys.stderr)
raise SystemExit(1)
args = list(sys.argv[1:] if argv is None else argv)
command = [settings.claude_cli_bin, *args]
env = _claude_child_env(settings, os.environ)
try:
result = subprocess.run(command, env=env, check=False)
except FileNotFoundError:
print(
f"Could not find Claude Code command: {settings.claude_cli_bin}",
file=sys.stderr,
)
print(
"Install Claude Code with: npm install -g @anthropic-ai/claude-code",
file=sys.stderr,
)
raise SystemExit(127) from None
raise SystemExit(result.returncode)
+2
View File
@@ -25,8 +25,10 @@ dependencies = [
]
[project.scripts]
fcc-server = "cli.entrypoints:serve"
free-claude-code = "cli.entrypoints:serve"
fcc-init = "cli.entrypoints:init"
fcc-claude = "cli.entrypoints:launch_claude"
[project.optional-dependencies]
voice = [
+61
View File
@@ -19,6 +19,7 @@ def _local_client(app):
def _set_home(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.chdir(tmp_path)
def _clear_process_config(monkeypatch) -> None:
@@ -28,6 +29,9 @@ def _clear_process_config(monkeypatch) -> None:
"OPENROUTER_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"FCC_ENV_FILE",
"HOST",
"PORT",
"LOG_FILE",
):
monkeypatch.delenv(key, raising=False)
@@ -103,6 +107,63 @@ def test_admin_apply_writes_complete_managed_env_and_masks_preview(
assert "MODEL=open_router/test-model" in text
assert "OPENROUTER_API_KEY=router-secret" in text
assert "ANTHROPIC_AUTH_TOKEN=" in text
assert body["restart"] == {
"required": False,
"automatic": False,
"admin_url": None,
"fields": [],
}
def test_admin_apply_restart_required_reports_automatic_restart(monkeypatch, tmp_path):
_set_home(monkeypatch, tmp_path)
_clear_process_config(monkeypatch)
app = create_app(lifespan_enabled=False)
callbacks: list[str] = []
async def restart_callback() -> None:
callbacks.append("restart")
app.state.admin_restart_callback = restart_callback
response = _local_client(app).post(
"/admin/api/config/apply",
json={"values": {"PORT": "9090"}},
)
assert response.status_code == 200
body = response.json()
assert body["applied"] is True
assert body["pending_fields"] == ["PORT"]
assert body["restart"] == {
"required": True,
"automatic": True,
"admin_url": "http://127.0.0.1:9090/admin",
"fields": ["PORT"],
}
assert callbacks == ["restart"]
def test_admin_apply_restart_required_reports_manual_fallback(monkeypatch, tmp_path):
_set_home(monkeypatch, tmp_path)
_clear_process_config(monkeypatch)
app = create_app(lifespan_enabled=False)
response = _local_client(app).post(
"/admin/api/config/apply",
json={"values": {"PORT": "9091"}},
)
assert response.status_code == 200
body = response.json()
assert body["applied"] is True
assert body["pending_fields"] == ["PORT"]
assert body["restart"] == {
"required": True,
"automatic": False,
"admin_url": None,
"fields": ["PORT"],
}
def test_admin_process_env_values_are_locked_and_not_written(monkeypatch, tmp_path):
+159 -3
View File
@@ -1,7 +1,28 @@
"""Tests for cli/entrypoints.py — fcc-init scaffolding logic."""
import subprocess
import tomllib
from pathlib import Path
from unittest.mock import patch
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from config.settings import Settings
def _launcher_settings(
*,
port: int = 8082,
token: str = "freecc",
claude_bin: str = "claude-test",
) -> Settings:
return Settings.model_construct(
host="0.0.0.0",
port=port,
anthropic_auth_token=token,
claude_cli_bin=claude_bin,
)
def _run_init(tmp_home: Path) -> tuple[str, Path]:
@@ -78,7 +99,142 @@ def test_init_skips_if_env_already_exists(tmp_path: Path) -> None:
def test_init_prints_next_step_hint(tmp_path: Path) -> None:
"""init() tells the user to run free-claude-code after editing .env."""
"""init() tells the user to run fcc-server after editing .env."""
output, _ = _run_init(tmp_path)
assert "free-claude-code" in output
assert "fcc-server" in output
def test_cli_scripts_are_registered() -> None:
pyproject = tomllib.loads(
(Path(__file__).resolve().parents[2] / "pyproject.toml").read_text(
encoding="utf-8"
)
)
scripts = pyproject["project"]["scripts"]
assert scripts["fcc-server"] == "cli.entrypoints:serve"
assert scripts["free-claude-code"] == "cli.entrypoints:serve"
assert scripts["fcc-claude"] == "cli.entrypoints:launch_claude"
def test_serve_supervisor_restarts_when_app_requests_restart() -> None:
from cli import entrypoints
settings = _launcher_settings()
get_settings = MagicMock(side_effect=[settings, settings])
get_settings.cache_clear = MagicMock()
servers: list[object] = []
class FakeServer:
def __init__(self, config):
self.config = config
self.should_exit = False
servers.append(self)
def run(self):
if len(servers) == 1:
self.config.app.app.state.admin_restart_callback()
assert self.should_exit is True
def fake_config(app, **kwargs):
return SimpleNamespace(app=app, kwargs=kwargs)
with (
patch.object(entrypoints, "get_settings", get_settings),
patch.object(entrypoints.uvicorn, "Config", side_effect=fake_config),
patch.object(entrypoints.uvicorn, "Server", side_effect=FakeServer),
patch.object(entrypoints, "kill_all_best_effort") as kill_all,
):
entrypoints.serve()
assert len(servers) == 2
get_settings.cache_clear.assert_called_once()
kill_all.assert_called_once()
def test_claude_child_env_targets_current_proxy_config() -> None:
from cli.entrypoints import _claude_child_env
env = _claude_child_env(
_launcher_settings(port=9090, token=" proxy-token "),
{
"PATH": "keep",
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
"ANTHROPIC_AUTH_TOKEN": "old-token",
"ANTHROPIC_API_KEY": "official-key",
},
)
assert env["PATH"] == "keep"
assert env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9090"
assert env["ANTHROPIC_AUTH_TOKEN"] == "proxy-token"
assert env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] == "1"
assert "ANTHROPIC_API_KEY" not in env
def test_claude_child_env_removes_blank_configured_auth_token() -> None:
from cli.entrypoints import _claude_child_env
env = _claude_child_env(
_launcher_settings(token=""),
{
"ANTHROPIC_AUTH_TOKEN": "inherited-token",
"ANTHROPIC_API_KEY": "official-key",
},
)
assert "ANTHROPIC_AUTH_TOKEN" not in env
assert "ANTHROPIC_API_KEY" not in env
def test_launch_claude_passes_args_and_child_env(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from cli.entrypoints import launch_claude
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "old-token")
monkeypatch.setenv("KEEP_ME", "yes")
settings = _launcher_settings(port=9191, token="proxy-token")
with (
patch("cli.entrypoints.get_settings", return_value=settings),
patch("cli.entrypoints._preflight_proxy", return_value=None),
patch(
"cli.entrypoints.subprocess.run",
return_value=subprocess.CompletedProcess(["claude-test"], 7),
) as run,
pytest.raises(SystemExit) as exc_info,
):
launch_claude(["--model", "sonnet"])
assert exc_info.value.code == 7
run.assert_called_once()
assert run.call_args.args[0] == ["claude-test", "--model", "sonnet"]
child_env = run.call_args.kwargs["env"]
assert child_env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9191"
assert child_env["ANTHROPIC_AUTH_TOKEN"] == "proxy-token"
assert child_env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] == "1"
assert child_env["KEEP_ME"] == "yes"
def test_launch_claude_unreachable_proxy_exits_with_hint(
capsys: pytest.CaptureFixture[str],
) -> None:
from cli.entrypoints import launch_claude
settings = _launcher_settings(port=9393)
with (
patch("cli.entrypoints.get_settings", return_value=settings),
patch("cli.entrypoints._preflight_proxy", return_value="connection refused"),
patch("cli.entrypoints.subprocess.run") as run,
pytest.raises(SystemExit) as exc_info,
):
launch_claude([])
assert exc_info.value.code == 1
run.assert_not_called()
captured = capsys.readouterr()
assert "http://127.0.0.1:9393" in captured.err
assert "fcc-server" in captured.err