forked from retoor/devplacepy
feat: add notification preference system with per-type per-channel toggles and admin defaults
Implement configurable notification preferences across in-app and push channels, including a new `notification_preferences` table with soft-delete support, per-user toggle endpoints, admin defaults management, and canonical type definitions. The change introduces `NOTIFICATION_TYPES` and `NOTIFICATION_CHANNELS` constants, `NotificationPrefForm`/`NotificationDefaultForm` models, `notification_enabled` resolution logic, and UI integration via the profile notifications tab and admin panel.
This commit is contained in:
@@ -156,6 +156,9 @@ Devii's project filesystem tools include surgical **line-range editing** (`proje
|
||||
### Per-user customization (`devplacepy/customization.py`)
|
||||
Users and guests inject their own **CSS** (look) and **JS** (behaviour), scoped to a **page type** or **globally**, configured **only through Devii** (no HTTP routes - persistence is owner-scoped from the trusted Devii session, like `LessonStore`). It is **per-owner only**: the code runs solely in that owner's own browser sessions (self-XSS, like a userscript), never in anyone else's view - never key injection off URL-supplied data. **Page type** = the matched route template (`request.scope["route"].path`, e.g. `/posts/{slug}`) via `page_type_for(request)`; one value covers all instances of a type (all posts), never one post; also rendered as `data-page-type` on `<main>` and surfaced to Devii via `DeviiClient._context().pageType`. **Owner** = `owner_for(request)`: user uid, else the `DEVII_GUEST_COOKIE` (`constants.py`, shared with `routers/devii.py`), else none. **Storage** (`database.py`): table `user_customizations` (per `owner_kind`/`owner_id`/`scope`/`lang`) with helpers `get_custom_overrides` (merges global-then-page-type, cached under the `"customizations"` cache-version name via `sync_local_cache`/`bump_cache_version`), `set_custom_override`, `delete_custom_override`. **Injection**: template globals `custom_css_tag`/`custom_js_tag` (registered in `templating.py`) emit a `<style id="user-custom-css">` after `extra_head` (CSS overrides the design system; `</`->`<\/` neutralizes breakout) and a JSON-island `<script>` after `Application.js` (`<`/`>` escaped, run via `new Function(JSON.parse(...))` so `app` exists). Both fail closed to empty Markup - a customization bug must never break page render. Kill-switches: `customization_enabled`, `customization_js_enabled`. **Devii** (`services/devii/customization/`, `handler="customization"`): `customize_list`/`get`/`set_css`/`set_js`/`reset`, all `requires_auth=False`; `CustomizationController(owner_kind, owner_id)` is built in `Dispatcher.__init__` (owner threaded from `DeviiSession`); set/reset are in `CONFIRM_REQUIRED` so `confirmation_error` forces Devii to ask **page-type vs global** before `confirm=true`. Devii previews live with `run_js` (ids `devii-preview-css`/`devii-preview-js`) and `reload_page` after saving.
|
||||
|
||||
### Notification preferences (`database.py`)
|
||||
Every notification type (`NOTIFICATION_TYPES`) is independently toggleable per user on two channels (in-app, push). Resolution `notification_enabled(user_uid, type, channel)` = live `notification_preferences` override -> admin global default (`notif_default_{type}_{channel}` setting, default `1`) -> on. Enforcement is the single funnel `utils.create_notification` (in-app insert gated by the `in_app` channel, `_schedule_push` by `push`); the `messages.py` DM path was routed through `create_notification` so it is gated too. UI: profile **Notifications** tab (`/profile/{username}?tab=notifications`, owner-or-admin, `POST /profile/{username}/notifications` + `/reset`) and admin global defaults at `/admin/notifications`. Devii tools `notification_list`/`notification_set`/`notification_reset` (`handler="notification"`, owner-scoped, `set`/`reset` in `CONFIRM_REQUIRED`). Storage is the soft-deletable `notification_preferences` table, cached under the `"notif_prefs"` cache-version name. Reuse this canonical-list + resolver + override-table + single-funnel pattern for future per-user per-channel preferences.
|
||||
|
||||
### Devii user-defined tools (`services/devii/virtual_tools/`)
|
||||
Users invent new Devii tools in natural language ("when I say woeii, do Y"); each is stored per-owner and dynamically added to Devii's LLM tool list, and when called its handler **re-prompts Devii itself** (a self-eval sub-agent) with the stored prompt plus the user's single free-form `input`. CRUD is **Devii-only** (`tool_create`/`tool_list`/`tool_get`/`tool_update`/`tool_delete`, `handler="virtual_tool"`). **Dynamic tool list:** the session tool list is normally frozen, but `DeviiSession._refresh_tools()` runs at the top of every `_run_turn` and rewrites `self.tools` **in place** (`self.tools[:] = CATALOG.tool_schemas_for(...) + virtual_store.tool_schemas()`); since `Agent`/`AgenticController`/`react_loop` share that list object, new/edited/deleted tools take effect next turn with no rebuild - never reassign `self.tools`, always mutate in place. **Self-eval engine:** `agentic/controller.py` `_spawn` (shared by `_delegate`, `run_subagent`, and the new `eval` tool) runs `react_loop` under a contextvar **depth guard** (`agentic/state.py`, `MAX_EVAL_DEPTH=2`) so self-calls can't loop. **Store** `VirtualToolStore(db, owner_kind, owner_id)` over `devii_virtual_tools` (persistent for users, `memory_db()` for guests, built in `hub.get_or_create`). **Dispatch:** the dispatcher's unknown-name branch (`if action is None`) resolves a virtual tool via `self._virtual_tools.has/run` before erroring, so virtual tools live only in the store, not the static catalog. Name validation rejects built-in collisions (`set(CATALOG.by_name())`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user