feat: schedules UI, task cron type, monthly/yearly schedules, AE DB tools, integrations page
- Schedules web UI (/settings/crons): list, add, edit, pause/resume, delete jobs - cron task type: full orchestrator tool loop on a schedule, result → notification channel - parse_schedule: monthly/yearly formats (monthly:DD:HH:MM, yearly:MM:DD:HH:MM) - HA inbound webhook tools toggle: orchestrator loop vs. direct LLM, configurable in UI - ae_db_query/describe/show_view: SELECT-only Aether MariaDB access (admin, per-user creds) - /settings/integrations: admin-only page for Aether DB credentials - Schedules nav link added to all settings pages - pymysql added to requirements - Docs updated: HELP.md, MASTER.md, CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
and are appended automatically by help.html when present.
|
||||
-->
|
||||
|
||||
*Last updated: 2026-05-12*
|
||||
*Last updated: 2026-05-13*
|
||||
|
||||
---
|
||||
|
||||
@@ -82,7 +82,7 @@ Orchestrated sessions persist to history exactly like regular chat.
|
||||
|
||||
### Available Tools
|
||||
|
||||
62 tools across 16 categories. Each tool schema is sent to the model on every orchestrated call — fewer active tools means fewer tokens per call.
|
||||
65 tools across 17 categories. Each tool schema is sent to the model on every orchestrated call — fewer active tools means fewer tokens per call.
|
||||
|
||||
| Category | Tools |
|
||||
|---|---|
|
||||
@@ -99,13 +99,15 @@ Orchestrated sessions persist to history exactly like regular chat.
|
||||
| **Notifications** | `web_push`, `email_send`, `nc_talk_send`, `nc_talk_history` |
|
||||
| **Aether Journals** | `ae_journal_list/search`, `ae_journal_entries_list`, `ae_journal_entry_read/create/update/disable/append/prepend` |
|
||||
| **Aether Tasks** | `ae_task_list` |
|
||||
| **Aether Database** (admin) | `ae_db_query`, `ae_db_describe`, `ae_db_show_view` |
|
||||
| **Agent Notes** | `agent_notes_read`, `agent_notes_write`, `agent_notes_append`, `agent_notes_clear` |
|
||||
| **Agents** | `spawn_agent` |
|
||||
| **Home Assistant** | `ha_get_state`, `ha_get_states`, `ha_call_service` |
|
||||
|
||||
Files, Shell, System, Agents, and some Notification/Web tools are **admin-only** and not visible to regular users.
|
||||
Files, Shell, System, Aether Database, Agents, and some Notification/Web tools are **admin-only** and not visible to regular users.
|
||||
`http_post` requires a URL prefix allowlist in `home/{user}/http_allowlist.json`.
|
||||
`nc_talk_history` requires `nc_username` and `nc_app_password` in `channels.json` under `nextcloud`.
|
||||
`ae_db_*` tools require Aether DB credentials configured in **Integrations** settings. All queries are SELECT-only — no writes possible.
|
||||
|
||||
### Per-Role Tool Sets
|
||||
|
||||
@@ -175,7 +177,8 @@ Each response shows a **model tag** (bottom-right of message) with the model lab
|
||||
| **Account** | View your username, role badge (Admin / User), rename your username |
|
||||
| **Connected Accounts** | See which Google account is linked for OAuth sign-in |
|
||||
| **Email Allowlist** | Regex patterns controlling which addresses the `email_send` tool can reach |
|
||||
| **Notifications** | Dedicated page — set channel (Browser Push, NC Talk, Google Chat, email) for proactive messages; test buttons for instant verification |
|
||||
| **Notifications** | Dedicated page — set channel (Browser Push, NC Talk, Google Chat, email) for proactive messages; configure Home Assistant inbound webhook; test buttons for instant verification |
|
||||
| **Schedules** | View, add, edit, pause, and delete scheduled jobs directly — without going through the AI |
|
||||
| **Tool Permissions** | Allow or block specific orchestrator tools for your account |
|
||||
| **Usage** | Token consumption by model — see below |
|
||||
| **Browser Cache** | Clear UI preferences stored locally (theme, font size, session ID, etc.) |
|
||||
@@ -382,6 +385,53 @@ Distillation builds up the memory layers from raw session logs. Runs automatical
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Jobs
|
||||
|
||||
Cortex can run recurring jobs on a schedule — reminders, daily briefings, automated research, and more. Manage them by asking Inara to set them up, or go directly to **☰ → Account → Schedules**.
|
||||
|
||||
### Job Types
|
||||
|
||||
| Type | What it does |
|
||||
|---|---|
|
||||
| `remind` | Appends to `REMINDERS.md` — automatically surfaced in chat context |
|
||||
| `note` | Appends to `SCRATCH.md` — read on demand via the scratchpad |
|
||||
| `message` | Sends the payload text directly to your notification channel |
|
||||
| `brief` | Calls the AI with your payload as the prompt, sends the response to your notification channel. Good for morning briefings, check-ins. |
|
||||
| `task` | Runs the full orchestrator tool loop with your payload as the request, sends Claude's response to your notification channel. Use this for agentic scheduled work: research, file updates, summaries that need tool access. |
|
||||
|
||||
For `task` jobs: tools that require confirmation are skipped in scheduled context. Pre-approve them in **Settings → Tools** to allow them in scheduled tasks.
|
||||
|
||||
### Schedule Formats
|
||||
|
||||
| Format | When it runs |
|
||||
|---|---|
|
||||
| `hourly` | Every hour at :00 |
|
||||
| `daily` | Every day at 09:00 |
|
||||
| `daily:HH:MM` | Every day at the specified time |
|
||||
| `weekly:DOW` | Every specified day at 09:00 (e.g. `weekly:mon`) |
|
||||
| `weekly:DOW:HH:MM` | Every specified day at the specified time (e.g. `weekly:fri:17:00`) |
|
||||
| `monthly` | 1st of every month at 09:00 |
|
||||
| `monthly:DD` | Specific day of month at 09:00 (e.g. `monthly:15`) |
|
||||
| `monthly:DD:HH:MM` | Specific day of month at the specified time |
|
||||
| `yearly:MM:DD` | Every year on that date at 09:00 — for birthdays, anniversaries (e.g. `yearly:03:15`) |
|
||||
| `yearly:MM:DD:HH:MM` | Every year on that date at the specified time |
|
||||
|
||||
DOW values: `mon tue wed thu fri sat sun`. All times are server-local.
|
||||
|
||||
Schedules take effect immediately when added or edited — no restart needed. Paused jobs stay in the list and can be resumed at any time.
|
||||
|
||||
### Home Assistant Integration
|
||||
|
||||
HA automations can trigger Inara via webhook. Configure in **Notifications → Home Assistant → Inbound webhook**:
|
||||
|
||||
- Set a **Webhook ID** (long random string — this is your secret URL component)
|
||||
- Your endpoint: `https://cortex.dgrzone.com/webhook/ha/{username}/{webhook_id}`
|
||||
- **Enable orchestrator tools** — when checked, HA events trigger the full tool loop; when unchecked, events get a direct LLM response (faster, no tools)
|
||||
|
||||
HA payload fields recognized: `message`, `entity_id`, `state`, `trigger`, `event`, `area`.
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Keys | Action |
|
||||
|
||||
150
cortex/static/crons.html
Normal file
150
cortex/static/crons.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cortex — Schedules</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/pg.css">
|
||||
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
|
||||
<style>
|
||||
.cron-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 0.82rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.cron-table th {
|
||||
text-align: left; padding: 0.4rem 0.6rem;
|
||||
border-bottom: 2px solid var(--pg-border);
|
||||
color: var(--pg-muted); font-weight: 600; font-size: 0.75rem;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
}
|
||||
.cron-table td {
|
||||
padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--pg-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cron-table tr:last-child td { border-bottom: none; }
|
||||
.cron-table tr:hover td { background: var(--pg-hover); }
|
||||
|
||||
.badge {
|
||||
display: inline-block; padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px; font-size: 0.72rem; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.03em;
|
||||
}
|
||||
.badge-enabled { background: color-mix(in srgb, var(--pg-accent) 18%, transparent); color: var(--pg-accent); }
|
||||
.badge-paused { background: color-mix(in srgb, var(--pg-muted) 18%, transparent); color: var(--pg-muted); }
|
||||
.badge-remind { background: color-mix(in srgb, #a78bfa 15%, transparent); color: #a78bfa; }
|
||||
.badge-note { background: color-mix(in srgb, #60a5fa 15%, transparent); color: #60a5fa; }
|
||||
.badge-message { background: color-mix(in srgb, #34d399 15%, transparent); color: #34d399; }
|
||||
.badge-brief { background: color-mix(in srgb, #fb923c 15%, transparent); color: #fb923c; }
|
||||
.badge-task { background: color-mix(in srgb, #f472b6 15%, transparent); color: #f472b6; }
|
||||
|
||||
.cron-actions { display: flex; gap: 0.35rem; }
|
||||
.btn-cron {
|
||||
padding: 0.2rem 0.55rem; border-radius: 4px; border: 1px solid var(--pg-border);
|
||||
background: transparent; color: var(--pg-muted); font-size: 0.75rem; cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-cron:hover { border-color: var(--pg-accent); color: var(--pg-accent); }
|
||||
.btn-cron-del:hover { border-color: var(--pg-danger, #ef4444); color: var(--pg-danger, #ef4444); }
|
||||
.btn-cron-del { color: var(--pg-dimmer); }
|
||||
|
||||
.payload-cell {
|
||||
max-width: 240px; overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; color: var(--pg-dimmer);
|
||||
}
|
||||
|
||||
.persona-group { margin-bottom: 0.25rem; }
|
||||
.persona-group-label {
|
||||
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.06em; color: var(--pg-dimmer); margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center; padding: 2rem 1rem;
|
||||
color: var(--pg-dimmer); font-size: 0.85rem;
|
||||
border: 1px dashed var(--pg-border); border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.add-form-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 0 0.75rem;
|
||||
}
|
||||
.add-form-grid .field-full { grid-column: 1 / -1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="page-nav">
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link active">Schedules</a>
|
||||
{{ integrations_nav }}
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
<div class="page-wrap">
|
||||
<h1 class="page-title">Schedules</h1>
|
||||
<p class="page-subtitle">Recurring jobs — reminders, notes, briefings, and agentic tasks.</p>
|
||||
|
||||
<!-- SUCCESS -->
|
||||
<!-- ERROR -->
|
||||
|
||||
<!-- Edit form (shown only when editing) -->
|
||||
{{ edit_html }}
|
||||
|
||||
<!-- Cron list -->
|
||||
{{ cron_list_html }}
|
||||
|
||||
<!-- Add new cron -->
|
||||
<div class="section">
|
||||
<h2>Add schedule</h2>
|
||||
<form method="POST" action="/settings/crons/add">
|
||||
<div class="add-form-grid">
|
||||
<div class="field">
|
||||
<label for="add_persona">Persona</label>
|
||||
<select id="add_persona" name="persona">
|
||||
{{ persona_options }}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="add_job_type">Type</label>
|
||||
<select id="add_job_type" name="job_type">
|
||||
<option value="remind">remind — append to REMINDERS.md</option>
|
||||
<option value="note">note — append to SCRATCH.md</option>
|
||||
<option value="message">message — send payload as-is</option>
|
||||
<option value="brief">brief — LLM response, no tools</option>
|
||||
<option value="task">task — full orchestrator tool loop</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="add_label">Label</label>
|
||||
<input type="text" id="add_label" name="label"
|
||||
placeholder="Monday morning summary"
|
||||
required autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="add_schedule">Schedule</label>
|
||||
<input type="text" id="add_schedule" name="schedule"
|
||||
placeholder="weekly:mon:08:00"
|
||||
required autocomplete="off" spellcheck="false">
|
||||
<p class="hint">
|
||||
hourly · daily · daily:HH:MM · weekly:DOW · weekly:DOW:HH:MM ·
|
||||
monthly · monthly:DD · monthly:DD:HH:MM · yearly:MM:DD · yearly:MM:DD:HH:MM
|
||||
</p>
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label for="add_payload">Payload / prompt</label>
|
||||
<textarea id="add_payload" name="payload" rows="3"
|
||||
placeholder="Check my open tasks and send a summary." required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Add schedule</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -94,6 +94,8 @@
|
||||
<a href="/settings" class="nav-link" id="nav-settings">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
{{ integrations_nav }}
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
|
||||
124
cortex/static/integrations.html
Normal file
124
cortex/static/integrations.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cortex — Integrations</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/pg.css">
|
||||
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
|
||||
<style>
|
||||
details.channel-block {
|
||||
border: 1px solid var(--pg-border); border-radius: 8px;
|
||||
margin-bottom: 0.75rem; overflow: hidden;
|
||||
}
|
||||
details.channel-block summary {
|
||||
padding: 0.75rem 1rem; font-size: 0.85rem; font-weight: 600;
|
||||
color: var(--pg-muted); cursor: pointer; list-style: none;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
user-select: none; background: var(--pg-bg);
|
||||
}
|
||||
details.channel-block summary::-webkit-details-marker { display: none; }
|
||||
details.channel-block summary::before {
|
||||
content: '▶'; font-size: 0.65rem; color: var(--pg-dimmer);
|
||||
transition: transform 0.15s; flex-shrink: 0;
|
||||
}
|
||||
details.channel-block[open] summary::before { transform: rotate(90deg); }
|
||||
details.channel-block[open] summary { border-bottom: 1px solid var(--pg-border); }
|
||||
.channel-block-body { padding: 1rem 1rem 0.25rem; }
|
||||
.channel-hint {
|
||||
font-size: 0.75rem; color: var(--pg-dimmer);
|
||||
margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
|
||||
}
|
||||
.field-row {
|
||||
display: grid; grid-template-columns: 1fr auto; gap: 0.75rem; align-items: end;
|
||||
}
|
||||
.field-row .field { margin-bottom: 0; }
|
||||
.field-narrow input { max-width: 120px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="page-nav">
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
<a href="/settings/integrations" class="nav-link active">Integrations</a>
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
<div class="page-wrap">
|
||||
<h1 class="page-title">Integrations</h1>
|
||||
<p class="page-subtitle">External service connections — admin only.</p>
|
||||
|
||||
<!-- SUCCESS -->
|
||||
<!-- ERROR -->
|
||||
|
||||
<form method="POST" action="/settings/integrations">
|
||||
|
||||
<!-- Aether Platform Database -->
|
||||
<div class="section">
|
||||
<h2>Aether Platform Database</h2>
|
||||
<p class="section-note">
|
||||
Gives the orchestrator direct read-only access to the Aether MariaDB via the
|
||||
<code>ae_db_query</code>, <code>ae_db_describe</code>, and <code>ae_db_show_view</code> tools.
|
||||
Only SELECT, SHOW, DESCRIBE, and EXPLAIN are permitted — no writes possible.
|
||||
</p>
|
||||
|
||||
<details class="channel-block" {{ ae_db_host and 'open' or '' }}>
|
||||
<summary>Connection</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
Use the same credentials as <code>agents_sync/mcp/scripts/sql_inspector.py</code>.
|
||||
The password field is left blank in the form — leave it blank to keep the stored value.
|
||||
</p>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="ae_db_host">Host</label>
|
||||
<input type="text" id="ae_db_host" name="ae_db_host"
|
||||
value="{{ ae_db_host }}"
|
||||
placeholder="192.168.64.5"
|
||||
autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="field field-narrow">
|
||||
<label for="ae_db_port">Port</label>
|
||||
<input type="number" id="ae_db_port" name="ae_db_port"
|
||||
value="{{ ae_db_port }}"
|
||||
placeholder="3306" min="1" max="65535"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ae_db_name">Database name</label>
|
||||
<input type="text" id="ae_db_name" name="ae_db_name"
|
||||
value="{{ ae_db_name }}"
|
||||
placeholder="aether_dev"
|
||||
autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ae_db_user">Username</label>
|
||||
<input type="text" id="ae_db_user" name="ae_db_user"
|
||||
value="{{ ae_db_user }}"
|
||||
placeholder="aether_dev"
|
||||
autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ae_db_password">Password</label>
|
||||
<input type="password" id="ae_db_password" name="ae_db_password"
|
||||
value=""
|
||||
placeholder="Leave blank to keep existing value"
|
||||
autocomplete="new-password" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">Save integrations</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -60,6 +60,8 @@
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link active">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
{{ integrations_nav }}
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
@@ -214,6 +216,13 @@
|
||||
autocomplete="off" spellcheck="false">
|
||||
<p class="hint">Treat this like a password — use a long, random string.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="ha_tools" value="1" {{ ha_tools_checked }}>
|
||||
Enable orchestrator tools
|
||||
</label>
|
||||
<p class="hint">When checked, HA events trigger the full tool loop (research, home control, tasks). When unchecked, events get a direct LLM response — faster but no tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
<a href="/settings" class="nav-link active">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
{{ integrations_nav }}
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
|
||||
@@ -107,6 +107,8 @@
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link active">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
{{ integrations_nav }}
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user