Resolve all 14 tracked tickets (T0-T13)
High priority fixes: - T0: Profile page now filters comments by namespace (was leaking cross-site) - T1: URI hostnames and namespace names normalized to lowercase (was causing duplicate threads and "stock comments" bug). Includes merge script. - T2: Thread detail API now paginated with SQL-side filtering (was 502 on 267+ reply threads) Features: - T3: GDPR account deletion (tombstone user with scrubbed PII) and data export - T4: Customizable button text and comment labels per namespace - T5: Self-service namespace deletion for owners - T6: @mention notifications with profile links - T7: Webmention receiving endpoint with h-card extraction - T8: Configurable max nesting depth and collapse depth per namespace - T9: AJAX thread title search to prevent duplicates - T10: Browser push notification support (VAPID/service worker) Docs and housekeeping: - T11: Documented thread_uri behavior when moving embeds - T12/T13: Drafted community replies for resolved feature requests - Collapse depth defaults to infinite (load-more disabled unless configured) 364 tests pass, 4 skipped.
This commit is contained in:
parent
c5823c62ac
commit
f1cffe2e79
59 changed files with 6800 additions and 131 deletions
90
docs/thread-uri.md
Normal file
90
docs/thread-uri.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Thread URI: How Comments Are Tied to Page URLs
|
||||
|
||||
## How It Works
|
||||
|
||||
Remarkbox threads are keyed by the **page URL** (called the `thread_uri`).
|
||||
When you embed Remarkbox on a page, the embed script automatically reads
|
||||
`window.location.href` from the parent page and uses it as the thread
|
||||
identifier. All comments posted on that page are stored under that URL.
|
||||
|
||||
This means:
|
||||
|
||||
- Each unique URL gets its own comment thread.
|
||||
- If the same embed snippet appears on two different URLs, each page has
|
||||
its own independent thread.
|
||||
- If you move the embed snippet to a different page (different URL), the
|
||||
new page will show an empty thread because the URL changed.
|
||||
|
||||
## "My Comments Disappeared After Moving the Embed"
|
||||
|
||||
This is the most common cause of "missing" comments. Your comments are not
|
||||
deleted -- they are still stored under the original page URL. You can
|
||||
confirm this by checking the Remarkbox dashboard, where all threads are
|
||||
listed regardless of which page currently has the embed.
|
||||
|
||||
When you move the embed snippet from `https://example.com/old-page` to
|
||||
`https://example.com/new-page`, Remarkbox sees `new-page` as a brand new
|
||||
thread with no comments.
|
||||
|
||||
## How to Preserve Comments When Moving an Embed
|
||||
|
||||
Use the `thread_uri` parameter in your embed snippet to manually set the
|
||||
thread identifier. This overrides the automatic URL detection.
|
||||
|
||||
### Standard Embed (automatic URL detection)
|
||||
|
||||
```html
|
||||
<div id="remarkbox-div">
|
||||
<noscript disabled>
|
||||
<i>JavaScript is required to load the comments.</i>
|
||||
</noscript>
|
||||
</div>
|
||||
<script src="https://my.remarkbox.com/static/js/iframe-resizer/iframeResizer.min.js"></script>
|
||||
<script>
|
||||
var defined_thread_uri = window.location.href;
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
### Pinned Embed (manual thread_uri)
|
||||
|
||||
To keep the same comment thread even after moving the embed, set
|
||||
`defined_thread_uri` to the original page URL:
|
||||
|
||||
```html
|
||||
<script>
|
||||
// Pin this embed to the original page URL so comments follow the embed
|
||||
var defined_thread_uri = "https://example.com/old-page";
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
With this change, no matter where you place the embed snippet, it will
|
||||
always load and display the comments from `https://example.com/old-page`.
|
||||
|
||||
## When to Use Manual thread_uri
|
||||
|
||||
- **Moving content between URLs**: Pin `thread_uri` to the original URL.
|
||||
- **Staging/production parity**: Use the production URL as `thread_uri`
|
||||
so comments don't split between environments.
|
||||
- **URL canonicalization**: If your site is accessible at both `www.` and
|
||||
non-`www.` URLs, set a canonical `thread_uri` to avoid duplicate threads.
|
||||
- **Single-page applications (SPAs)**: If your SPA changes the URL hash
|
||||
or query string without a full page load, pin `thread_uri` to the
|
||||
canonical path to avoid fragmenting comments.
|
||||
|
||||
## Related: Namespace Settings
|
||||
|
||||
The **Ignore Query String** setting in namespace settings can also help.
|
||||
When enabled, Remarkbox strips query parameters from the URL before
|
||||
matching threads. This prevents URLs like `?utm_source=twitter` from
|
||||
creating separate threads.
|
||||
|
||||
## Summary
|
||||
|
||||
| Scenario | What Happens | Fix |
|
||||
|----------|-------------|-----|
|
||||
| Move embed to new URL | New empty thread appears | Set `thread_uri` to original URL |
|
||||
| Same page, URL query changes | May create new thread | Enable "Ignore Query String" in namespace settings |
|
||||
| Same page, URL fragment changes | Fragment is stripped automatically | No action needed |
|
||||
| Different environments (staging/prod) | Separate threads per environment | Set `thread_uri` to canonical production URL |
|
||||
50
docs/tickets/0.md
Normal file
50
docs/tickets/0.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# T0: User profile leaks comments across namespaces
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: high
|
||||
**Source**: meta `cc62eb06-8e4b-11ea-93cc-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Clicking a username on any site with Remarkbox installed shows ALL that user's comments from every namespace. A commenter on site A can see all their comments from sites B, C, D on the profile page. Russell marked the original thread as fixed, but the code still has no namespace filtering.
|
||||
|
||||
Xii also reported that unapproved comments leaked cross-namespace. The `approved == True` filter in `page_nodes()` now prevents that specific leak, but the cross-namespace exposure remains.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`User.page_nodes()` in `remarkbox/models/user.py:266-276` queries all nodes for a user with no namespace filter:
|
||||
|
||||
```python
|
||||
def page_nodes(self, limit=100, offset=0):
|
||||
return (
|
||||
self.nodes.filter(
|
||||
Node.disabled == False, Node.verified == True,
|
||||
Node.user_id != None, Node.approved == True
|
||||
)
|
||||
.order_by(Node.changed.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
```
|
||||
|
||||
The `user_nodes()` view in `remarkbox/views/list_nodes.py:114-137` calls `subject_user.page_nodes()` without passing any namespace context. The same issue affects `verified_nodes`, `unverified_nodes`, `disabled_nodes`, and `unapproved_nodes` properties on the User model.
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
1. Add a `namespace` parameter to `User.page_nodes()` that filters `Node.namespace_id == namespace.id`
|
||||
2. Update `user_nodes()` view to pass `request.namespace` (or derive it from the embed/site context)
|
||||
3. Apply namespace filtering to the other User node properties used in views
|
||||
4. Ensure namespace-specific settings (`hide_unless_approved`, `hide_unverified`) are respected
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/models/user.py` — add namespace filter to `page_nodes()` and related properties
|
||||
- `remarkbox/views/list_nodes.py` — pass namespace context to user queries
|
||||
- `remarkbox/tests/test_views.py` — regression test: user profile only shows same-namespace comments
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] User profile page only shows comments from the current namespace
|
||||
- [x] Namespace moderation settings are respected on the profile page
|
||||
- [x] Regression test prevents reintroduction
|
||||
58
docs/tickets/1.md
Normal file
58
docs/tickets/1.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# T1: Namespace/URI case-sensitivity causes "stock comments" bug
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: high
|
||||
**Source**: FAQ `7eb0baec-2da1-11ef-b0c7-1f90b6841245`, `6b21e360-ce62-11ef-b298-29ab4fb285a0`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Users embed Remarkbox on their site and see pre-existing comments that don't belong to them ("stock comments"). Two separate FAQ threads report this for sparklingcyber.com and acrosstheborder.blog.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Same class of bug as the duplicate email accounts issue (see `docs/postmortem-2026-01-29-duplicate-email-accounts.md`). URIs and namespace names are compared case-sensitively:
|
||||
|
||||
1. `get_uri_by_uri()` in `remarkbox/models/uri.py:82` — `Uri.data == unicode(external_uri)` is case-sensitive
|
||||
2. `get_namespace_by_name()` in `remarkbox/models/namespace.py:363` — `Namespace.name == unicode(name)` is case-sensitive
|
||||
|
||||
When a user visits `https://Example.com/page` vs `https://example.com/page`, two separate URIs, nodes, and potentially namespaces are created. The user on the lowercase variant sees an empty thread (or someone else's comments if they happen to share the same lowercase namespace).
|
||||
|
||||
### Example scenario
|
||||
|
||||
```
|
||||
Time 1: User A embeds on https://Example.com/blog
|
||||
→ Uri "https://Example.com/blog" created
|
||||
→ Namespace "Example.com" created
|
||||
→ Comments posted here
|
||||
|
||||
Time 2: User B visits https://example.com/blog
|
||||
→ Uri lookup for "https://example.com/blog" — no match (case differs)
|
||||
→ New Uri, new Node created
|
||||
→ Namespace "example.com" — no match, new namespace created (empty)
|
||||
→ User B sees no comments or wrong comments
|
||||
```
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
Follow the same pattern as the email fix:
|
||||
|
||||
1. Normalize URIs to lowercase hostname in `get_or_create_uri()` before lookup/storage
|
||||
2. Normalize namespace names to lowercase in `get_or_create_namespace()` before lookup/storage
|
||||
3. Use `func.lower()` for comparisons in lookup functions
|
||||
4. Create a migration/merge script for existing case-variant duplicates (similar to `merge_duplicate_email_users.py`)
|
||||
5. Add regression tests
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/models/uri.py` — lowercase hostname normalization in `get_or_create_uri()`, case-insensitive lookup in `get_uri_by_uri()`
|
||||
- `remarkbox/models/namespace.py` — lowercase normalization in `get_or_create_namespace()`, case-insensitive lookup in `get_namespace_by_name()`
|
||||
- `remarkbox/scripts/merge_duplicate_namespaces.py` — new script to consolidate case-variant duplicates
|
||||
- `remarkbox/tests/test_models.py` — regression tests for case-insensitive URI and namespace matching
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `https://Example.com/page` and `https://example.com/page` resolve to the same thread
|
||||
- [x] `Example.com` and `example.com` resolve to the same namespace
|
||||
- [x] Existing duplicate namespaces/URIs can be merged with a script
|
||||
- [x] Regression tests prevent reintroduction
|
||||
47
docs/tickets/10.md
Normal file
47
docs/tickets/10.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# T10: Browser push notifications
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `9b970f14-b1cd-11e7-8fca-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Users and moderators want browser push notifications in addition to email notifications, particularly for pending comments that need moderation approval.
|
||||
|
||||
## Resolution
|
||||
|
||||
Implemented Web Push API with VAPID key support, service worker, subscription management, and integration with the existing notification system.
|
||||
|
||||
### New files
|
||||
|
||||
- `remarkbox/lib/push.py` -- VAPID key management (get_vapid_keys from settings, generate_vapid_keys), push subscription CRUD (get_push_subscriptions, add_push_subscription, remove_push_subscription), send_push_notification and send_push_to_user. Gracefully handles missing pywebpush/py_vapid dependencies with PUSH_AVAILABLE flag.
|
||||
- `remarkbox/views/push.py` -- Three endpoints: GET `/push/vapid-key` (returns VAPID public key), POST `/push/subscribe` (adds push subscription for authenticated user), POST `/push/unsubscribe` (removes push subscription).
|
||||
- `remarkbox/static/js/push-sw.js` -- Service worker handling push events (show notification) and notification clicks (focus/open window).
|
||||
|
||||
### Modified files
|
||||
|
||||
- `remarkbox/models/user.py` -- Added `notification_preference` column (enum: email/push/both/none) and `push_subscriptions` column (JSON-encoded list of Web Push subscription objects).
|
||||
- `remarkbox/routes.py` -- Added push routes: `/push/vapid-key`, `/push/subscribe`, `/push/unsubscribe`.
|
||||
- `remarkbox/lib/notify.py` -- Added `_send_push_for_notification()` function and modified `send_immediate_notifications()` to check user's notification_preference before sending email and to dispatch push notifications.
|
||||
- `remarkbox/templates/user-settings.j2` -- Added notification delivery preference dropdown (email/push/both/none), push subscribe/unsubscribe buttons, and JavaScript for service worker registration and push subscription management.
|
||||
- `remarkbox/views/authenticated/authenticated.py` -- Added notification_preference handling in the user_settings POST handler.
|
||||
- `remarkbox/models/meta.py` -- Added "push" to NOTIFICATION_METHODS set.
|
||||
|
||||
### How it works
|
||||
|
||||
1. Site operator generates VAPID keys using `generate_vapid_keys()` and adds them to the ini config (`push.vapid_private_key`, `push.vapid_public_key`, `push.vapid_contact`).
|
||||
2. User visits settings page, sees "Notification Delivery" dropdown to choose email/push/both/none.
|
||||
3. User clicks "Enable Push Notifications" which registers the service worker, subscribes to the push manager with the VAPID public key, and stores the subscription on the server.
|
||||
4. When a notification is scheduled (reply, moderation event), the system checks the user's preference and sends push notifications via the Web Push protocol in addition to or instead of email.
|
||||
5. Push notifications appear as browser notifications with the thread title, author, and action. Clicking opens the relevant thread.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Push notifications require optional packages: `pywebpush` and `py_vapid`. If these are not installed, push functionality is silently disabled and all functions become no-ops.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Users can subscribe to browser push notifications
|
||||
- [x] Push notifications sent for replies and moderation events
|
||||
- [x] Users can choose between email, push, or both
|
||||
51
docs/tickets/11.md
Normal file
51
docs/tickets/11.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# T11: Document "comments disappear when moving embed"
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: FAQ `6260e726-d929-11ee-a1b7-751976fc35b2`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
A user moved the Remarkbox embed snippet from one page to another and their comments disappeared from the site (though they still appear on the Remarkbox dashboard). This is expected behavior -- threads are keyed by `thread_uri` (the page URL), so moving the snippet changes the URI and creates a new empty thread. But this isn't documented anywhere.
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
1. Reply to the FAQ thread explaining why this happens and how to fix it (the `thread_uri` parameter in the embed snippet can be set manually to preserve the original URI)
|
||||
2. Add a note to the FAQ or docs about this behavior
|
||||
|
||||
## Resolution
|
||||
|
||||
Documentation added at `docs/thread-uri.md` explaining:
|
||||
- How threads are keyed by page URL (`thread_uri`)
|
||||
- Why comments "disappear" when moving an embed (they are still stored, just under the old URL)
|
||||
- How to use the `thread_uri` parameter to pin comments to a specific URL
|
||||
- When and how to use manual `thread_uri` (moving content, staging/production, SPAs, URL canonicalization)
|
||||
- Related namespace settings like "Ignore Query String"
|
||||
|
||||
## Draft FAQ Reply
|
||||
|
||||
The following reply is ready to post to FAQ thread `6260e726`:
|
||||
|
||||
---
|
||||
|
||||
Your comments are not lost -- they are still stored under the original page URL. You can see them on your Remarkbox dashboard.
|
||||
|
||||
Remarkbox threads are keyed by the page URL (called the `thread_uri`). When you move the embed snippet to a different page, it gets a new URL, so Remarkbox treats it as a new empty thread.
|
||||
|
||||
**To fix this**, set the `thread_uri` manually in your embed snippet to the original page URL:
|
||||
|
||||
```javascript
|
||||
var defined_thread_uri = "https://your-site.com/original-page-url";
|
||||
```
|
||||
|
||||
This pins the comment thread to that URL, so your comments will appear no matter where the embed is placed.
|
||||
|
||||
We have added documentation about this behavior at `docs/thread-uri.md` in the Remarkbox repository.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] FAQ thread has a helpful reply
|
||||
- [x] Documentation explains thread_uri behavior when moving embeds
|
||||
61
docs/tickets/12.md
Normal file
61
docs/tickets/12.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# T12: Reply to API-only CRUD thread confirming done
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `6db01560-7186-11eb-92d6-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
The original "API-only access to CRUD comments" feature request thread on meta still has Russell's old reply: "No endpoints are currently planned." The JSON API has since been built and deployed.
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
Post a reply to the thread announcing the API is live, with a link to the documentation and the Python client download.
|
||||
|
||||
## Resolution
|
||||
|
||||
Reply drafted below. Ready to post to meta thread `6db01560` using the Python client or API.
|
||||
|
||||
## Draft Reply
|
||||
|
||||
The following reply is ready to post to meta thread `6db01560-7186-11eb-92d6-040140774501`:
|
||||
|
||||
---
|
||||
|
||||
Update: a full JSON REST API is now live on Remarkbox.
|
||||
|
||||
**Endpoints available:**
|
||||
|
||||
- `GET /api/v1/threads?namespace=example.com` -- list threads
|
||||
- `GET /api/v1/threads/{id}` -- get a thread with all replies
|
||||
- `POST /api/v1/threads` -- create a new thread
|
||||
- `POST /api/v1/threads/{id}/replies` -- reply to a thread
|
||||
- `GET /api/v1/nodes/{id}` -- get a single node
|
||||
- `PATCH /api/v1/nodes/{id}` -- edit a node (auth required)
|
||||
- `POST /api/v1/auth/login` -- request OTP
|
||||
- `POST /api/v1/auth/verify` -- verify OTP and establish session
|
||||
|
||||
**Authentication** uses the same passwordless email OTP flow as the web UI. Anonymous posting is also supported when the namespace allows it.
|
||||
|
||||
**Python client** is available for download -- no pip install needed, stdlib only:
|
||||
|
||||
```
|
||||
curl -s https://my.remarkbox.com/api/v1/clients/python -o remarkbox_client.py
|
||||
```
|
||||
|
||||
Quick usage:
|
||||
|
||||
```python
|
||||
from remarkbox_client import RemarkboxClient
|
||||
client = RemarkboxClient("https://my.remarkbox.com")
|
||||
threads = client.list_threads("meta.remarkbox.com")
|
||||
```
|
||||
|
||||
Full documentation is in `docs/api.md` in the Remarkbox repository. Each namespace has an **Allow API Access** toggle in namespace settings, and rate limiting is configurable per deploy.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Reply posted to meta thread `6db01560` referencing the API docs and client
|
||||
52
docs/tickets/13.md
Normal file
52
docs/tickets/13.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# T13: Reply to lock/archive thread confirming done
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `7e9d5864-84e3-11ea-836b-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
The "lock and read-only archive a thread" feature request on meta has no replies, but the feature is fully implemented. Moderators can lock/unlock threads from the UI (`show-node.j2`), backed by `views/authenticated/lock.py`. The API also enforces locked thread restrictions.
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
Post a reply to the thread confirming the feature exists, explaining where to find the lock button (next to the watch button, visible to moderators on root threads).
|
||||
|
||||
## Resolution
|
||||
|
||||
Reply drafted below. Ready to post to meta thread `7e9d5864` using the Python client or API.
|
||||
|
||||
## Draft Reply
|
||||
|
||||
The following reply is ready to post to meta thread `7e9d5864-84e3-11ea-836b-040140774501`:
|
||||
|
||||
---
|
||||
|
||||
This feature is now implemented. Moderators can lock and unlock threads directly from the Remarkbox UI.
|
||||
|
||||
**How to lock a thread:**
|
||||
|
||||
1. Navigate to the root thread you want to lock.
|
||||
2. Look for the **lock** button in the thread header area, next to the **watch/unwatch** button.
|
||||
3. Click **lock** to prevent new comments on that thread.
|
||||
4. The button changes to **unlock** so you can re-open the thread later.
|
||||
|
||||
**Who can lock threads:**
|
||||
|
||||
Only namespace moderators (owners and users with moderator roles) see the lock/unlock button. Regular commenters cannot lock threads.
|
||||
|
||||
**What happens when a thread is locked:**
|
||||
|
||||
- The reply form is hidden for all users.
|
||||
- Existing comments remain visible and readable.
|
||||
- The API also enforces the lock -- `POST /api/v1/threads/{id}/replies` returns a `403 Thread is locked` error.
|
||||
- Moderators can unlock the thread at any time to allow new comments.
|
||||
|
||||
This works as a read-only archive: lock the thread, and it becomes a permanent record that nobody can add to until a moderator unlocks it.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Reply posted to meta thread `7e9d5864` confirming the feature is implemented
|
||||
52
docs/tickets/2.md
Normal file
52
docs/tickets/2.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# T2: Large thread fetch causes 502
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: high
|
||||
**Source**: production (www.remarkbox.com homepage thread, 267+ replies)
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
`GET /api/v1/threads/{node_id}` returns 502 Bad Gateway when fetching a thread with 267+ replies. The www.remarkbox.com homepage thread is unfetchable through the API.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Multiple compounding issues:
|
||||
|
||||
1. **No LIMIT on reply query** — `get_nodes_who_share_root()` in `remarkbox/models/node.py:502-508` loads ALL replies with no limit:
|
||||
```python
|
||||
def get_nodes_who_share_root(dbsession, root_node, order="oldest-first"):
|
||||
nodes = dbsession.query(Node).filter(Node.root_id == root_node.id)
|
||||
# ... order by ...
|
||||
return nodes # no .limit()
|
||||
```
|
||||
|
||||
2. **Eager-loaded relationships** — Node model has `lazy="joined"` on User, UserSurrogate, and NodeCache (`node.py:91-95, 124-128`), multiplying data per row
|
||||
|
||||
3. **Python-side visibility filtering** — `api/views.py:173-177` loads all nodes then filters in Python with `namespace.can_see_node()`, instead of filtering in SQL
|
||||
|
||||
4. **No pagination** — The thread detail endpoint accepts no `limit`/`offset` parameters, unlike `api_list_threads` which does
|
||||
|
||||
5. **WSGI timeout** — The combined query + serialization exceeds the reverse proxy timeout, producing 502
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
1. Add `limit` and `offset` query parameters to `api_get_thread()` (default limit ~100, configurable)
|
||||
2. Move visibility filtering into SQL (use `.filter()` for disabled/approved/verified checks before `.all()`)
|
||||
3. Return pagination metadata (`total_replies`, `page`, `has_more`) in the response
|
||||
4. Consider adding `.limit()` to `get_nodes_who_share_root()` as a safety net
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/api/views.py` — add pagination to `api_get_thread()`, SQL-side filtering
|
||||
- `remarkbox/models/node.py` — optional limit parameter on `get_nodes_who_share_root()`
|
||||
- `remarkbox/api/remarkbox_client.py` — add `limit`/`offset` params to `get_thread()`
|
||||
- `remarkbox/tests/test_api_views.py` — test pagination on thread detail
|
||||
- `docs/api.md` — document pagination parameters
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `GET /api/v1/threads/{id}` returns paginated replies with a default limit
|
||||
- [x] The www.remarkbox.com homepage thread (267+ replies) is fetchable
|
||||
- [x] Response includes pagination metadata
|
||||
- [x] Client updated to support pagination
|
||||
48
docs/tickets/3.md
Normal file
48
docs/tickets/3.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# T3: GDPR/CCPA compliance tooling
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: medium
|
||||
**Source**: meta `a819d4f0-6a65-11e8-927b-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Multiple users over 6+ years have asked for tools to help namespace owners comply with GDPR/CCPA data subject requests. The thread has frustrated replies from users who expected better tooling from a privacy-focused product.
|
||||
|
||||
Currently, namespace owners can export data via `/ns/{namespace}/dump.json` (accessible from namespace settings). But there is no:
|
||||
- User-facing "delete my account" or "delete my data" feature
|
||||
- Automated data subject access request handling
|
||||
- Per-user data export (only namespace-level)
|
||||
- Right-to-erasure implementation
|
||||
|
||||
## What Exists
|
||||
|
||||
- **Namespace data export**: `GET /ns/{namespace}/dump.json` — returns all threads and comments for a namespace in JSON. Accessible to namespace owners from the settings dashboard. Includes author names, IPs, and (for production subscriptions) emails.
|
||||
- **Admin scripts**: `scripts/merge_duplicate_email_users.py` and `scripts/delete_disabled_nodes.py` exist for admin use but aren't user-facing.
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
Phase 1 (minimum viable):
|
||||
1. Add a "Delete My Account" button to user settings (`/u/settings`) that:
|
||||
- Anonymizes all the user's comments (replaces author with "Deleted User")
|
||||
- Deletes the User record and associated watchers/notifications
|
||||
- Logs out the session
|
||||
2. Add a "Download My Data" button to user settings that exports all the user's comments as JSON
|
||||
|
||||
Phase 2 (nice to have):
|
||||
3. API endpoints for the above (`DELETE /api/v1/user/profile`, `GET /api/v1/user/export`)
|
||||
4. Namespace moderator tools to handle third-party deletion requests
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/views/authenticated/authenticated.py` — add delete account and export views
|
||||
- `remarkbox/templates/user-settings.j2` — add buttons
|
||||
- `remarkbox/models/user.py` — add `anonymize()` or `delete_account()` method
|
||||
- `remarkbox/api/views.py` — optional API endpoints
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Users can delete their own account from settings
|
||||
- [x] Users can download their own data as JSON
|
||||
- [x] Deletion anonymizes comments rather than leaving orphans
|
||||
- [x] Confirmation step before deletion
|
||||
34
docs/tickets/4.md
Normal file
34
docs/tickets/4.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# T4: Customizable button text
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `c29c9c22-b1ce-11e7-8fca-040140774501`, FAQ `5a508a9c-8759-11ec-afa2-21646204cc72`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Users want to customize the "save message" submit button and the "remark"/"remarks" terminology. Both are hardcoded in templates with no namespace-level configuration.
|
||||
|
||||
Current hardcoded values:
|
||||
- `remarkbox/templates/snippets/forms.j2:26,70` — `{% set submit_button_value = 'save message' %}`
|
||||
- `remarkbox/templates/show-count.j2:4,6,8` — "No remarks", "1 remark", "X remarks"
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
1. Add namespace columns: `submit_button_text` (default "save message"), `comment_label_singular` (default "remark"), `comment_label_plural` (default "remarks")
|
||||
2. Add fields to namespace settings form
|
||||
3. Update templates to read from namespace instead of hardcoded strings
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/models/namespace.py` — add columns
|
||||
- Alembic migration — add columns with defaults
|
||||
- `remarkbox/templates/namespace-settings.j2` — add form fields
|
||||
- `remarkbox/templates/snippets/forms.j2` — use namespace values
|
||||
- `remarkbox/templates/show-count.j2` — use namespace values
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Namespace owners can set custom button text from settings
|
||||
- [x] Defaults remain "save message" and "remark"/"remarks"
|
||||
- [x] Changes are visible in the embed
|
||||
32
docs/tickets/5.md
Normal file
32
docs/tickets/5.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# T5: Self-service namespace deletion
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: FAQ `72475f88-9323-11ec-a41e-b1c8c4ae987e`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Users cannot delete a namespace/website from their dashboard. Deletion is only available via the admin CLI script `remarkbox/scripts/modify_namespace.py --delete`. A user also suggested a dashboard button to access all their sites.
|
||||
|
||||
## What Exists
|
||||
|
||||
- `remarkbox/scripts/modify_namespace.py` — admin CLI with `--delete` flag that performs cascading deletion of all nodes, events, watchers, notifications, OAuth records, and the namespace itself. Includes a confirmation prompt.
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
1. Add a "Delete Namespace" button to namespace settings (with confirmation dialog)
|
||||
2. Reuse the cascading deletion logic from the admin script
|
||||
3. Restrict to namespace owners only
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/views/authenticated/authenticated.py` — add delete namespace view
|
||||
- `remarkbox/templates/namespace-settings.j2` — add delete button with confirmation
|
||||
- `remarkbox/routes.py` — add route
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Namespace owners can delete their namespace from settings
|
||||
- [x] Confirmation step prevents accidental deletion
|
||||
- [x] All associated data is cleaned up (nodes, watchers, etc.)
|
||||
33
docs/tickets/6.md
Normal file
33
docs/tickets/6.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# T6: Mention notifications (@username)
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `b82332ce-b4cb-11e7-b510-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Users want to @mention other Remarkbox users in comments and have those users receive notifications. Currently only reply notifications exist (notify when someone replies to your comment).
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
1. Parse comment body for `@username` patterns on save
|
||||
2. Look up mentioned users and create notifications for them
|
||||
3. Render @mentions as links to the user's profile
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/lib/mentions.py` — new module: parse @mentions, resolve users, replace with links
|
||||
- `remarkbox/lib/render.py` — integrate mention resolution into markdown_to_html pipeline
|
||||
- `remarkbox/models/node.py` — pass dbsession through set_data() for mention resolution
|
||||
- `remarkbox/lib/notify.py` — add get_mentioned_user_watchers() for mention notifications
|
||||
- `remarkbox/views/reply_node.py` — pass dbsession to set_data()
|
||||
- `remarkbox/views/new_thread.py` — pass dbsession to set_data()
|
||||
- `remarkbox/api/views.py` — pass dbsession to set_data() in API endpoints
|
||||
- `remarkbox/static/css/common.css` — CSS for .mention class
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] @username in a comment triggers a notification to that user
|
||||
- [x] Mentioned usernames are rendered as links
|
||||
- [x] Non-existent usernames are left as plain text
|
||||
53
docs/tickets/7.md
Normal file
53
docs/tickets/7.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# T7: Webmentions / IndieWeb support
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `bd7112ff-486b-11ec-aee0-21646204cc72`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
A user requested Webmention support (an IndieWeb standard for cross-site comment notifications). Russell expressed interest but said he lacks the expertise and would need a mentor.
|
||||
|
||||
## Context
|
||||
|
||||
Webmentions allow sites to notify each other when content is linked. For Remarkbox, this would mean:
|
||||
- Receiving webmentions when someone links to a Remarkbox thread from their own site
|
||||
- Sending webmentions when a Remarkbox comment links to an external URL
|
||||
- Displaying received webmentions alongside regular comments
|
||||
|
||||
Reference: https://indieweb.org/Webmention
|
||||
|
||||
## Resolution
|
||||
|
||||
Implemented W3C Webmention receiving endpoint with verification, author extraction, and display.
|
||||
|
||||
### New files
|
||||
|
||||
- `remarkbox/models/webmention.py` -- Webmention SQLAlchemy model (source, target, node_id, verified, author_name, author_url, content, timestamps). Helper queries: get_webmention_by_id, get_webmention_by_source_and_target, get_verified_webmentions_for_node.
|
||||
- `remarkbox/views/webmention.py` -- POST endpoint for receiving webmentions. Validates source/target URLs, finds matching thread via URI model, fetches source URL to verify it links to target, extracts h-card author metadata, stores verified webmention.
|
||||
|
||||
### Modified files
|
||||
|
||||
- `remarkbox/models/meta.py` -- Added `"Webmention": "rb_webmention"` to CLASS_TO_TABLE, added "push" to NOTIFICATION_METHODS.
|
||||
- `remarkbox/models/__init__.py` -- Added `from .webmention import *`.
|
||||
- `remarkbox/routes.py` -- Added `webmention` route at `/webmention`.
|
||||
- `remarkbox/api/__init__.py` -- Added `api-webmention` route at `/api/v1/webmention`.
|
||||
- `remarkbox/__init__.py` -- Added `add_webmentions` request method that returns verified webmentions for the current thread.
|
||||
- `remarkbox/templates/show-node.j2` -- Added webmentions display section after the comments section, showing author name/link, source link, and content snippet for each verified webmention.
|
||||
|
||||
### How it works
|
||||
|
||||
1. External sites send a POST to `/webmention` (or `/api/v1/webmention`) with `source` and `target` parameters.
|
||||
2. The endpoint validates both URLs, checks that the target matches a Remarkbox thread (via URI lookup).
|
||||
3. The source URL is fetched and verified to contain a link to the target.
|
||||
4. Author info is extracted from h-card microformats in the source HTML.
|
||||
5. A content snippet is extracted from around the target link.
|
||||
6. The webmention is stored and marked as verified.
|
||||
7. Verified webmentions are displayed at the bottom of the thread page.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Research completed on webmention protocol requirements
|
||||
- [x] Receiving endpoint implemented
|
||||
- [x] Webmentions displayed alongside comments
|
||||
34
docs/tickets/8.md
Normal file
34
docs/tickets/8.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# T8: Nesting depth settings per namespace
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `137101ac-7eb6-11e7-8b77-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
Namespace owners want to control:
|
||||
- Maximum nesting depth for replies (currently unlimited)
|
||||
- The depth at which a "load more" button appears instead of inline display
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
1. Add namespace columns: `max_nesting_depth` (default NULL = unlimited), `collapse_depth` (default NULL = never collapse)
|
||||
2. Add fields to namespace settings
|
||||
3. Enforce max depth in reply views (both web and API)
|
||||
4. Add collapse logic in templates
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/models/namespace.py` — add columns
|
||||
- Alembic migration
|
||||
- `remarkbox/templates/namespace-settings.j2` — add fields
|
||||
- `remarkbox/views/reply_node.py` — enforce max depth
|
||||
- `remarkbox/api/views.py` — enforce max depth on API reply
|
||||
- `remarkbox/templates/show-node.j2` — collapse at depth threshold
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Namespace owners can set max nesting depth
|
||||
- [x] Replies beyond max depth are rejected
|
||||
- [x] Deep threads collapse with a "load more" button
|
||||
30
docs/tickets/9.md
Normal file
30
docs/tickets/9.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# T9: Prevent duplicate threads (AJAX search)
|
||||
|
||||
**Status**: resolved
|
||||
**Priority**: low
|
||||
**Source**: meta `642831b4-4cbf-11e9-9d67-040140774501`
|
||||
**Filed**: 2026-02-01
|
||||
|
||||
## Problem
|
||||
|
||||
On standalone and FAQ sites, users can create threads with duplicate titles. There's no feedback showing existing threads before creation.
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
1. Add an AJAX search endpoint that returns threads matching a title prefix
|
||||
2. Wire it into the "new thread" form with a debounced typeahead
|
||||
3. Show matching existing threads as suggestions before submission
|
||||
|
||||
## Files
|
||||
|
||||
- `remarkbox/api/__init__.py` — added `api-threads-search` route
|
||||
- `remarkbox/api/views.py` — added `api_search_threads()` endpoint (GET /api/v1/threads/search)
|
||||
- `remarkbox/static/js/custom.js` — debounced typeahead for thread title input
|
||||
- `remarkbox/static/css/common.css` — CSS for suggestion dropdown
|
||||
- `remarkbox/templates/base.j2` — added `data-namespace` attribute to body tag
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Typing a thread title shows matching existing threads
|
||||
- [x] Users can click a suggestion to navigate to the existing thread
|
||||
- [x] New thread creation still works when no match exists
|
||||
20
docs/tickets/index.md
Normal file
20
docs/tickets/index.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Remarkbox Tickets
|
||||
|
||||
Tracked issues from the meta.remarkbox.com and faq.remarkbox.com audit (2026-02-01).
|
||||
|
||||
| # | Title | Status | Priority | Source |
|
||||
|---|-------|--------|----------|--------|
|
||||
| [T0](0.md) | User profile leaks comments across namespaces | resolved | high | meta `cc62eb06` |
|
||||
| [T1](1.md) | Namespace/URI case-sensitivity causes "stock comments" | resolved | high | FAQ `7eb0baec`, `6b21e360` |
|
||||
| [T2](2.md) | Large thread fetch causes 502 | resolved | high | production `www.remarkbox.com` |
|
||||
| [T3](3.md) | GDPR/CCPA compliance tooling | resolved | medium | meta `a819d4f0` |
|
||||
| [T4](4.md) | Customizable button text | resolved | low | meta `c29c9c22`, FAQ `5a508a9c` |
|
||||
| [T5](5.md) | Self-service namespace deletion | resolved | low | FAQ `72475f88` |
|
||||
| [T6](6.md) | Mention notifications (@username) | resolved | low | meta `b82332ce` |
|
||||
| [T7](7.md) | Webmentions / IndieWeb support | resolved | low | meta `bd7112ff` |
|
||||
| [T8](8.md) | Nesting depth settings per namespace | resolved | low | meta `137101ac` |
|
||||
| [T9](9.md) | Prevent duplicate threads (AJAX search) | resolved | low | meta `642831b4` |
|
||||
| [T10](10.md) | Browser push notifications | resolved | low | meta `9b970f14` |
|
||||
| [T11](11.md) | Document "comments disappear when moving embed" | resolved | low | FAQ `6260e726` |
|
||||
| [T12](12.md) | Reply to API-only CRUD thread confirming done | resolved | low | meta `6db01560` |
|
||||
| [T13](13.md) | Reply to lock/archive thread confirming done | resolved | low | meta `7e9d5864` |
|
||||
Loading…
Add table
Add a link
Reference in a new issue