fix(gateway): resolve Docker connectivity — 404 endpoints, EROFS write, missing OPENCLAW_HOME (#343)
* docs: add screenshot drift prevention guide and CI workflow - Add docs/SCREENSHOT-GUIDE.md with step-by-step instructions for capturing, optimising, and committing fresh README screenshots - Add .github/workflows/screenshot-drift.yml that detects UI file changes in PRs, applies a 'screenshot-drift' label, and posts a checklist comment reminding contributors to verify screenshots Addresses: README screenshots may drift as the UI evolves quickly (setup wizard, health history timeline, and onboarding were added after the last screenshot refresh on 2026-03-11) * fix(gateway): resolve Docker connectivity — 404 endpoints, EROFS write, missing OPENCLAW_HOME Fixes #332, #333 Root causes ----------- 1. nodes/route.ts called non-existent HTTP REST endpoints on the gateway (/api/presence, /api/devices, /api/rpc). The OpenClaw gateway only exposes HTTP at /health; all other operations use WebSocket RPC. 2. gateway-runtime.ts attempted fs.writeFileSync on openclaw.json which fails with EROFS when the container or mount is read-only (docker-compose uses read_only: true by default). 3. install.sh never wrote the detected OPENCLAW_HOME or Docker host gateway IP into .env, leaving OPENCLAW_HOME empty after a fresh install. Fixes ----- - nodes/route.ts: replace HTTP calls to /api/presence and /api/devices with gateway /health reachability check + callOpenClawGateway() RPC (node.list / device.pair.list). When the openclaw CLI is unavailable inside Docker, the RPC falls back gracefully and returns connected=true with an empty list rather than a misleading 'Gateway offline' error. POST device actions likewise use callOpenClawGateway instead of /api/rpc. - gateway-runtime.ts: catch EROFS / EACCES / EPERM on config write and emit a warn-level log with a hint, instead of an error. This eliminates the 'Failed to register MC in gateway config EROFS: read-only file system' log spam on Docker installs where openclaw.json is read-only. - install.sh: after generating .env, write the detected OPENCLAW_HOME. In --docker mode also detect the Docker host IP (host-gateway or bridge default) and set OPENCLAW_GATEWAY_HOST so the container can reach a gateway running on the host out of the box. - docker-compose.yml: add extra_hosts host-gateway so the container can resolve the Docker host IP; add a commented volume snippet for mounting the OpenClaw state directory read-only.
This commit is contained in:
parent
2b4438b63a
commit
8706147f84
|
|
@ -0,0 +1,98 @@
|
||||||
|
name: Screenshot Drift Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/app/**"
|
||||||
|
- "src/components/**"
|
||||||
|
- "public/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flag-drift:
|
||||||
|
name: Flag potential screenshot drift
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check for UI changes
|
||||||
|
id: ui_changed
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: files } = await github.rest.pulls.listFiles({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiPaths = files
|
||||||
|
.map(f => f.filename)
|
||||||
|
.filter(f =>
|
||||||
|
f.startsWith('src/app/') ||
|
||||||
|
f.startsWith('src/components/') ||
|
||||||
|
f.startsWith('public/')
|
||||||
|
);
|
||||||
|
|
||||||
|
core.setOutput('count', uiPaths.length);
|
||||||
|
core.setOutput('paths', uiPaths.slice(0, 10).join('\n'));
|
||||||
|
|
||||||
|
- name: Add label if UI changed
|
||||||
|
if: steps.ui_changed.outputs.count > 0
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
// Ensure the label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'screenshot-drift',
|
||||||
|
color: 'f9d71c',
|
||||||
|
description: 'UI changed — README screenshots may need updating',
|
||||||
|
});
|
||||||
|
} catch (_) { /* label already exists */ }
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
labels: ['screenshot-drift'],
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Post checklist comment
|
||||||
|
if: steps.ui_changed.outputs.count > 0
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const paths = `${{ steps.ui_changed.outputs.paths }}`;
|
||||||
|
const body = [
|
||||||
|
'## 📸 Screenshot Drift Check',
|
||||||
|
'',
|
||||||
|
'This PR modifies UI source files. Please verify whether the README screenshots need refreshing:',
|
||||||
|
'',
|
||||||
|
'- [ ] `docs/mission-control-overview.png` — main dashboard',
|
||||||
|
'- [ ] `docs/mission-control-agents.png` — agents panel',
|
||||||
|
'- [ ] `docs/mission-control-memory-graph.png` — memory graph',
|
||||||
|
'',
|
||||||
|
'<details><summary>Changed UI files</summary>',
|
||||||
|
'',
|
||||||
|
'```',
|
||||||
|
paths,
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'</details>',
|
||||||
|
'',
|
||||||
|
'See [`docs/SCREENSHOT-GUIDE.md`](docs/SCREENSHOT-GUIDE.md) for instructions on capturing and optimising screenshots.',
|
||||||
|
'',
|
||||||
|
'_This comment is posted automatically and can be dismissed if no visual changes occurred._',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
@ -11,6 +11,15 @@ services:
|
||||||
required: false
|
required: false
|
||||||
volumes:
|
volumes:
|
||||||
- mc-data:/app/.data
|
- mc-data:/app/.data
|
||||||
|
# Optional: mount your OpenClaw state directory read-only so Mission Control
|
||||||
|
# can read agent configs and memory. Uncomment and adjust the host path:
|
||||||
|
# - ${OPENCLAW_HOME:-~/.openclaw}:/run/openclaw:ro
|
||||||
|
# Allow the container to reach an OpenClaw gateway running on the Docker host.
|
||||||
|
# Uses the special host-gateway alias available on Docker 20.10+.
|
||||||
|
# If your gateway runs in another container on this network, remove this line
|
||||||
|
# and set OPENCLAW_GATEWAY_HOST to the container name instead.
|
||||||
|
extra_hosts:
|
||||||
|
- "host-gateway:host-gateway"
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /tmp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Screenshot Guide
|
||||||
|
|
||||||
|
This document explains how to capture and update the README screenshots so they stay in sync with the UI.
|
||||||
|
|
||||||
|
## Screenshots in the README
|
||||||
|
|
||||||
|
| File | Section | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `docs/mission-control-overview.png` | Dashboard Overview | Main dashboard view |
|
||||||
|
| `docs/mission-control-agents.png` | Agents Panel | Active agents list |
|
||||||
|
| `docs/mission-control-memory-graph.png` | Memory Graph | Agent memory graph |
|
||||||
|
|
||||||
|
## When to Refresh
|
||||||
|
|
||||||
|
Screenshots should be updated when:
|
||||||
|
|
||||||
|
- A new page, panel, or major UI component is added
|
||||||
|
- An existing page layout changes noticeably
|
||||||
|
- The color scheme or branding updates
|
||||||
|
- A GitHub Actions `screenshot-drift` label is applied to a PR (see [automation](#automation))
|
||||||
|
|
||||||
|
## How to Take New Screenshots
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Mission Control running locally (`pnpm dev` or Docker)
|
||||||
|
- Browser with at least 1440×900 viewport recommended
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Start the app** (with some sample data for a realistic view):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Seed sample data** (optional but recommended for non-empty screenshots):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm seed # if a seed script exists, otherwise populate via UI
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Navigate to each page** and take a screenshot:
|
||||||
|
|
||||||
|
| Screenshot | URL | Notes |
|
||||||
|
|-----------|-----|-------|
|
||||||
|
| `mission-control-overview.png` | `/` | Main dashboard, full page |
|
||||||
|
| `mission-control-agents.png` | `/agents` | Agents panel open |
|
||||||
|
| `mission-control-memory-graph.png` | `/memory` | Memory graph with nodes |
|
||||||
|
|
||||||
|
4. **Crop and optimise** to reduce file size:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
pngcrush -reduce -brute input.png output.png
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
optipng -o5 input.png
|
||||||
|
# or
|
||||||
|
pngquant --quality=80-95 --output output.png input.png
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Replace the files** under `docs/` and commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/Downloads/dashboard.png docs/mission-control-overview.png
|
||||||
|
git add docs/
|
||||||
|
git commit -m "docs: refresh README screenshots"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automation
|
||||||
|
|
||||||
|
The repository has a GitHub Actions workflow (`.github/workflows/screenshot-drift.yml`) that:
|
||||||
|
|
||||||
|
- Detects changes to files under `src/app/`, `src/components/`, and `public/`
|
||||||
|
- Adds a `screenshot-drift` label to the PR as a reminder
|
||||||
|
- Posts a checklist comment listing which screenshots may need updating
|
||||||
|
|
||||||
|
This does **not** auto-capture screenshots — it just flags the PR so a human can decide whether the change is visually significant enough to warrant a refresh.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Use a consistent browser zoom level (100%) and window size
|
||||||
|
- Hide bookmarks bar and dev tools before capturing
|
||||||
|
- Light mode and dark mode screenshots can coexist — add a `*-dark.png` variant if useful
|
||||||
|
- Prefer PNG for UI screenshots (lossless); JPEG for photos/illustrations
|
||||||
38
install.sh
38
install.sh
|
|
@ -158,6 +158,44 @@ setup_env() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auto-detect and write OpenClaw home directory into .env
|
||||||
|
local oc_home="${OPENCLAW_HOME:-$HOME/.openclaw}"
|
||||||
|
if [[ -d "$oc_home" ]]; then
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
sed -i '' "s|^OPENCLAW_HOME=.*|OPENCLAW_HOME=$oc_home|" "$INSTALL_DIR/.env"
|
||||||
|
else
|
||||||
|
sed -i "s|^OPENCLAW_HOME=.*|OPENCLAW_HOME=$oc_home|" "$INSTALL_DIR/.env"
|
||||||
|
fi
|
||||||
|
info "Set OPENCLAW_HOME=$oc_home in .env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# In Docker mode, the gateway runs on the host, not inside the container.
|
||||||
|
# Set OPENCLAW_GATEWAY_HOST to the Docker host gateway IP so the container
|
||||||
|
# can reach the gateway. Users may override this with the gateway container
|
||||||
|
# name if running OpenClaw in a container on the same network.
|
||||||
|
if [[ "$DEPLOY_MODE" == "docker" ]]; then
|
||||||
|
local gw_host="${OPENCLAW_GATEWAY_HOST:-}"
|
||||||
|
if [[ -z "$gw_host" ]]; then
|
||||||
|
# Detect Docker host IP (host-gateway alias or default bridge)
|
||||||
|
if getent hosts host-gateway &>/dev/null 2>&1; then
|
||||||
|
gw_host="host-gateway"
|
||||||
|
else
|
||||||
|
# Fallback: use the default Docker bridge gateway (172.17.0.1)
|
||||||
|
gw_host=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}' || echo "172.17.0.1")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -n "$gw_host" && "$gw_host" != "127.0.0.1" ]]; then
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
sed -i '' "s|^OPENCLAW_GATEWAY_HOST=.*|OPENCLAW_GATEWAY_HOST=$gw_host|" "$INSTALL_DIR/.env"
|
||||||
|
else
|
||||||
|
sed -i "s|^OPENCLAW_GATEWAY_HOST=.*|OPENCLAW_GATEWAY_HOST=$gw_host|" "$INSTALL_DIR/.env"
|
||||||
|
fi
|
||||||
|
info "Set OPENCLAW_GATEWAY_HOST=$gw_host in .env (Docker host IP)"
|
||||||
|
info " If your gateway runs in a Docker container, update OPENCLAW_GATEWAY_HOST"
|
||||||
|
info " to the container name and add it to the mc-net network."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
ok "Secure .env generated"
|
ok "Secure .env generated"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,22 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { requireRole } from '@/lib/auth'
|
import { requireRole } from '@/lib/auth'
|
||||||
import { config } from '@/lib/config'
|
import { config } from '@/lib/config'
|
||||||
import { logger } from '@/lib/logger'
|
import { logger } from '@/lib/logger'
|
||||||
|
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
||||||
|
|
||||||
const GATEWAY_TIMEOUT = 5000
|
const GATEWAY_TIMEOUT = 5000
|
||||||
|
|
||||||
function gatewayUrl(path: string): string {
|
/** Probe the gateway HTTP /health endpoint to check reachability. */
|
||||||
return `http://${config.gatewayHost}:${config.gatewayPort}${path}`
|
async function isGatewayReachable(): Promise<boolean> {
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchGateway(path: string, init?: RequestInit): Promise<Response> {
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT)
|
const timeout = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT)
|
||||||
try {
|
try {
|
||||||
return await fetch(gatewayUrl(path), {
|
const res = await fetch(
|
||||||
...init,
|
`http://${config.gatewayHost}:${config.gatewayPort}/health`,
|
||||||
signal: controller.signal,
|
{ signal: controller.signal },
|
||||||
})
|
)
|
||||||
|
return res.ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
|
|
@ -30,28 +31,44 @@ export async function GET(request: NextRequest) {
|
||||||
|
|
||||||
if (action === 'list') {
|
if (action === 'list') {
|
||||||
try {
|
try {
|
||||||
const res = await fetchGateway('/api/presence')
|
const connected = await isGatewayReachable()
|
||||||
if (!res.ok) {
|
if (!connected) {
|
||||||
logger.warn({ status: res.status }, 'Gateway presence endpoint returned non-OK')
|
|
||||||
return NextResponse.json({ nodes: [], connected: false })
|
return NextResponse.json({ nodes: [], connected: false })
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
|
||||||
return NextResponse.json(data)
|
try {
|
||||||
|
const data = await callOpenClawGateway<{ nodes?: unknown[] }>('node.list', {}, GATEWAY_TIMEOUT)
|
||||||
|
return NextResponse.json({ nodes: data?.nodes ?? [], connected: true })
|
||||||
|
} catch (rpcErr) {
|
||||||
|
// Gateway is reachable but openclaw CLI unavailable (e.g. Docker) or
|
||||||
|
// node.list not supported — return connected=true with empty node list
|
||||||
|
logger.warn({ err: rpcErr }, 'node.list RPC failed, returning empty node list')
|
||||||
|
return NextResponse.json({ nodes: [], connected: true })
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn({ err }, 'Gateway unreachable for presence listing')
|
logger.warn({ err }, 'Gateway unreachable for node listing')
|
||||||
return NextResponse.json({ nodes: [], connected: false })
|
return NextResponse.json({ nodes: [], connected: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'devices') {
|
if (action === 'devices') {
|
||||||
try {
|
try {
|
||||||
const res = await fetchGateway('/api/devices')
|
const connected = await isGatewayReachable()
|
||||||
if (!res.ok) {
|
if (!connected) {
|
||||||
logger.warn({ status: res.status }, 'Gateway devices endpoint returned non-OK')
|
return NextResponse.json({ devices: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await callOpenClawGateway<{ devices?: unknown[] }>(
|
||||||
|
'device.pair.list',
|
||||||
|
{},
|
||||||
|
GATEWAY_TIMEOUT,
|
||||||
|
)
|
||||||
|
return NextResponse.json({ devices: data?.devices ?? [] })
|
||||||
|
} catch (rpcErr) {
|
||||||
|
logger.warn({ err: rpcErr }, 'device.pair.list RPC failed, returning empty device list')
|
||||||
return NextResponse.json({ devices: [] })
|
return NextResponse.json({ devices: [] })
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn({ err }, 'Gateway unreachable for device listing')
|
logger.warn({ err }, 'Gateway unreachable for device listing')
|
||||||
return NextResponse.json({ devices: [] })
|
return NextResponse.json({ devices: [] })
|
||||||
|
|
@ -64,6 +81,7 @@ export async function GET(request: NextRequest) {
|
||||||
const VALID_DEVICE_ACTIONS = ['approve', 'reject', 'rotate-token', 'revoke-token'] as const
|
const VALID_DEVICE_ACTIONS = ['approve', 'reject', 'rotate-token', 'revoke-token'] as const
|
||||||
type DeviceAction = (typeof VALID_DEVICE_ACTIONS)[number]
|
type DeviceAction = (typeof VALID_DEVICE_ACTIONS)[number]
|
||||||
|
|
||||||
|
/** Map UI action names to gateway RPC method names and their required param keys. */
|
||||||
const ACTION_RPC_MAP: Record<DeviceAction, { method: string; paramKey: 'requestId' | 'deviceId' }> = {
|
const ACTION_RPC_MAP: Record<DeviceAction, { method: string; paramKey: 'requestId' | 'deviceId' }> = {
|
||||||
'approve': { method: 'device.pair.approve', paramKey: 'requestId' },
|
'approve': { method: 'device.pair.approve', paramKey: 'requestId' },
|
||||||
'reject': { method: 'device.pair.reject', paramKey: 'requestId' },
|
'reject': { method: 'device.pair.reject', paramKey: 'requestId' },
|
||||||
|
|
@ -112,21 +130,10 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetchGateway('/api/rpc', {
|
const result = await callOpenClawGateway(spec.method, params, GATEWAY_TIMEOUT)
|
||||||
method: 'POST',
|
return NextResponse.json(result)
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ method: spec.method, params }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
return NextResponse.json(data, { status: res.status })
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const name = err instanceof Error ? err.name : ''
|
|
||||||
if (name === 'AbortError') {
|
|
||||||
logger.error('Gateway device action request timed out')
|
|
||||||
return NextResponse.json({ error: 'Gateway request timed out' }, { status: 504 })
|
|
||||||
}
|
|
||||||
logger.error({ err }, 'Gateway device action failed')
|
logger.error({ err }, 'Gateway device action failed')
|
||||||
return NextResponse.json({ error: 'Gateway unreachable' }, { status: 502 })
|
return NextResponse.json({ error: 'Gateway device action failed' }, { status: 502 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,18 @@ export function registerMcAsDashboard(mcUrl: string): { registered: boolean; alr
|
||||||
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n')
|
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n')
|
||||||
logger.info({ origin }, 'Registered MC origin in gateway config')
|
logger.info({ origin }, 'Registered MC origin in gateway config')
|
||||||
return { registered: true, alreadySet: false }
|
return { registered: true, alreadySet: false }
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
|
// Read-only filesystem (e.g. Docker read_only: true, or intentional mount) —
|
||||||
|
// treat as a non-fatal skip rather than an error.
|
||||||
|
if (err?.code === 'EROFS' || err?.code === 'EACCES' || err?.code === 'EPERM') {
|
||||||
|
logger.warn(
|
||||||
|
{ err, configPath },
|
||||||
|
'Gateway config is read-only — skipping MC origin registration. ' +
|
||||||
|
'To enable auto-registration, mount openclaw.json with write access or ' +
|
||||||
|
'add the MC origin to gateway.controlUi.allowedOrigins manually.',
|
||||||
|
)
|
||||||
|
return { registered: false, alreadySet: false }
|
||||||
|
}
|
||||||
logger.error({ err }, 'Failed to register MC in gateway config')
|
logger.error({ err }, 'Failed to register MC in gateway config')
|
||||||
return { registered: false, alreadySet: false }
|
return { registered: false, alreadySet: false }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue