EXTENSION for TreeOS
notifications
The notification infrastructure for the entire extension ecosystem. Owns the Notification model, a Mongoose schema with userId, rootId, type, title, content, dreamSessionIds, and createdAt fields, indexed by userId and createdAt for fast reverse-chronological queries. This extension does not generate notifications itself. It provides the storage layer and query API that other extensions use to create and retrieve notifications. Extensions like dreams, gateway, and any custom extension create notifications by importing the Notification model from this extension's exports and writing documents directly. The type field is a freeform string owned by the creating extension (e.g., 'dream-summary', 'dream-thought'), allowing each extension to define its own notification taxonomy. The rootId field ties every notification to a specific tree, so notifications can be scoped per-tree or queried globally for a user. The query function getNotifications supports filtering by userId (required), rootId (optional), and sinceDays (only return notifications from the last N days). Pagination is handled through limit (default 50, max 100) and offset parameters. The GET /user/:userId/notifications route exposes this query function over HTTP with optional authentication, so both authenticated clients and public consumers can access notifications where permitted. Results include the notification array and total count for pagination. The extension exports both getNotifications and the Notification model for direct use by other extensions.
v1.0.2 by TreeOS Site 0 downloads 9 files 540 lines 17.0 KB published 38d ago
treeos ext install notifications
View changelog

Manifest

Provides

  • 1 models
  • routes

Requires

  • models: Node

Optional

  • extensions: html-rendering, treeos-base
SHA256: e43dd73503f8bf5097481ca1b3754186498287cba30e5d0c612691ccc5200cf8

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

Source Code

1// 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}
44
1import 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}
42
1import 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}
36
1export 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};
48
1import 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;
49
1{
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}
190
1{
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}
92
1import 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

Versions

Version Published Downloads
1.0.2 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star notifications

Comments

Loading comments...

Post comments from the CLI: treeos ext comment notifications "your comment"
Max 3 comments per extension. One star and one flag per user.