mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 01:35:36 -04:00
feat(reminders): add generic webhook as a fourth reminder channel (#2952)
Replaces any Discord-specific reminder channel with a generic outbound
webhook channel. Users pick any saved Integration as the target and
supply a JSON payload template with {{title}} and {{message}}
placeholders — values are JSON-escaped before substitution. Works with
Discord, Slack, Teams, ntfy (JSON mode), or any service that accepts a
POST with a JSON body.
- `src/settings.py` — reminder_webhook_integration_id +
reminder_webhook_payload_template defaults
- `routes/note_routes.py` — webhook delivery block; Integration lookup,
template rendering, auth wiring; built-in preset defaults so
discord_webhook works out of the box without a configured template;
settings_override kwarg avoids test-button race condition
- `routes/auth_routes.py` — discord_webhook preset test handler
- `src/integrations.py` — discord_webhook preset with description +
example templates; hides auth/key fields in the Integration form
- `src/builtin_actions.py` — webhook_sent delivery check
- `src/tool_implementations.py` — webhook aliases + enum updated
- `static/index.html` — Webhook channel option; Integration picker +
payload template textarea
- `static/js/settings.js` — Integration list, populateWebhookIntegrations,
syncChannelRows, hints, load/save, auto-fill preset templates,
test-button override payload, hide auth/key for URL-auth presets
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -585,6 +585,27 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
|||||||
hint = " If this is Docker Compose ntfy, set NTFY_BIND to that host/Tailscale IP and NTFY_BASE_URL to the same server URL in .env, then recreate ntfy."
|
hint = " If this is Docker Compose ntfy, set NTFY_BIND to that host/Tailscale IP and NTFY_BASE_URL to the same server URL in .env, then recreate ntfy."
|
||||||
return {"ok": False, "message": f"ntfy publish to {full_url} failed: {e}.{hint}"[:500]}
|
return {"ok": False, "message": f"ntfy publish to {full_url} failed: {e}.{hint}"[:500]}
|
||||||
|
|
||||||
|
if preset == "discord_webhook":
|
||||||
|
import httpx
|
||||||
|
webhook_url = (integ.get("base_url") or "").strip()
|
||||||
|
if not webhook_url:
|
||||||
|
return {"ok": False, "message": "No webhook URL set — paste the full Discord webhook URL into the Base URL field."}
|
||||||
|
payload = {
|
||||||
|
"embeds": [{
|
||||||
|
"title": "Odysseus connectivity test",
|
||||||
|
"description": "If you see this, your Discord Webhook integration is wired up correctly.",
|
||||||
|
"color": 5793266,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
||||||
|
r = await client.post(webhook_url, json=payload)
|
||||||
|
if r.is_success:
|
||||||
|
return {"ok": True, "message": "Test embed sent — check your Discord channel to confirm it arrived."}
|
||||||
|
return {"ok": False, "message": f"Discord returned HTTP {r.status_code}: {r.text[:200]}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "message": f"Request failed: {e}"[:400]}
|
||||||
|
|
||||||
# All other presets: GET against a known health endpoint.
|
# All other presets: GET against a known health endpoint.
|
||||||
# Fall back to detecting from name if preset is missing.
|
# Fall back to detecting from name if preset is missing.
|
||||||
health_paths = {
|
health_paths = {
|
||||||
|
|||||||
+91
-7
@@ -114,8 +114,9 @@ async def dispatch_reminder(
|
|||||||
note_id: str,
|
note_id: str,
|
||||||
owner: str = "",
|
owner: str = "",
|
||||||
queue_browser: bool = True,
|
queue_browser: bool = True,
|
||||||
|
settings_override: dict | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Fire a reminder via the configured channel (browser/email/ntfy).
|
"""Fire a reminder via the configured channel (browser/email/ntfy/webhook).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: short headline shown to the user
|
title: short headline shown to the user
|
||||||
@@ -129,7 +130,7 @@ async def dispatch_reminder(
|
|||||||
nothing is "sent" synchronously for it — the channel just routes there.
|
nothing is "sent" synchronously for it — the channel just routes there.
|
||||||
"""
|
"""
|
||||||
from src.settings import load_settings
|
from src.settings import load_settings
|
||||||
settings = load_settings()
|
settings = {**load_settings(), **(settings_override or {})}
|
||||||
channel = settings.get("reminder_channel", "browser")
|
channel = settings.get("reminder_channel", "browser")
|
||||||
llm_on = bool(settings.get("reminder_llm_synthesis", False))
|
llm_on = bool(settings.get("reminder_llm_synthesis", False))
|
||||||
title = (title or "").strip()
|
title = (title or "").strip()
|
||||||
@@ -160,13 +161,14 @@ async def dispatch_reminder(
|
|||||||
# Treat those as browser-only dedupe so email reminders can be
|
# Treat those as browser-only dedupe so email reminders can be
|
||||||
# retried by the backend scanner after a failed frontend path.
|
# retried by the backend scanner after a failed frontend path.
|
||||||
should_skip = last_dt >= _dt.now(_tz.utc) - _td(minutes=25)
|
should_skip = last_dt >= _dt.now(_tz.utc) - _td(minutes=25)
|
||||||
if should_skip and channel in ("email", "ntfy"):
|
if should_skip and channel in ("email", "ntfy", "webhook"):
|
||||||
should_skip = last_channel == channel
|
should_skip = last_channel == channel
|
||||||
if should_skip:
|
if should_skip:
|
||||||
return {
|
return {
|
||||||
"synthesis": None,
|
"synthesis": None,
|
||||||
"email_sent": False,
|
"email_sent": False,
|
||||||
"ntfy_sent": False,
|
"ntfy_sent": False,
|
||||||
|
"webhook_sent": False,
|
||||||
"browser_sent": True,
|
"browser_sent": True,
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
}
|
}
|
||||||
@@ -360,6 +362,76 @@ async def dispatch_reminder(
|
|||||||
email_error = str(e) or e.__class__.__name__
|
email_error = str(e) or e.__class__.__name__
|
||||||
logger.warning(f"Reminder email send failed: {e}")
|
logger.warning(f"Reminder email send failed: {e}")
|
||||||
|
|
||||||
|
webhook_sent = False
|
||||||
|
webhook_error = ""
|
||||||
|
if channel == "webhook":
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
import json as _wjson
|
||||||
|
from src.integrations import load_integrations
|
||||||
|
# Built-in payload defaults for known presets so users don't have
|
||||||
|
# to configure a template just to use a standard service.
|
||||||
|
_PRESET_TEMPLATE_DEFAULTS = {
|
||||||
|
"discord_webhook": '{"embeds": [{"title": "{{title}}", "description": "{{message}}", "color": 5793266}]}',
|
||||||
|
}
|
||||||
|
intg_id = settings.get("reminder_webhook_integration_id", "").strip()
|
||||||
|
template = settings.get("reminder_webhook_payload_template", "").strip()
|
||||||
|
if not intg_id:
|
||||||
|
webhook_error = "No webhook integration selected"
|
||||||
|
else:
|
||||||
|
intg = next(
|
||||||
|
(i for i in load_integrations()
|
||||||
|
if i.get("id") == intg_id and i.get("base_url")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not intg:
|
||||||
|
webhook_error = f"Integration {intg_id!r} not found or missing base URL"
|
||||||
|
else:
|
||||||
|
# Fall back to a built-in default for known presets so
|
||||||
|
# users don't have to configure a template for standard
|
||||||
|
# services like Discord.
|
||||||
|
if not template:
|
||||||
|
template = _PRESET_TEMPLATE_DEFAULTS.get(intg.get("preset", ""), "")
|
||||||
|
if not template:
|
||||||
|
webhook_error = "No payload template configured"
|
||||||
|
else:
|
||||||
|
# Render template: JSON-escape the values so the result
|
||||||
|
# is always valid JSON regardless of special characters.
|
||||||
|
# dumps() returns `"value"` — strip outer quotes.
|
||||||
|
msg = (synthesis or note_body or title or "Reminder")[:4000]
|
||||||
|
_t = _wjson.dumps(title or "Reminder")[1:-1]
|
||||||
|
_m = _wjson.dumps(msg)[1:-1]
|
||||||
|
rendered = template.replace("{{title}}", _t).replace("{{message}}", _m)
|
||||||
|
hdrs = {"Content-Type": "application/json"}
|
||||||
|
api_key = intg.get("api_key", "")
|
||||||
|
auth_type = (intg.get("auth_type") or "none").lower()
|
||||||
|
if api_key:
|
||||||
|
if auth_type == "bearer":
|
||||||
|
hdrs["Authorization"] = f"Bearer {api_key}"
|
||||||
|
elif auth_type == "header":
|
||||||
|
hdrs[intg.get("auth_header") or "Authorization"] = api_key
|
||||||
|
url = intg["base_url"].rstrip("/")
|
||||||
|
# SSRF guard — matches the pattern used by webhook_routes,
|
||||||
|
# CalDAV, search, and embeddings. Blocks link-local / metadata
|
||||||
|
# addresses (169.254.x.x) by default; set
|
||||||
|
# REMINDER_WEBHOOK_BLOCK_PRIVATE_IPS=true to also block
|
||||||
|
# RFC-1918 ranges for locked-down deployments.
|
||||||
|
import os as _os
|
||||||
|
from src.url_safety import check_outbound_url as _chk
|
||||||
|
_block = _os.getenv("REMINDER_WEBHOOK_BLOCK_PRIVATE_IPS", "false").lower() == "true"
|
||||||
|
_ok, _reason = _chk(url, block_private=_block)
|
||||||
|
if not _ok:
|
||||||
|
webhook_error = f"Webhook URL rejected: {_reason}"
|
||||||
|
else:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
resp = await client.post(url, content=rendered.encode(), headers=hdrs)
|
||||||
|
webhook_sent = resp.is_success
|
||||||
|
if not webhook_sent:
|
||||||
|
webhook_error = f"Webhook returned HTTP {resp.status_code}"
|
||||||
|
except Exception as e:
|
||||||
|
webhook_error = str(e) or e.__class__.__name__
|
||||||
|
logger.warning(f"Reminder webhook send failed: {e}")
|
||||||
|
|
||||||
ntfy_sent = False
|
ntfy_sent = False
|
||||||
ntfy_error = ""
|
ntfy_error = ""
|
||||||
if channel == "ntfy":
|
if channel == "ntfy":
|
||||||
@@ -415,7 +487,7 @@ async def dispatch_reminder(
|
|||||||
# second send for the same note within 25 min. Without this, a note
|
# second send for the same note within 25 min. Without this, a note
|
||||||
# whose due_date fires while the user has the app open got TWO emails
|
# whose due_date fires while the user has the app open got TWO emails
|
||||||
# (frontend-fired here + background-fired by ping_notes 0–5 min later).
|
# (frontend-fired here + background-fired by ping_notes 0–5 min later).
|
||||||
if (email_sent or ntfy_sent or browser_sent or local_browser_sent) and note_id:
|
if (email_sent or ntfy_sent or webhook_sent or browser_sent or local_browser_sent) and note_id:
|
||||||
try:
|
try:
|
||||||
import json as _json
|
import json as _json
|
||||||
from datetime import datetime as _dt, timezone as _tz
|
from datetime import datetime as _dt, timezone as _tz
|
||||||
@@ -431,7 +503,7 @@ async def dispatch_reminder(
|
|||||||
_cache = cache or (_json.loads(_STATE.read_text(encoding="utf-8")) if _STATE.exists() else {})
|
_cache = cache or (_json.loads(_STATE.read_text(encoding="utf-8")) if _STATE.exists() else {})
|
||||||
except Exception:
|
except Exception:
|
||||||
_cache = {}
|
_cache = {}
|
||||||
sent_channel = "email" if email_sent else "ntfy" if ntfy_sent else "browser"
|
sent_channel = "email" if email_sent else "ntfy" if ntfy_sent else "webhook" if webhook_sent else "browser"
|
||||||
_cache[cache_key or str(note_id)] = {
|
_cache[cache_key or str(note_id)] = {
|
||||||
"at": _dt.now(_tz.utc).isoformat(),
|
"at": _dt.now(_tz.utc).isoformat(),
|
||||||
"channel": sent_channel,
|
"channel": sent_channel,
|
||||||
@@ -441,11 +513,14 @@ async def dispatch_reminder(
|
|||||||
logger.debug(f"dispatch_reminder: cache write failed: {_e}")
|
logger.debug(f"dispatch_reminder: cache write failed: {_e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"channel": channel,
|
||||||
"synthesis": synthesis,
|
"synthesis": synthesis,
|
||||||
"email_sent": email_sent,
|
"email_sent": email_sent,
|
||||||
"email_error": email_error,
|
"email_error": email_error,
|
||||||
"ntfy_sent": ntfy_sent,
|
"ntfy_sent": ntfy_sent,
|
||||||
"ntfy_error": ntfy_error,
|
"ntfy_error": ntfy_error,
|
||||||
|
"webhook_sent": webhook_sent,
|
||||||
|
"webhook_error": webhook_error,
|
||||||
"browser_sent": browser_sent or local_browser_sent,
|
"browser_sent": browser_sent or local_browser_sent,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,12 +767,21 @@ def setup_note_routes(task_scheduler=None):
|
|||||||
if not note_id:
|
if not note_id:
|
||||||
raise HTTPException(400, "note_id required")
|
raise HTTPException(400, "note_id required")
|
||||||
|
|
||||||
# Delegate to the module-level helper so background tasks can reuse
|
# Optional overrides let the test button pass the current UI values
|
||||||
# the same dispatch without an HTTP roundtrip + auth cookie.
|
# directly so the test never races against a pending settings save.
|
||||||
|
_override: dict = {}
|
||||||
|
if body.get("channel"):
|
||||||
|
_override["reminder_channel"] = body["channel"]
|
||||||
|
if body.get("webhook_integration_id"):
|
||||||
|
_override["reminder_webhook_integration_id"] = body["webhook_integration_id"]
|
||||||
|
if body.get("webhook_payload_template"):
|
||||||
|
_override["reminder_webhook_payload_template"] = body["webhook_payload_template"]
|
||||||
|
|
||||||
return await dispatch_reminder(
|
return await dispatch_reminder(
|
||||||
title=title, note_body=note_body, note_id=note_id,
|
title=title, note_body=note_body, note_id=note_id,
|
||||||
owner=_owner(request) or "",
|
owner=_owner(request) or "",
|
||||||
queue_browser=False,
|
queue_browser=False,
|
||||||
|
settings_override=_override or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- REORDER NOTES ---
|
# --- REORDER NOTES ---
|
||||||
|
|||||||
@@ -1902,6 +1902,8 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]:
|
|||||||
delivered = bool(dispatch_result.get("email_sent"))
|
delivered = bool(dispatch_result.get("email_sent"))
|
||||||
elif channel == "ntfy":
|
elif channel == "ntfy":
|
||||||
delivered = bool(dispatch_result.get("ntfy_sent"))
|
delivered = bool(dispatch_result.get("ntfy_sent"))
|
||||||
|
elif channel == "webhook":
|
||||||
|
delivered = bool(dispatch_result.get("webhook_sent"))
|
||||||
if delivered:
|
if delivered:
|
||||||
newly_notified.update(new_urgent)
|
newly_notified.update(new_urgent)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -100,6 +100,19 @@ INTEGRATION_PRESETS: Dict[str, Dict[str, Any]] = {
|
|||||||
" GET /{topic}/json?poll=1 — poll for messages"
|
" GET /{topic}/json?poll=1 — poll for messages"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"discord_webhook": {
|
||||||
|
"name": "Discord Webhook",
|
||||||
|
"auth_type": "none",
|
||||||
|
"description": (
|
||||||
|
"Discord Incoming Webhook. Paste the full webhook URL (including the token) as the Base URL.\n"
|
||||||
|
"To get a URL: Discord server -> Server Settings -> Integrations -> Webhooks -> New Webhook -> Copy Webhook URL.\n"
|
||||||
|
"The secret is embedded in the URL — leave auth type as None.\n\n"
|
||||||
|
"Use this integration as the target in Settings -> Reminders -> Webhook channel.\n"
|
||||||
|
"Payload template examples:\n"
|
||||||
|
" Simple: {\"content\": \"{{title}}: {{message}}\"}\n"
|
||||||
|
" Embed: {\"embeds\": [{\"title\": \"{{title}}\", \"description\": \"{{message}}\", \"color\": 5793266}]}"
|
||||||
|
),
|
||||||
|
},
|
||||||
"vaultwarden": {
|
"vaultwarden": {
|
||||||
"name": "Vaultwarden",
|
"name": "Vaultwarden",
|
||||||
"auth_type": "header",
|
"auth_type": "header",
|
||||||
|
|||||||
+8
-1
@@ -141,10 +141,17 @@ DEFAULT_SETTINGS = {
|
|||||||
# library can grow beyond this; cleanup/retirement is an explicit review flow.
|
# library can grow beyond this; cleanup/retirement is an explicit review flow.
|
||||||
"skill_max_injected": 3,
|
"skill_max_injected": 3,
|
||||||
# Reminders
|
# Reminders
|
||||||
"reminder_channel": "browser", # "browser" | "email" | "ntfy"
|
"reminder_channel": "browser", # "browser" | "email" | "ntfy" | "webhook"
|
||||||
"reminder_llm_synthesis": False,
|
"reminder_llm_synthesis": False,
|
||||||
"reminder_ntfy_topic": "Reminders",
|
"reminder_ntfy_topic": "Reminders",
|
||||||
"reminder_email_to": "",
|
"reminder_email_to": "",
|
||||||
|
# Generic outbound webhook channel: pick any saved Integration as the
|
||||||
|
# target and supply a JSON payload template. Use {{title}} and {{message}}
|
||||||
|
# as placeholders — they are JSON-escaped before substitution, so the
|
||||||
|
# rendered string is always valid JSON. Works with Discord, Slack, Teams,
|
||||||
|
# ntfy (JSON mode), or any service that accepts a POST with a JSON body.
|
||||||
|
"reminder_webhook_integration_id": "",
|
||||||
|
"reminder_webhook_payload_template": "",
|
||||||
# Email triage scanner rules. Running/paused state and schedule live in
|
# Email triage scanner rules. Running/paused state and schedule live in
|
||||||
# Tasks via the built-in `check_email_urgency` task.
|
# Tasks via the built-in `check_email_urgency` task.
|
||||||
"urgent_email_prompt": (
|
"urgent_email_prompt": (
|
||||||
|
|||||||
@@ -1566,6 +1566,8 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
|||||||
"image gen": "image_gen_enabled", "image generation": "image_gen_enabled",
|
"image gen": "image_gen_enabled", "image generation": "image_gen_enabled",
|
||||||
"reminder channel": "reminder_channel", "reminders": "reminder_channel",
|
"reminder channel": "reminder_channel", "reminders": "reminder_channel",
|
||||||
"ntfy topic": "reminder_ntfy_topic",
|
"ntfy topic": "reminder_ntfy_topic",
|
||||||
|
"webhook integration": "reminder_webhook_integration_id",
|
||||||
|
"webhook template": "reminder_webhook_payload_template", "webhook payload": "reminder_webhook_payload_template",
|
||||||
"agent tool calls": "agent_max_tool_calls", "max tool calls": "agent_max_tool_calls",
|
"agent tool calls": "agent_max_tool_calls", "max tool calls": "agent_max_tool_calls",
|
||||||
"agent timeout": "agent_stream_timeout_seconds", "stream timeout": "agent_stream_timeout_seconds",
|
"agent timeout": "agent_stream_timeout_seconds", "stream timeout": "agent_stream_timeout_seconds",
|
||||||
"token budget": "agent_input_token_budget", "input budget": "agent_input_token_budget",
|
"token budget": "agent_input_token_budget", "input budget": "agent_input_token_budget",
|
||||||
@@ -1581,7 +1583,7 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
|||||||
|
|
||||||
_ENUMS = {
|
_ENUMS = {
|
||||||
"image_quality": ["low", "medium", "high"],
|
"image_quality": ["low", "medium", "high"],
|
||||||
"reminder_channel": ["browser", "email", "ntfy"],
|
"reminder_channel": ["browser", "email", "ntfy", "webhook"],
|
||||||
}
|
}
|
||||||
def _coerce(value, default):
|
def _coerce(value, default):
|
||||||
if isinstance(default, bool):
|
if isinstance(default, bool):
|
||||||
|
|||||||
+10
-1
@@ -1957,6 +1957,7 @@
|
|||||||
<option value="browser">Browser notification (default)</option>
|
<option value="browser">Browser notification (default)</option>
|
||||||
<option value="email" id="set-reminder-channel-email-opt">Email</option>
|
<option value="email" id="set-reminder-channel-email-opt">Email</option>
|
||||||
<option value="ntfy" id="set-reminder-channel-ntfy-opt">ntfy</option>
|
<option value="ntfy" id="set-reminder-channel-ntfy-opt">ntfy</option>
|
||||||
|
<option value="webhook" id="set-reminder-channel-webhook-opt">Webhook</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="set-reminder-email-from-row" class="settings-row" style="display:none">
|
<div id="set-reminder-email-from-row" class="settings-row" style="display:none">
|
||||||
@@ -1971,13 +1972,21 @@
|
|||||||
<label class="settings-label">ntfy topic</label>
|
<label class="settings-label">ntfy topic</label>
|
||||||
<input id="set-reminder-ntfy-topic" class="settings-select" type="text" placeholder="reminders" />
|
<input id="set-reminder-ntfy-topic" class="settings-select" type="text" placeholder="reminders" />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="set-reminder-webhook-intg-row" class="settings-row" style="display:none">
|
||||||
|
<label class="settings-label">Integration</label>
|
||||||
|
<select id="set-reminder-webhook-intg" class="settings-select"></select>
|
||||||
|
</div>
|
||||||
|
<div id="set-reminder-webhook-template-row" class="settings-row" style="display:none;align-items:flex-start">
|
||||||
|
<label class="settings-label" style="padding-top:6px">Payload</label>
|
||||||
|
<textarea id="set-reminder-webhook-template" class="settings-select" rows="3" style="font-family:inherit;resize:vertical;flex:1" placeholder='{"content": "{{title}}: {{message}}"}'></textarea>
|
||||||
|
</div>
|
||||||
<div id="set-reminder-channel-hint" style="font-size:11px;opacity:0.6;"></div>
|
<div id="set-reminder-channel-hint" style="font-size:11px;opacity:0.6;"></div>
|
||||||
<div style="font-size:11px;opacity:0.6;margin-top:4px;">Configure email account, ntfy server, etc. in <a href="#" id="set-reminders-open-integrations" style="color:var(--accent, var(--red));text-decoration:none;font-weight:600;">Integrations</a>.</div>
|
<div style="font-size:11px;opacity:0.6;margin-top:4px;">Configure email account, ntfy server, etc. in <a href="#" id="set-reminders-open-integrations" style="color:var(--accent, var(--red));text-decoration:none;font-weight:600;">Integrations</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg>AI Synthesis<span style="flex:1"></span><label class="admin-switch" title="Use the utility model to write reminder messages"><input type="checkbox" id="set-reminder-llm-toggle"><span class="admin-slider"></span></label></h2>
|
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg>AI Synthesis<span style="flex:1"></span><label class="admin-switch" title="Use the utility model to write reminder messages"><input type="checkbox" id="set-reminder-llm-toggle"><span class="admin-slider"></span></label></h2>
|
||||||
<div class="admin-toggle-sub" style="margin-bottom:8px">When on, the utility model writes a short, warm one-line reminder for browser, email, AND ntfy reminders instead of just the raw note content.</div>
|
<div class="admin-toggle-sub" style="margin-bottom:8px">When on, the utility model writes a short, warm one-line reminder for browser, email, ntfy, AND webhook reminders instead of just the raw note content.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Public App URL</h2>
|
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Public App URL</h2>
|
||||||
|
|||||||
+112
-2
@@ -2244,6 +2244,7 @@ async function initReminderSettings() {
|
|||||||
const channelSel = el('set-reminder-channel');
|
const channelSel = el('set-reminder-channel');
|
||||||
const emailOpt = el('set-reminder-channel-email-opt');
|
const emailOpt = el('set-reminder-channel-email-opt');
|
||||||
const ntfyOpt = el('set-reminder-channel-ntfy-opt');
|
const ntfyOpt = el('set-reminder-channel-ntfy-opt');
|
||||||
|
const webhookOpt = el('set-reminder-channel-webhook-opt');
|
||||||
const hint = el('set-reminder-channel-hint');
|
const hint = el('set-reminder-channel-hint');
|
||||||
const llmToggle = el('set-reminder-llm-toggle');
|
const llmToggle = el('set-reminder-llm-toggle');
|
||||||
// "Integrations" link in the channel-hint copy. Jumps to the
|
// "Integrations" link in the channel-hint copy. Jumps to the
|
||||||
@@ -2306,12 +2307,33 @@ async function initReminderSettings() {
|
|||||||
ntfyOpt.textContent = 'ntfy (add in Integrations first)';
|
ntfyOpt.textContent = 'ntfy (add in Integrations first)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Webhook: available whenever at least one integration with a base_url exists.
|
||||||
|
// The user picks which integration to target and supplies a payload template.
|
||||||
|
let allIntegrations = [];
|
||||||
|
let webhookConfigured = false;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/integrations', { credentials: 'same-origin' });
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
allIntegrations = (data.integrations || []).filter(i => i.base_url && i.enabled !== false);
|
||||||
|
webhookConfigured = allIntegrations.length > 0;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
if (!webhookConfigured && webhookOpt) {
|
||||||
|
webhookOpt.disabled = true;
|
||||||
|
webhookOpt.textContent = 'Webhook (add an Integration first)';
|
||||||
|
}
|
||||||
|
|
||||||
const emailFromRow = el('set-reminder-email-from-row');
|
const emailFromRow = el('set-reminder-email-from-row');
|
||||||
const emailAcctSel = el('set-reminder-email-account');
|
const emailAcctSel = el('set-reminder-email-account');
|
||||||
const emailToRow = el('set-reminder-email-to-row');
|
const emailToRow = el('set-reminder-email-to-row');
|
||||||
const emailToIn = el('set-reminder-email-to');
|
const emailToIn = el('set-reminder-email-to');
|
||||||
const ntfyTopicRow = el('set-reminder-ntfy-topic-row');
|
const ntfyTopicRow = el('set-reminder-ntfy-topic-row');
|
||||||
const ntfyTopicIn = el('set-reminder-ntfy-topic');
|
const ntfyTopicIn = el('set-reminder-ntfy-topic');
|
||||||
|
const webhookIntgRow = el('set-reminder-webhook-intg-row');
|
||||||
|
const webhookIntgSel = el('set-reminder-webhook-intg');
|
||||||
|
const webhookTemplateRow = el('set-reminder-webhook-template-row');
|
||||||
|
const webhookTemplateIn = el('set-reminder-webhook-template');
|
||||||
|
|
||||||
function populateReminderEmailAccounts(selectedId = '') {
|
function populateReminderEmailAccounts(selectedId = '') {
|
||||||
if (!emailAcctSel) return;
|
if (!emailAcctSel) return;
|
||||||
@@ -2322,6 +2344,14 @@ async function initReminderSettings() {
|
|||||||
emailAcctSel.value = (selectedId && emailAccounts.some(a => a.id === selectedId)) ? selectedId : fallback;
|
emailAcctSel.value = (selectedId && emailAccounts.some(a => a.id === selectedId)) ? selectedId : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateWebhookIntegrations(selectedId = '') {
|
||||||
|
if (!webhookIntgSel) return;
|
||||||
|
webhookIntgSel.innerHTML = allIntegrations.length
|
||||||
|
? allIntegrations.map(i => `<option value="${esc(i.id)}">${esc(i.name || i.id)}</option>`).join('')
|
||||||
|
: '<option value="">No integrations configured</option>';
|
||||||
|
if (selectedId && allIntegrations.some(i => i.id === selectedId)) webhookIntgSel.value = selectedId;
|
||||||
|
}
|
||||||
|
|
||||||
function applyReminderChannelAvailability() {
|
function applyReminderChannelAvailability() {
|
||||||
if (emailOpt) {
|
if (emailOpt) {
|
||||||
emailOpt.disabled = !smtpConfigured;
|
emailOpt.disabled = !smtpConfigured;
|
||||||
@@ -2331,11 +2361,16 @@ async function initReminderSettings() {
|
|||||||
ntfyOpt.disabled = !ntfyConfigured;
|
ntfyOpt.disabled = !ntfyConfigured;
|
||||||
ntfyOpt.textContent = ntfyConfigured ? 'ntfy' : 'ntfy (add in Integrations first)';
|
ntfyOpt.textContent = ntfyConfigured ? 'ntfy' : 'ntfy (add in Integrations first)';
|
||||||
}
|
}
|
||||||
|
if (webhookOpt) {
|
||||||
|
webhookOpt.disabled = !webhookConfigured;
|
||||||
|
webhookOpt.textContent = webhookConfigured ? 'Webhook' : 'Webhook (add an Integration first)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshReminderChannelAvailability() {
|
async function refreshReminderChannelAvailability() {
|
||||||
const currentChannel = channelSel.value || 'browser';
|
const currentChannel = channelSel.value || 'browser';
|
||||||
const currentEmailAccount = emailAcctSel?.value || '';
|
const currentEmailAccount = emailAcctSel?.value || '';
|
||||||
|
const currentWebhookIntg = webhookIntgSel?.value || '';
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/email/accounts', { credentials: 'same-origin' });
|
const res = await fetch('/api/email/accounts', { credentials: 'same-origin' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -2353,6 +2388,8 @@ async function initReminderSettings() {
|
|||||||
ntfyConfigured = (data.integrations || []).some(
|
ntfyConfigured = (data.integrations || []).some(
|
||||||
i => (i.preset === 'ntfy' || (i.name || '').toLowerCase() === 'ntfy') && i.enabled !== false && i.base_url
|
i => (i.preset === 'ntfy' || (i.name || '').toLowerCase() === 'ntfy') && i.enabled !== false && i.base_url
|
||||||
);
|
);
|
||||||
|
allIntegrations = (data.integrations || []).filter(i => i.base_url && i.enabled !== false);
|
||||||
|
webhookConfigured = allIntegrations.length > 0;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (!ntfyConfigured) {
|
if (!ntfyConfigured) {
|
||||||
@@ -2365,8 +2402,10 @@ async function initReminderSettings() {
|
|||||||
|
|
||||||
applyReminderChannelAvailability();
|
applyReminderChannelAvailability();
|
||||||
populateReminderEmailAccounts(currentEmailAccount);
|
populateReminderEmailAccounts(currentEmailAccount);
|
||||||
|
populateWebhookIntegrations(currentWebhookIntg);
|
||||||
if (currentChannel === 'email' && !smtpConfigured) channelSel.value = 'browser';
|
if (currentChannel === 'email' && !smtpConfigured) channelSel.value = 'browser';
|
||||||
else if (currentChannel === 'ntfy' && !ntfyConfigured) channelSel.value = 'browser';
|
else if (currentChannel === 'ntfy' && !ntfyConfigured) channelSel.value = 'browser';
|
||||||
|
else if (currentChannel === 'webhook' && !webhookConfigured) channelSel.value = 'browser';
|
||||||
else channelSel.value = currentChannel;
|
else channelSel.value = currentChannel;
|
||||||
if (hint) hint.textContent = CHANNEL_HINTS[channelSel.value] || '';
|
if (hint) hint.textContent = CHANNEL_HINTS[channelSel.value] || '';
|
||||||
syncChannelRows();
|
syncChannelRows();
|
||||||
@@ -2377,9 +2416,12 @@ async function initReminderSettings() {
|
|||||||
|
|
||||||
function syncChannelRows() {
|
function syncChannelRows() {
|
||||||
const isEmail = channelSel.value === 'email';
|
const isEmail = channelSel.value === 'email';
|
||||||
|
const isWebhook = channelSel.value === 'webhook';
|
||||||
if (emailFromRow) emailFromRow.style.display = (isEmail && emailAccounts.length > 1) ? 'flex' : 'none';
|
if (emailFromRow) emailFromRow.style.display = (isEmail && emailAccounts.length > 1) ? 'flex' : 'none';
|
||||||
if (emailToRow) emailToRow.style.display = isEmail ? 'flex' : 'none';
|
if (emailToRow) emailToRow.style.display = isEmail ? 'flex' : 'none';
|
||||||
if (ntfyTopicRow) ntfyTopicRow.style.display = channelSel.value === 'ntfy' ? 'flex' : 'none';
|
if (ntfyTopicRow) ntfyTopicRow.style.display = channelSel.value === 'ntfy' ? 'flex' : 'none';
|
||||||
|
if (webhookIntgRow) webhookIntgRow.style.display = isWebhook ? 'flex' : 'none';
|
||||||
|
if (webhookTemplateRow) webhookTemplateRow.style.display = isWebhook ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browser notifications fire on EVERY reminder (see
|
// Browser notifications fire on EVERY reminder (see
|
||||||
@@ -2390,6 +2432,7 @@ async function initReminderSettings() {
|
|||||||
browser: 'Reminders appear as browser notifications inside Odysseus.',
|
browser: 'Reminders appear as browser notifications inside Odysseus.',
|
||||||
email: 'Reminders are emailed AND shown as a browser notification.',
|
email: 'Reminders are emailed AND shown as a browser notification.',
|
||||||
ntfy: 'Reminders are pushed via ntfy AND shown as a browser notification.',
|
ntfy: 'Reminders are pushed via ntfy AND shown as a browser notification.',
|
||||||
|
webhook: 'Reminders are POSTed to the selected integration AND shown as a browser notification. Use {{title}} and {{message}} in the payload template.',
|
||||||
};
|
};
|
||||||
|
|
||||||
applyReminderChannelAvailability();
|
applyReminderChannelAvailability();
|
||||||
@@ -2400,16 +2443,36 @@ async function initReminderSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default payload templates for known presets — auto-filled when the user
|
||||||
|
// picks a matching integration so they don't have to write JSON from scratch.
|
||||||
|
// Defined here (before the load block) so both the load path and the change
|
||||||
|
// handler can reference it.
|
||||||
|
const WEBHOOK_PRESET_TEMPLATES = {
|
||||||
|
discord_webhook: '{"embeds": [{"title": "{{title}}", "description": "{{message}}", "color": 5793266}]}',
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/settings', { credentials: 'same-origin' });
|
const res = await fetch('/api/auth/settings', { credentials: 'same-origin' });
|
||||||
const s = await res.json();
|
const s = await res.json();
|
||||||
let savedChannel = s.reminder_channel || 'browser';
|
let savedChannel = s.reminder_channel || 'browser';
|
||||||
if (savedChannel === 'email' && !smtpConfigured) savedChannel = 'browser';
|
if (savedChannel === 'email' && !smtpConfigured) savedChannel = 'browser';
|
||||||
if (savedChannel === 'ntfy' && !ntfyConfigured) savedChannel = 'browser';
|
if (savedChannel === 'ntfy' && !ntfyConfigured) savedChannel = 'browser';
|
||||||
|
if (savedChannel === 'webhook' && !webhookConfigured) savedChannel = 'browser';
|
||||||
channelSel.value = savedChannel;
|
channelSel.value = savedChannel;
|
||||||
llmToggle.checked = !!s.reminder_llm_synthesis;
|
llmToggle.checked = !!s.reminder_llm_synthesis;
|
||||||
if (emailToIn) emailToIn.value = s.reminder_email_to || '';
|
if (emailToIn) emailToIn.value = s.reminder_email_to || '';
|
||||||
if (ntfyTopicIn) ntfyTopicIn.value = s.reminder_ntfy_topic || 'Reminders';
|
if (ntfyTopicIn) ntfyTopicIn.value = s.reminder_ntfy_topic || 'Reminders';
|
||||||
|
populateWebhookIntegrations(s.reminder_webhook_integration_id || '');
|
||||||
|
if (webhookTemplateIn) {
|
||||||
|
webhookTemplateIn.value = s.reminder_webhook_payload_template || '';
|
||||||
|
// If an integration is already selected but no template was ever saved,
|
||||||
|
// auto-fill with the preset default so the first test works out of the box.
|
||||||
|
if (!webhookTemplateIn.value && webhookIntgSel?.value) {
|
||||||
|
const intg = allIntegrations.find(i => i.id === webhookIntgSel.value);
|
||||||
|
const tpl = WEBHOOK_PRESET_TEMPLATES[intg?.preset] || '';
|
||||||
|
if (tpl) { webhookTemplateIn.value = tpl; save({ reminder_webhook_payload_template: tpl }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
// Restore the previously-picked email account (if any), otherwise
|
// Restore the previously-picked email account (if any), otherwise
|
||||||
// default to the account flagged is_default in the integrations
|
// default to the account flagged is_default in the integrations
|
||||||
// list. Falls through to the first option if neither exists.
|
// list. Falls through to the first option if neither exists.
|
||||||
@@ -2459,6 +2522,28 @@ async function initReminderSettings() {
|
|||||||
topicDebounce = setTimeout(() => save({ reminder_ntfy_topic: ntfyTopicIn.value.trim() || 'reminders' }), 600);
|
topicDebounce = setTimeout(() => save({ reminder_ntfy_topic: ntfyTopicIn.value.trim() || 'reminders' }), 600);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (webhookIntgSel) {
|
||||||
|
webhookIntgSel.addEventListener('change', () => {
|
||||||
|
save({ reminder_webhook_integration_id: webhookIntgSel.value || '' });
|
||||||
|
// If the template is empty and we recognise the integration's preset,
|
||||||
|
// pre-fill with a sensible default so users can test immediately.
|
||||||
|
if (webhookTemplateIn && !webhookTemplateIn.value.trim()) {
|
||||||
|
const intg = allIntegrations.find(i => i.id === webhookIntgSel.value);
|
||||||
|
const tpl = WEBHOOK_PRESET_TEMPLATES[intg?.preset] || '';
|
||||||
|
if (tpl) {
|
||||||
|
webhookTemplateIn.value = tpl;
|
||||||
|
save({ reminder_webhook_payload_template: tpl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (webhookTemplateIn) {
|
||||||
|
let templateDebounce;
|
||||||
|
webhookTemplateIn.addEventListener('input', () => {
|
||||||
|
clearTimeout(templateDebounce);
|
||||||
|
templateDebounce = setTimeout(() => save({ reminder_webhook_payload_template: webhookTemplateIn.value.trim() }), 600);
|
||||||
|
});
|
||||||
|
}
|
||||||
// Dim the whole AI Synthesis card when off (matches Vision/Utility/etc.).
|
// Dim the whole AI Synthesis card when off (matches Vision/Utility/etc.).
|
||||||
function syncSynthesisDim() {
|
function syncSynthesisDim() {
|
||||||
const card = llmToggle.closest('.admin-card');
|
const card = llmToggle.closest('.admin-card');
|
||||||
@@ -2495,6 +2580,11 @@ async function initReminderSettings() {
|
|||||||
note_id: 'test-' + Date.now(),
|
note_id: 'test-' + Date.now(),
|
||||||
title: 'Test Reminder',
|
title: 'Test Reminder',
|
||||||
body: 'This is a test reminder to verify your settings are working.',
|
body: 'This is a test reminder to verify your settings are working.',
|
||||||
|
channel: channelSel.value,
|
||||||
|
...(channelSel.value === 'webhook' ? {
|
||||||
|
webhook_integration_id: webhookIntgSel?.value || '',
|
||||||
|
webhook_payload_template: webhookTemplateIn?.value.trim() || '',
|
||||||
|
} : {}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -2505,10 +2595,15 @@ async function initReminderSettings() {
|
|||||||
if (channelSel.value === 'ntfy' && !data.ntfy_sent) {
|
if (channelSel.value === 'ntfy' && !data.ntfy_sent) {
|
||||||
throw new Error(data.ntfy_error || 'ntfy reminder was not sent');
|
throw new Error(data.ntfy_error || 'ntfy reminder was not sent');
|
||||||
}
|
}
|
||||||
|
if (channelSel.value === 'webhook' && !data.webhook_sent) {
|
||||||
|
const activeChannel = data.channel ? ` (server used channel: "${data.channel}")` : '';
|
||||||
|
throw new Error((data.webhook_error || 'Webhook reminder was not sent') + activeChannel);
|
||||||
|
}
|
||||||
let status = 'Delivered via ' + channelSel.value;
|
let status = 'Delivered via ' + channelSel.value;
|
||||||
if (data.synthesis) status += ' (AI: "' + data.synthesis.slice(0, 60) + '...")';
|
if (data.synthesis) status += ' (AI: "' + data.synthesis.slice(0, 60) + '...")';
|
||||||
if (data.email_sent) status += ' — email sent';
|
if (data.email_sent) status += ' — email sent';
|
||||||
if (data.ntfy_sent) status += ' — ntfy sent';
|
if (data.ntfy_sent) status += ' — ntfy sent';
|
||||||
|
if (data.webhook_sent) status += ' — webhook sent';
|
||||||
if (testMsg) { testMsg.textContent = status; testMsg.style.color = 'var(--green, #50fa7b)'; }
|
if (testMsg) { testMsg.textContent = status; testMsg.style.color = 'var(--green, #50fa7b)'; }
|
||||||
// Also fire a browser notification so user can see it
|
// Also fire a browser notification so user can see it
|
||||||
if ('Notification' in window && Notification.permission === 'granted') {
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
@@ -2932,12 +3027,18 @@ async function initIntegrations() {
|
|||||||
let editingId = null;
|
let editingId = null;
|
||||||
let presets = {};
|
let presets = {};
|
||||||
|
|
||||||
// Toggle auth header row visibility
|
// Presets where the secret is embedded in the URL — no separate key or
|
||||||
|
// auth header is used, so hiding those fields avoids confusion.
|
||||||
|
const URL_AUTH_PRESETS = ['discord_webhook'];
|
||||||
|
|
||||||
|
// Toggle auth header + key row visibility based on auth type and preset.
|
||||||
function syncAuthRow() {
|
function syncAuthRow() {
|
||||||
const v = authTypeSel.value;
|
const v = authTypeSel.value;
|
||||||
authHeaderRow.style.display = (v === 'header' || v === 'query') ? 'flex' : 'none';
|
authHeaderRow.style.display = (v === 'header' || v === 'query') ? 'flex' : 'none';
|
||||||
if (v === 'query') authHeaderIn.placeholder = 'api_key';
|
if (v === 'query') authHeaderIn.placeholder = 'api_key';
|
||||||
else authHeaderIn.placeholder = 'X-Auth-Token';
|
else authHeaderIn.placeholder = 'X-Auth-Token';
|
||||||
|
const keyRow = keyIn?.closest('.settings-row');
|
||||||
|
if (keyRow) keyRow.style.display = URL_AUTH_PRESETS.includes(presetSel?.value) ? 'none' : '';
|
||||||
}
|
}
|
||||||
authTypeSel.addEventListener('change', syncAuthRow);
|
authTypeSel.addEventListener('change', syncAuthRow);
|
||||||
|
|
||||||
@@ -3487,6 +3588,7 @@ async function initUnifiedIntegrations() {
|
|||||||
const _applyPreset = () => {
|
const _applyPreset = () => {
|
||||||
const p = presets[preset.value];
|
const p = presets[preset.value];
|
||||||
const isNtfy = preset.value === 'ntfy' || (p && (p.name || '').toLowerCase() === 'ntfy');
|
const isNtfy = preset.value === 'ntfy' || (p && (p.name || '').toLowerCase() === 'ntfy');
|
||||||
|
const isUrlAuth = preset.value === 'discord_webhook'; // secret embedded in URL — no key/auth fields needed
|
||||||
if (ntfyHint) {
|
if (ntfyHint) {
|
||||||
ntfyHint.style.display = isNtfy ? 'block' : 'none';
|
ntfyHint.style.display = isNtfy ? 'block' : 'none';
|
||||||
if (isNtfy) {
|
if (isNtfy) {
|
||||||
@@ -3494,8 +3596,16 @@ async function initUnifiedIntegrations() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
url.placeholder = isNtfy ? 'http://127.0.0.1:8091' : 'http://localhost:8080';
|
url.placeholder = isNtfy ? 'http://127.0.0.1:8091' : isUrlAuth ? 'https://discord.com/api/webhooks/...' : 'http://localhost:8080';
|
||||||
}
|
}
|
||||||
|
// For presets that embed the secret in the URL, hide auth/key/header rows
|
||||||
|
// so users aren't confused into thinking they need to fill them in.
|
||||||
|
const keyRow = key?.closest('.settings-row');
|
||||||
|
const authRow = auth?.closest('.settings-row');
|
||||||
|
const headerRow = el('uf-api-header-row');
|
||||||
|
if (keyRow) keyRow.style.display = isUrlAuth ? 'none' : '';
|
||||||
|
if (authRow) authRow.style.display = isUrlAuth ? 'none' : '';
|
||||||
|
if (headerRow) headerRow.style.display = isUrlAuth ? 'none' : '';
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
name.value = p.name || '';
|
name.value = p.name || '';
|
||||||
auth.value = p.auth_type || 'none';
|
auth.value = p.auth_type || 'none';
|
||||||
|
|||||||
Reference in New Issue
Block a user