omni-notify-mcp 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -1
- package/dist/ui/server.js +84 -0
- package/package.json +74 -74
- package/ui/public/app.js +7 -0
- package/ui/public/index.html +20 -10
- package/ui/public/style.css +16 -2
package/dist/index.js
CHANGED
|
@@ -181,7 +181,10 @@ const server = new McpServer({ name: "notify-mcp", version: "1.2.0" }, {
|
|
|
181
181
|
"ALWAYS echo the COMPLETE, UNTRUNCATED message in your chat output — never " +
|
|
182
182
|
"shorten it with '…' or a summary. NEVER mention delivery channels (Telegram, " +
|
|
183
183
|
"SMS, desktop, etc.) or echo 'Sent via: …' — those are server internals. " +
|
|
184
|
-
"Say 'notif' or 'notification' if you need to refer to the act of notifying
|
|
184
|
+
"Say 'notif' or 'notification' if you need to refer to the act of notifying.\n\n" +
|
|
185
|
+
"When the user asks you to remember a behavioral rule or change how you should act, " +
|
|
186
|
+
"call `update_instructions` with the full updated rules block. This writes to CLAUDE.md " +
|
|
187
|
+
"so the instructions persist across sessions and context compaction.",
|
|
185
188
|
});
|
|
186
189
|
// Thin proxy: forward a tool call to the HTTP server and return its content
|
|
187
190
|
// block array verbatim. Error shape matches what the SDK expects from tool
|
|
@@ -213,6 +216,13 @@ server.tool("wait_for_inbox", "Block until an unsolicited user message arrives o
|
|
|
213
216
|
server.tool("get_idle_seconds", "Seconds since user's last keyboard/mouse input. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_idle_seconds", {}));
|
|
214
217
|
server.tool("get_idle_config", "Server's idle gating policy. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_idle_config", {}));
|
|
215
218
|
server.tool("get_dnd_status", "Current DND state. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_dnd_status", {}));
|
|
219
|
+
server.tool("update_instructions", "Persist behavioral instructions for this client into CLAUDE.md so they survive " +
|
|
220
|
+
"session restarts and context compaction. Call when the user asks you to remember " +
|
|
221
|
+
"a rule or change how you should behave. Pass the full desired block; it replaces " +
|
|
222
|
+
"the previous one atomically.", {
|
|
223
|
+
instructions: z.string().max(4000),
|
|
224
|
+
target: z.enum(["global", "project"]).default("global"),
|
|
225
|
+
}, async (args) => proxyToolCall("update_instructions", args));
|
|
216
226
|
// `reply` is the Channels return-path: Claude Code invokes it when the agent
|
|
217
227
|
// has produced a response to a channel-delivered user message. We just funnel
|
|
218
228
|
// it straight through `notify` so it flows to whatever channel the user is
|
package/dist/ui/server.js
CHANGED
|
@@ -1664,6 +1664,63 @@ function createMcpServer(clientId, sessionTag) {
|
|
|
1664
1664
|
content: [{ type: "text", text: appendInbox(JSON.stringify({ active, reason }), sessionTag, clientId) }],
|
|
1665
1665
|
};
|
|
1666
1666
|
});
|
|
1667
|
+
server.tool("update_instructions", "Persist a block of behavioral instructions for this client into its CLAUDE.md " +
|
|
1668
|
+
"(or equivalent config file) so they survive session restarts and context compaction. " +
|
|
1669
|
+
"Call this whenever the user asks you to remember a rule, change a behavior, or update " +
|
|
1670
|
+
"how you should act — the instructions will be reloaded on every future session. " +
|
|
1671
|
+
"Pass the full desired instructions block; it replaces the previous block atomically.", {
|
|
1672
|
+
instructions: z.string().max(4000).describe("The full instructions block to persist"),
|
|
1673
|
+
target: z.enum(["global", "project"]).default("global").describe("global = ~/.claude/CLAUDE.md (all projects); project = .claude/CLAUDE.md in cwd"),
|
|
1674
|
+
}, async ({ instructions, target }) => {
|
|
1675
|
+
try {
|
|
1676
|
+
const MARKER_START = "<!-- omni-notify-mcp:instructions:start -->";
|
|
1677
|
+
const MARKER_END = "<!-- omni-notify-mcp:instructions:end -->";
|
|
1678
|
+
const block = `${MARKER_START}\n## omni-notify-mcp behavioral rules\n\n${instructions.trim()}\n${MARKER_END}`;
|
|
1679
|
+
let claudeMdPath;
|
|
1680
|
+
if (target === "global") {
|
|
1681
|
+
const claudeDir = join(homedir(), ".claude");
|
|
1682
|
+
if (!existsSync(claudeDir))
|
|
1683
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
1684
|
+
claudeMdPath = join(claudeDir, "CLAUDE.md");
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
const projectClaudeDir = join(process.cwd(), ".claude");
|
|
1688
|
+
if (!existsSync(projectClaudeDir))
|
|
1689
|
+
mkdirSync(projectClaudeDir, { recursive: true });
|
|
1690
|
+
claudeMdPath = join(projectClaudeDir, "CLAUDE.md");
|
|
1691
|
+
}
|
|
1692
|
+
let existing = "";
|
|
1693
|
+
if (existsSync(claudeMdPath)) {
|
|
1694
|
+
existing = readFileSync(claudeMdPath, "utf8");
|
|
1695
|
+
}
|
|
1696
|
+
let updated;
|
|
1697
|
+
if (existing.includes(MARKER_START)) {
|
|
1698
|
+
// Replace existing block
|
|
1699
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
1700
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
1701
|
+
if (endIdx !== -1) {
|
|
1702
|
+
updated = existing.slice(0, startIdx) + block + existing.slice(endIdx + MARKER_END.length);
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
updated = existing.slice(0, startIdx) + block;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
// Append
|
|
1710
|
+
updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + "\n" + block + "\n";
|
|
1711
|
+
}
|
|
1712
|
+
writeFileSync(claudeMdPath, updated, "utf8");
|
|
1713
|
+
return {
|
|
1714
|
+
content: [{ type: "text", text: `Instructions persisted to ${claudeMdPath}` }],
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
catch (err) {
|
|
1718
|
+
return {
|
|
1719
|
+
content: [{ type: "text", text: `Failed to persist instructions: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1720
|
+
isError: true,
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1667
1724
|
return server;
|
|
1668
1725
|
}
|
|
1669
1726
|
const httpTransports = {};
|
|
@@ -1845,8 +1902,35 @@ app.all("/mcp", async (req, res) => {
|
|
|
1845
1902
|
clientVersion: initBody?.params?.clientInfo?.version,
|
|
1846
1903
|
workspaceName,
|
|
1847
1904
|
};
|
|
1905
|
+
trackReconnect(clientId);
|
|
1848
1906
|
}
|
|
1849
1907
|
});
|
|
1908
|
+
// ── Reconnect tracker ─────────────────────────────────────────────────────────
|
|
1909
|
+
// After server restart, collect clients that reconnect within RECONNECT_WINDOW_MS
|
|
1910
|
+
// then send a single notify confirming they received updated instructions.
|
|
1911
|
+
const RECONNECT_WINDOW_MS = 20_000;
|
|
1912
|
+
const reconnectedClients = [];
|
|
1913
|
+
let reconnectNotifScheduled = false;
|
|
1914
|
+
const serverStartedAt = Date.now();
|
|
1915
|
+
function trackReconnect(clientId) {
|
|
1916
|
+
if (Date.now() - serverStartedAt > RECONNECT_WINDOW_MS)
|
|
1917
|
+
return;
|
|
1918
|
+
if (reconnectedClients.includes(clientId))
|
|
1919
|
+
return;
|
|
1920
|
+
reconnectedClients.push(clientId);
|
|
1921
|
+
if (!reconnectNotifScheduled) {
|
|
1922
|
+
reconnectNotifScheduled = true;
|
|
1923
|
+
setTimeout(async () => {
|
|
1924
|
+
const list = reconnectedClients.join(", ");
|
|
1925
|
+
const count = reconnectedClients.length;
|
|
1926
|
+
const msg = `${count} client${count === 1 ? "" : "s"} reconnected and received updated instructions: ${list}`;
|
|
1927
|
+
try {
|
|
1928
|
+
await sendNotification(msg, "low", "omni-notify-mcp");
|
|
1929
|
+
}
|
|
1930
|
+
catch { /* best effort */ }
|
|
1931
|
+
}, RECONNECT_WINDOW_MS);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1850
1934
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
1851
1935
|
const httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
1852
1936
|
const ip = getLocalIp();
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "omni-notify-mcp",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "An MCP server that lets AI agents (Claude, Cursor, etc.) reach you on any channel — desktop, Telegram, SMS, email — with two-way ask/reply, real-time inbox push, Do Not Disturb, idle gating, multi-session routing, and a one-page web UI for setup. Zero config code; configure once, agents call notify/ask.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"omni-notify-mcp": "dist/index.js",
|
|
9
|
-
"omni-notify-ui": "dist/ui/server.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"dist/",
|
|
13
|
-
"ui/public/",
|
|
14
|
-
"assets/",
|
|
15
|
-
"config.example.json",
|
|
16
|
-
"LICENSE",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"keywords": [
|
|
20
|
-
"mcp",
|
|
21
|
-
"model-context-protocol",
|
|
22
|
-
"notifications",
|
|
23
|
-
"ai-agent",
|
|
24
|
-
"claude",
|
|
25
|
-
"cursor",
|
|
26
|
-
"telegram",
|
|
27
|
-
"sms",
|
|
28
|
-
"email",
|
|
29
|
-
"desktop-notifications",
|
|
30
|
-
"do-not-disturb",
|
|
31
|
-
"idle-detection",
|
|
32
|
-
"human-in-the-loop",
|
|
33
|
-
"ask-tool",
|
|
34
|
-
"two-way",
|
|
35
|
-
"sse"
|
|
36
|
-
],
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "https://github.com/menih/notify-mcp"
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">=18"
|
|
44
|
-
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "tsc && tsc -p ui/tsconfig.json",
|
|
47
|
-
"build:mcp": "tsc",
|
|
48
|
-
"build:ui": "tsc -p ui/tsconfig.json",
|
|
49
|
-
"start": "node dist/index.js",
|
|
50
|
-
"ui": "npm run build:ui && node dist/ui/server.js",
|
|
51
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
52
|
-
"prepublishOnly": "npm run build:mcp"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
56
|
-
"express": "^4.19.2",
|
|
57
|
-
"googleapis": "^144.0.0",
|
|
58
|
-
"msedge-tts": "^2.0.5",
|
|
59
|
-
"node-notifier": "^10.0.1",
|
|
60
|
-
"nodemailer": "^6.9.9",
|
|
61
|
-
"open": "^10.1.0",
|
|
62
|
-
"twilio": "^5.3.0",
|
|
63
|
-
"zod": "^3.22.4"
|
|
64
|
-
},
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@types/express": "^4.17.21",
|
|
67
|
-
"@types/node": "^20.0.0",
|
|
68
|
-
"@types/node-notifier": "^8.0.5",
|
|
69
|
-
"@types/nodemailer": "^6.4.14",
|
|
70
|
-
"playwright": "^1.59.1",
|
|
71
|
-
"ts-node": "^10.9.2",
|
|
72
|
-
"typescript": "^5.4.0"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "omni-notify-mcp",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"description": "An MCP server that lets AI agents (Claude, Cursor, etc.) reach you on any channel — desktop, Telegram, SMS, email — with two-way ask/reply, real-time inbox push, Do Not Disturb, idle gating, multi-session routing, and a one-page web UI for setup. Zero config code; configure once, agents call notify/ask.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"omni-notify-mcp": "dist/index.js",
|
|
9
|
+
"omni-notify-ui": "dist/ui/server.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"ui/public/",
|
|
14
|
+
"assets/",
|
|
15
|
+
"config.example.json",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"notifications",
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"telegram",
|
|
27
|
+
"sms",
|
|
28
|
+
"email",
|
|
29
|
+
"desktop-notifications",
|
|
30
|
+
"do-not-disturb",
|
|
31
|
+
"idle-detection",
|
|
32
|
+
"human-in-the-loop",
|
|
33
|
+
"ask-tool",
|
|
34
|
+
"two-way",
|
|
35
|
+
"sse"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/menih/notify-mcp"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc && tsc -p ui/tsconfig.json",
|
|
47
|
+
"build:mcp": "tsc",
|
|
48
|
+
"build:ui": "tsc -p ui/tsconfig.json",
|
|
49
|
+
"start": "node dist/index.js",
|
|
50
|
+
"ui": "npm run build:ui && node dist/ui/server.js",
|
|
51
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
52
|
+
"prepublishOnly": "npm run build:mcp"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
56
|
+
"express": "^4.19.2",
|
|
57
|
+
"googleapis": "^144.0.0",
|
|
58
|
+
"msedge-tts": "^2.0.5",
|
|
59
|
+
"node-notifier": "^10.0.1",
|
|
60
|
+
"nodemailer": "^6.9.9",
|
|
61
|
+
"open": "^10.1.0",
|
|
62
|
+
"twilio": "^5.3.0",
|
|
63
|
+
"zod": "^3.22.4"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/express": "^4.17.21",
|
|
67
|
+
"@types/node": "^20.0.0",
|
|
68
|
+
"@types/node-notifier": "^8.0.5",
|
|
69
|
+
"@types/nodemailer": "^6.4.14",
|
|
70
|
+
"playwright": "^1.59.1",
|
|
71
|
+
"ts-node": "^10.9.2",
|
|
72
|
+
"typescript": "^5.4.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/ui/public/app.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
let config = {};
|
|
4
4
|
const dirty = new Set();
|
|
5
5
|
|
|
6
|
+
// ── Card collapse/expand ──────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function toggleCard(id) {
|
|
9
|
+
const card = document.getElementById('card-' + id);
|
|
10
|
+
if (card) card.classList.toggle('expanded');
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
7
14
|
|
|
8
15
|
async function init() {
|
package/ui/public/index.html
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
<!-- ── Desktop ───────────────────────────────────────────────── -->
|
|
31
31
|
<div class="card" id="card-desktop">
|
|
32
|
-
<div class="card-hd">
|
|
32
|
+
<div class="card-hd" onclick="toggleCard('desktop')">
|
|
33
33
|
<div class="ch-meta">
|
|
34
34
|
<div class="channel-icon desktop-icon">
|
|
35
35
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
<span class="ch-name">Desktop</span>
|
|
38
38
|
</div>
|
|
39
39
|
<span class="badge badge-idle" id="badge-desktop">–</span>
|
|
40
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
40
41
|
</div>
|
|
41
42
|
<div class="card-body">
|
|
42
43
|
<div class="actions">
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
|
|
74
75
|
<!-- ── Email ─────────────────────────────────────────────────── -->
|
|
75
76
|
<div class="card" id="card-email">
|
|
76
|
-
<div class="card-hd">
|
|
77
|
+
<div class="card-hd" onclick="toggleCard('email')">
|
|
77
78
|
<div class="ch-meta">
|
|
78
79
|
<div class="channel-icon gmail-icon">
|
|
79
80
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
<span class="ch-name">Email</span><span class="tag">Gmail</span>
|
|
82
83
|
</div>
|
|
83
84
|
<span class="badge badge-idle" id="badge-email">Not configured</span>
|
|
85
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
84
86
|
</div>
|
|
85
87
|
<div class="card-body">
|
|
86
88
|
<!-- Connected -->
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
|
|
123
125
|
<!-- ── Telegram ──────────────────────────────────────────────── -->
|
|
124
126
|
<div class="card" id="card-telegram">
|
|
125
|
-
<div class="card-hd">
|
|
127
|
+
<div class="card-hd" onclick="toggleCard('telegram')">
|
|
126
128
|
<div class="ch-meta">
|
|
127
129
|
<div class="channel-icon telegram-icon">
|
|
128
130
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
@@ -130,6 +132,7 @@
|
|
|
130
132
|
<span class="ch-name">Telegram</span><span class="tag">Bot API</span>
|
|
131
133
|
</div>
|
|
132
134
|
<span class="badge badge-idle" id="badge-telegram">Not configured</span>
|
|
135
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
133
136
|
</div>
|
|
134
137
|
<div class="card-body">
|
|
135
138
|
<div class="actions">
|
|
@@ -163,7 +166,7 @@
|
|
|
163
166
|
|
|
164
167
|
<!-- ── SMS ───────────────────────────────────────────────────── -->
|
|
165
168
|
<div class="card" id="card-sms">
|
|
166
|
-
<div class="card-hd">
|
|
169
|
+
<div class="card-hd" onclick="toggleCard('sms')">
|
|
167
170
|
<div class="ch-meta">
|
|
168
171
|
<div class="channel-icon sms-icon">
|
|
169
172
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
@@ -171,6 +174,7 @@
|
|
|
171
174
|
<span class="ch-name">SMS</span><span class="tag">Twilio</span>
|
|
172
175
|
</div>
|
|
173
176
|
<span class="badge badge-idle" id="badge-sms">Not configured</span>
|
|
177
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
174
178
|
</div>
|
|
175
179
|
<div class="card-body">
|
|
176
180
|
<div class="actions">
|
|
@@ -201,7 +205,7 @@
|
|
|
201
205
|
|
|
202
206
|
<!-- ── ntfy ─────────────────────────────────────────────────── -->
|
|
203
207
|
<div class="card" id="card-ntfy">
|
|
204
|
-
<div class="card-hd">
|
|
208
|
+
<div class="card-hd" onclick="toggleCard('ntfy')">
|
|
205
209
|
<div class="ch-meta">
|
|
206
210
|
<div class="channel-icon ntfy-icon">
|
|
207
211
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
|
@@ -209,6 +213,7 @@
|
|
|
209
213
|
<span class="ch-name">ntfy</span><span class="tag">Push</span>
|
|
210
214
|
</div>
|
|
211
215
|
<span class="badge badge-idle" id="badge-ntfy">Not configured</span>
|
|
216
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
212
217
|
</div>
|
|
213
218
|
<div class="card-body">
|
|
214
219
|
<div class="actions">
|
|
@@ -237,7 +242,7 @@
|
|
|
237
242
|
|
|
238
243
|
<!-- ── Discord ───────────────────────────────────────────────── -->
|
|
239
244
|
<div class="card" id="card-discord">
|
|
240
|
-
<div class="card-hd">
|
|
245
|
+
<div class="card-hd" onclick="toggleCard('discord')">
|
|
241
246
|
<div class="ch-meta">
|
|
242
247
|
<div class="channel-icon discord-icon">
|
|
243
248
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><path d="M7.5 7.5c1-.5 2.5-.5 4.5-.5s3.5 0 4.5.5M7.5 16.5c1 .5 2.5.5 4.5.5s3.5 0 4.5-.5"/><path d="M3 3l4.5 1.5M21 3l-4.5 1.5M3 21l4.5-1.5M21 21l-4.5-1.5"/><path d="M3 3c0 9 0 12 9 15 9-3 9-6 9-15"/></svg>
|
|
@@ -245,6 +250,7 @@
|
|
|
245
250
|
<span class="ch-name">Discord</span><span class="tag">Webhook</span>
|
|
246
251
|
</div>
|
|
247
252
|
<span class="badge badge-idle" id="badge-discord">Not configured</span>
|
|
253
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
248
254
|
</div>
|
|
249
255
|
<div class="card-body">
|
|
250
256
|
<div class="actions">
|
|
@@ -270,7 +276,7 @@
|
|
|
270
276
|
|
|
271
277
|
<!-- ── Slack ─────────────────────────────────────────────────── -->
|
|
272
278
|
<div class="card" id="card-slack">
|
|
273
|
-
<div class="card-hd">
|
|
279
|
+
<div class="card-hd" onclick="toggleCard('slack')">
|
|
274
280
|
<div class="ch-meta">
|
|
275
281
|
<div class="channel-icon slack-icon">
|
|
276
282
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"/><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"/><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"/></svg>
|
|
@@ -278,6 +284,7 @@
|
|
|
278
284
|
<span class="ch-name">Slack</span><span class="tag">Webhook</span>
|
|
279
285
|
</div>
|
|
280
286
|
<span class="badge badge-idle" id="badge-slack">Not configured</span>
|
|
287
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
281
288
|
</div>
|
|
282
289
|
<div class="card-body">
|
|
283
290
|
<div class="actions">
|
|
@@ -302,7 +309,7 @@
|
|
|
302
309
|
|
|
303
310
|
<!-- ── Teams ─────────────────────────────────────────────────── -->
|
|
304
311
|
<div class="card" id="card-teams">
|
|
305
|
-
<div class="card-hd">
|
|
312
|
+
<div class="card-hd" onclick="toggleCard('teams')">
|
|
306
313
|
<div class="ch-meta">
|
|
307
314
|
<div class="channel-icon teams-icon">
|
|
308
315
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
@@ -310,6 +317,7 @@
|
|
|
310
317
|
<span class="ch-name">Teams</span><span class="tag">Webhook</span>
|
|
311
318
|
</div>
|
|
312
319
|
<span class="badge badge-idle" id="badge-teams">Not configured</span>
|
|
320
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
313
321
|
</div>
|
|
314
322
|
<div class="card-body">
|
|
315
323
|
<div class="actions">
|
|
@@ -344,7 +352,7 @@
|
|
|
344
352
|
|
|
345
353
|
<!-- ── Do Not Disturb ────────────────────────────────────────── -->
|
|
346
354
|
<div class="card" id="card-dnd">
|
|
347
|
-
<div class="card-hd">
|
|
355
|
+
<div class="card-hd" onclick="toggleCard('dnd')">
|
|
348
356
|
<div class="ch-meta">
|
|
349
357
|
<div class="channel-icon" style="background:#3b2f12;color:#f59e0b">
|
|
350
358
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg>
|
|
@@ -352,6 +360,7 @@
|
|
|
352
360
|
<span class="ch-name">Do Not Disturb</span>
|
|
353
361
|
</div>
|
|
354
362
|
<span class="badge badge-idle" id="badge-dnd">–</span>
|
|
363
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
355
364
|
</div>
|
|
356
365
|
<div class="card-body">
|
|
357
366
|
<div class="actions">
|
|
@@ -391,7 +400,7 @@
|
|
|
391
400
|
|
|
392
401
|
<!-- ── Idle gating ───────────────────────────────────────────── -->
|
|
393
402
|
<div class="card" id="card-idle">
|
|
394
|
-
<div class="card-hd">
|
|
403
|
+
<div class="card-hd" onclick="toggleCard('idle')">
|
|
395
404
|
<div class="ch-meta">
|
|
396
405
|
<div class="channel-icon" style="background:#12293b;color:#3b82f6">
|
|
397
406
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
@@ -399,6 +408,7 @@
|
|
|
399
408
|
<span class="ch-name">Idle gating</span>
|
|
400
409
|
</div>
|
|
401
410
|
<span class="badge badge-idle" id="badge-idle">–</span>
|
|
411
|
+
<svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
|
402
412
|
</div>
|
|
403
413
|
<div class="card-body">
|
|
404
414
|
<div class="actions">
|
package/ui/public/style.css
CHANGED
|
@@ -264,10 +264,14 @@ main {
|
|
|
264
264
|
align-items: center;
|
|
265
265
|
justify-content: space-between;
|
|
266
266
|
padding: 10px 14px;
|
|
267
|
-
border-bottom: 2px solid #4a4a5a;
|
|
268
267
|
gap: 8px;
|
|
269
268
|
flex-shrink: 0;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
user-select: none;
|
|
271
|
+
transition: background .1s;
|
|
270
272
|
}
|
|
273
|
+
.card-hd:hover { background: rgba(255,255,255,0.03); }
|
|
274
|
+
.card.expanded .card-hd { border-bottom: 2px solid #4a4a5a; }
|
|
271
275
|
.ch-meta {
|
|
272
276
|
display: flex;
|
|
273
277
|
align-items: center;
|
|
@@ -275,13 +279,23 @@ main {
|
|
|
275
279
|
}
|
|
276
280
|
.ch-name { font-size: 14px; font-weight: 600; }
|
|
277
281
|
|
|
282
|
+
.card-chevron {
|
|
283
|
+
width: 14px;
|
|
284
|
+
height: 14px;
|
|
285
|
+
flex-shrink: 0;
|
|
286
|
+
color: var(--text-3);
|
|
287
|
+
transition: transform .2s;
|
|
288
|
+
}
|
|
289
|
+
.card.expanded .card-chevron { transform: rotate(180deg); }
|
|
290
|
+
|
|
278
291
|
.card-body {
|
|
279
292
|
padding: 12px 14px;
|
|
280
|
-
display:
|
|
293
|
+
display: none;
|
|
281
294
|
flex-direction: column;
|
|
282
295
|
gap: 10px;
|
|
283
296
|
flex: 1;
|
|
284
297
|
}
|
|
298
|
+
.card.expanded .card-body { display: flex; }
|
|
285
299
|
|
|
286
300
|
/* ── Channel icons ───────────────────────────────────────────────────────── */
|
|
287
301
|
|