e43dd73503f8bf5097481ca1b3754186498287cba30e5d0c612691ccc5200cf81// notifications/core.js
2// Query functions for the Notification model.
3
4import Notification from "./model.js";
5
6/**
7 * Get notifications for a user, optionally filtered by rootId.
8 * @param {object} opts
9 * @param {string} opts.userId - required
10 * @param {string} [opts.rootId] - filter to a single tree
11 * @param {number} [opts.limit] - max results (default 50, max 100)
12 * @param {number} [opts.offset] - pagination offset
13 * @param {number} [opts.sinceDays] - only return notifications from the last N days
14 * @returns {{ notifications: object[], total: number }}
15 */
16export async function getNotifications({
17 userId,
18 rootId,
19 limit = 50,
20 offset = 0,
21 sinceDays,
22} = {}) {
23 const filter = { userId };
24 if (rootId) filter.rootId = rootId;
25 if (sinceDays) {
26 const since = new Date();
27 since.setDate(since.getDate() - sinceDays);
28 filter.createdAt = { $gte: since };
29 }
30
31 const safeLimit = Math.min(Math.max(limit, 1), 100);
32
33 const [notifications, total] = await Promise.all([
34 Notification.find(filter)
35 .sort({ createdAt: -1 })
36 .skip(offset)
37 .limit(safeLimit)
38 .lean(),
39 Notification.countDocuments(filter),
40 ]);
41
42 return { notifications, total };
43}
441import express from "express";
2import log from "../../seed/log.js";
3import Node from "../../seed/models/node.js";
4import User from "../../seed/models/user.js";
5import { sendError, ERR } from "../../seed/protocol.js";
6import urlAuth from "../html-rendering/urlAuth.js";
7import { htmlOnly, buildQS, tokenQS } from "../html-rendering/htmlHelpers.js";
8import { getExtension } from "../loader.js";
9import { renderNotifications } from "./pages/notifications.js";
10
11export default function buildNotificationsHtmlRoutes() {
12 const router = express.Router();
13
14 router.get("/user/:userId/notifications", urlAuth, htmlOnly, async (req, res) => {
15 try {
16 const { userId } = req.params;
17 const user = await User.findById(userId).select("username").lean();
18 if (!user) return sendError(res, 404, ERR.USER_NOT_FOUND, "User not found");
19
20 const notifExt = getExtension("notifications");
21 if (!notifExt?.exports?.getNotifications) return sendError(res, 404, ERR.EXTENSION_NOT_FOUND, "Notifications extension not loaded");
22
23 const limit = Math.min(parseInt(req.query.limit, 10) || 50, 200);
24 const offset = parseInt(req.query.offset, 10) || 0;
25 const { notifications, total } = await notifExt.exports.getNotifications({ userId, limit, offset });
26
27 return res.send(renderNotifications({
28 userId,
29 notifications,
30 total,
31 username: user.username,
32 token: req.query.token ?? "",
33 }));
34 } catch (err) {
35 log.error("HTML", "Notifications render error:", err.message);
36 sendError(res, 500, ERR.INTERNAL, err.message);
37 }
38 });
39
40 return router;
41}
421import log from "../../seed/log.js";
2import router from "./routes.js";
3import { getNotifications } from "./core.js";
4import Notification from "./model.js";
5
6export async function init(core) {
7 log.verbose("Notifications", "Extension initialized");
8
9 try {
10 const { getExtension } = await import("../loader.js");
11 const htmlExt = getExtension("html-rendering");
12 if (htmlExt) {
13 const { default: buildHtmlRoutes } = await import("./htmlRoutes.js");
14 htmlExt.router.use("/", buildHtmlRoutes());
15 }
16 } catch {}
17
18 // Register quick link on user profile
19 try {
20 const { getExtension: getExt } = await import("../loader.js");
21 const treeos = getExt("treeos-base");
22 treeos?.exports?.registerSlot?.("user-quick-links", "notifications", ({ userId, queryString }) =>
23 `<li><a href="/api/v1/user/${userId}/notifications${queryString}">Notifications</a></li>`,
24 { priority: 40 }
25 );
26 } catch {}
27
28 return {
29 router,
30 exports: {
31 getNotifications,
32 Notification,
33 },
34 };
35}
361export default {
2 name: "notifications",
3 version: "1.0.2",
4 builtFor: "TreeOS",
5 description:
6 "The notification infrastructure for the entire extension ecosystem. Owns the Notification " +
7 "model, a Mongoose schema with userId, rootId, type, title, content, dreamSessionIds, and " +
8 "createdAt fields, indexed by userId and createdAt for fast reverse-chronological queries. " +
9 "This extension does not generate notifications itself. It provides the storage layer and " +
10 "query API that other extensions use to create and retrieve notifications.\n\n" +
11 "Extensions like dreams, gateway, and any custom extension create notifications by importing " +
12 "the Notification model from this extension's exports and writing documents directly. The " +
13 "type field is a freeform string owned by the creating extension (e.g., 'dream-summary', " +
14 "'dream-thought'), allowing each extension to define its own notification taxonomy. The " +
15 "rootId field ties every notification to a specific tree, so notifications can be scoped " +
16 "per-tree or queried globally for a user.\n\n" +
17 "The query function getNotifications supports filtering by userId (required), rootId " +
18 "(optional), and sinceDays (only return notifications from the last N days). Pagination " +
19 "is handled through limit (default 50, max 100) and offset parameters. The GET " +
20 "/user/:userId/notifications route exposes this query function over HTTP with optional " +
21 "authentication, so both authenticated clients and public consumers can access notifications " +
22 "where permitted. Results include the notification array and total count for pagination. " +
23 "The extension exports both getNotifications and the Notification model for direct use by " +
24 "other extensions.",
25
26 npm: ["web-push@^3.6.7"],
27
28 needs: {
29 models: ["Node"],
30 },
31
32 optional: {
33 extensions: ["html-rendering", "treeos-base"],
34 },
35
36 provides: {
37 models: {
38 Notification: "./model.js",
39 },
40 routes: "./routes.js",
41 tools: false,
42 jobs: false,
43 orchestrator: false,
44 energyActions: {},
45 sessionTypes: {},
46 },
47};
481import mongoose from "mongoose";
2import { v4 as uuidv4 } from "uuid";
3
4const NotificationSchema = new mongoose.Schema({
5 _id: {
6 type: String,
7 default: uuidv4,
8 },
9
10 userId: {
11 type: String,
12 ref: "User",
13 required: true,
14 },
15
16 rootId: {
17 type: String,
18 ref: "Node",
19 required: true,
20 },
21
22 type: {
23 type: String,
24 required: true,
25 },
26
27 title: {
28 type: String,
29 required: true,
30 },
31
32 content: {
33 type: String,
34 required: true,
35 },
36
37 dreamSessionIds: [String],
38
39 createdAt: {
40 type: Date,
41 default: Date.now,
42 },
43});
44
45NotificationSchema.index({ userId: 1, createdAt: -1 });
46
47const Notification = mongoose.model("Notification", NotificationSchema);
48export default Notification;
491{
2 "name": "treeos-ext-notifications",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "treeos-ext-notifications",
9 "version": "1.0.0",
10 "dependencies": {
11 "web-push": "^3.6.7"
12 }
13 },
14 "node_modules/agent-base": {
15 "version": "7.1.4",
16 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
17 "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
18 "license": "MIT",
19 "engines": {
20 "node": ">= 14"
21 }
22 },
23 "node_modules/asn1.js": {
24 "version": "5.4.1",
25 "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
26 "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
27 "license": "MIT",
28 "dependencies": {
29 "bn.js": "^4.0.0",
30 "inherits": "^2.0.1",
31 "minimalistic-assert": "^1.0.0",
32 "safer-buffer": "^2.1.0"
33 }
34 },
35 "node_modules/bn.js": {
36 "version": "4.12.3",
37 "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
38 "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
39 "license": "MIT"
40 },
41 "node_modules/buffer-equal-constant-time": {
42 "version": "1.0.1",
43 "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
44 "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
45 "license": "BSD-3-Clause"
46 },
47 "node_modules/debug": {
48 "version": "4.4.3",
49 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
50 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
51 "license": "MIT",
52 "dependencies": {
53 "ms": "^2.1.3"
54 },
55 "engines": {
56 "node": ">=6.0"
57 },
58 "peerDependenciesMeta": {
59 "supports-color": {
60 "optional": true
61 }
62 }
63 },
64 "node_modules/ecdsa-sig-formatter": {
65 "version": "1.0.11",
66 "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
67 "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
68 "license": "Apache-2.0",
69 "dependencies": {
70 "safe-buffer": "^5.0.1"
71 }
72 },
73 "node_modules/http_ece": {
74 "version": "1.2.0",
75 "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
76 "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
77 "license": "MIT",
78 "engines": {
79 "node": ">=16"
80 }
81 },
82 "node_modules/https-proxy-agent": {
83 "version": "7.0.6",
84 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
85 "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
86 "license": "MIT",
87 "dependencies": {
88 "agent-base": "^7.1.2",
89 "debug": "4"
90 },
91 "engines": {
92 "node": ">= 14"
93 }
94 },
95 "node_modules/inherits": {
96 "version": "2.0.4",
97 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
98 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
99 "license": "ISC"
100 },
101 "node_modules/jwa": {
102 "version": "2.0.1",
103 "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
104 "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
105 "license": "MIT",
106 "dependencies": {
107 "buffer-equal-constant-time": "^1.0.1",
108 "ecdsa-sig-formatter": "1.0.11",
109 "safe-buffer": "^5.0.1"
110 }
111 },
112 "node_modules/jws": {
113 "version": "4.0.1",
114 "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
115 "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
116 "license": "MIT",
117 "dependencies": {
118 "jwa": "^2.0.1",
119 "safe-buffer": "^5.0.1"
120 }
121 },
122 "node_modules/minimalistic-assert": {
123 "version": "1.0.1",
124 "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
125 "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
126 "license": "ISC"
127 },
128 "node_modules/minimist": {
129 "version": "1.2.8",
130 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
131 "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
132 "license": "MIT",
133 "funding": {
134 "url": "https://github.com/sponsors/ljharb"
135 }
136 },
137 "node_modules/ms": {
138 "version": "2.1.3",
139 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
140 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
141 "license": "MIT"
142 },
143 "node_modules/safe-buffer": {
144 "version": "5.2.1",
145 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
146 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
147 "funding": [
148 {
149 "type": "github",
150 "url": "https://github.com/sponsors/feross"
151 },
152 {
153 "type": "patreon",
154 "url": "https://www.patreon.com/feross"
155 },
156 {
157 "type": "consulting",
158 "url": "https://feross.org/support"
159 }
160 ],
161 "license": "MIT"
162 },
163 "node_modules/safer-buffer": {
164 "version": "2.1.2",
165 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
166 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
167 "license": "MIT"
168 },
169 "node_modules/web-push": {
170 "version": "3.6.7",
171 "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
172 "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
173 "license": "MPL-2.0",
174 "dependencies": {
175 "asn1.js": "^5.3.0",
176 "http_ece": "1.2.0",
177 "https-proxy-agent": "^7.0.0",
178 "jws": "^4.0.0",
179 "minimist": "^1.2.5"
180 },
181 "bin": {
182 "web-push": "src/cli.js"
183 },
184 "engines": {
185 "node": ">= 16"
186 }
187 }
188 }
189}
1901{
2 "type": "module",
3 "name": "treeos-ext-notifications",
4 "version": "1.0.0",
5 "private": true,
6 "dependencies": {
7 "web-push": "^3.6.7"
8 }
9}1import { page } from "../../html-rendering/html/layout.js";
2import { esc } from "../../html-rendering/html/utils.js";
3
4export function renderNotifications({ userId, notifications, total, username, token }) {
5 const tokenQS = token ? `?token=${encodeURIComponent(token)}&html` : `?html`;
6
7 const items = notifications
8 .map((n) => {
9 const icon = n.type === "dream-thought" ? "\uD83D\uDCAD" : "\uD83D\uDCCB";
10 const typeLabel = n.type === "dream-thought" ? "Thought" : "Summary";
11 const colorClass =
12 n.type === "dream-thought" ? "glass-purple" : "glass-indigo";
13 const date = new Date(n.createdAt).toLocaleString();
14
15 return `
16 <li class="note-card ${colorClass}">
17 <div class="note-content">
18 <div class="contribution-action">
19 <span style="font-size:20px;margin-right:6px">${icon}</span>
20 ${esc(n.title)}
21 <span class="badge badge-type">${typeLabel}</span>
22 </div>
23 <div style="margin-top:10px;font-size:14px;color:rgba(255,255,255,0.9);line-height:1.6;white-space:pre-wrap">${esc(n.content)}</div>
24 </div>
25 <div class="note-meta">
26 ${date}
27 </div>
28 </li>`;
29 })
30 .join("");
31
32 const css = `
33.header-subtitle {
34 margin-bottom: 16px;
35}
36
37
38/* ── Badges ─────────────────────────────────────── */
39
40.badge {
41 display: inline-flex; align-items: center;
42 padding: 3px 10px; border-radius: 980px;
43 font-size: 11px; font-weight: 700; letter-spacing: 0.3px;
44 border: 1px solid rgba(255,255,255,0.2);
45}
46
47.badge-type {
48 background: rgba(255,255,255,0.15);
49 color: rgba(255,255,255,0.8);
50 text-transform: uppercase;
51 letter-spacing: 0.5px;
52 font-size: 10px;
53 margin-left: 8px;
54}
55
56/* ── Responsive ─────────────────────────────────── */
57`;
58
59 const body = `
60 <div class="container">
61 <div class="back-nav">
62 <a href="/api/v1/user/${userId}${tokenQS}" class="back-link">\u2190 Back to Profile</a>
63 </div>
64
65 <div class="header">
66 <h1>
67 Notifications for
68 <a href="/api/v1/user/${userId}${tokenQS}">@${esc(username)}</a>
69 ${notifications.length > 0 ? `<span class="message-count">${total}</span>` : ""}
70 </h1>
71 <div class="header-subtitle">Dream summaries and thoughts from your trees</div>
72 </div>
73
74 ${
75 items.length
76 ? `<ul class="notes-list">${items}</ul>`
77 : `
78 <div class="empty-state">
79 <div class="empty-state-icon">\uD83D\uDD14</div>
80 <div class="empty-state-text">No notifications yet</div>
81 <div class="empty-state-subtext">Dreams will generate summaries and thoughts automatically</div>
82 </div>`
83 }
84 </div>`;
85
86 return page({
87 title: `${esc(username)} - Notifications`,
88 css,
89 body,
90 });
91}
921import log from "../../seed/log.js";
2import express from "express";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { sendOk, sendError, ERR } from "../../seed/protocol.js";
5import { getNotifications } from "./core.js";
6
7const router = express.Router();
8
9router.get("/user/:userId/notifications", authenticate, async (req, res) => {
10 try {
11 const { userId } = req.params;
12 const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
13 const offset = parseInt(req.query.offset, 10) || 0;
14
15 const { notifications, total } = await getNotifications({
16 userId,
17 rootId: req.query.rootId,
18 limit,
19 offset,
20 });
21
22 return sendOk(res, { notifications, total, limit, offset });
23 } catch (err) {
24 log.error("Notifications", "Route error:", err);
25 sendError(res, 500, ERR.INTERNAL, err.message);
26 }
27});
28
29export default router;
30
| Version | Published | Downloads |
|---|---|---|
| 1.0.2 | 38d ago | 0 |
| 1.0.0 | 48d ago | 0 |
treeos ext star notifications
Post comments from the CLI: treeos ext comment notifications "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...