Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
16 KiB
Phase 8 — Reader Engagement & Real-Time Admin
Status: Implemented — Linux validation pending
All work targets the SERVER/ directory. Implementation is in place; run Prisma generation, schema apply, and builds on the Linux production/dev host, not this Windows workstation.
Overview
Phase 8 adds two major capability layers on top of the production-hardened Phase 7 baseline:
- Reader engagement — comments, reading history, in-app notifications, and writer follows. Readers become active participants rather than passive consumers.
- Real-time admin — live visitor counter, live activity feed, live article view counters, and writer push notifications via Server-Sent Events (SSE).
Transport choice: Server-Sent Events for all real-time pushes. Every real-time feature in this phase is server→client only, which SSE handles natively through standard HTTP. No WebSocket upgrade headers, no nginx special config, no socket lifecycle management.
8.1 — Comments
Goal
Readers can comment on any published article. Comments appear immediately (no moderation queue). Registered users can flag a comment; flagged comments surface in the admin panel for review.
Schema
model Comment {
id Int @id @default(autoincrement())
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
articleId Int
author User @relation(fields: [authorId], references: [id])
authorId Int
replyTo Comment? @relation("Replies", fields: [replyToId], references: [id])
replyToId Int?
replies Comment[] @relation("Replies")
body String @db.VarChar(1000)
flagCount Int @default(0)
flagged Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CommentFlag {
id Int @id @default(autoincrement())
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
commentId Int
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
@@unique([commentId, userId])
}
Backend Routes
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/articles/:slug/comments |
public | List comments for article (paginated, newest first) |
POST |
/api/articles/:slug/comments |
session | Post a comment (body max 1000 chars) |
POST |
/api/comments/:id/flag |
session | Flag a comment (idempotent per user) |
GET |
/api/admin/comments |
editor+ | List all comments; filter ?flagged=true |
DELETE |
/api/admin/comments/:id |
editor+ | Delete a comment |
PUT |
/api/admin/comments/:id/unflag |
editor+ | Clear flag on a comment |
Business Rules
- Only authenticated users can comment or flag.
- A user may not flag the same comment twice (
CommentFlagunique index). - When
flagCount >= 5,flaggedis set totrueautomatically. Flagged comments remain visible but are highlighted in the admin panel. - Editor and admin roles can delete any comment; writers can delete their own article's comments.
- Comment body is stripped of HTML on the backend before storage.
- Posting a comment triggers a
commentnotification to the article's author (see 8.3). - Posting a reply (
replyToIdset) triggers acomment_replynotification to the parent comment's author.
Frontend
ArticleCommentSectioncomponent added to the article detail page (below article body).- Shows comment count in the article header.
- Comments load with
GET /api/articles/:slug/comments. - Logged-in users see a textarea + submit button; guests see "برای نظر دادن وارد شوید".
- Each comment shows avatar, display name,
@username, relative time, body, and a flag icon. - Admin/editor viewing an article sees a "حذف" button on each comment.
8.2 — Reading History
Goal
Every article a logged-in user opens is recorded. Users can review and clear their history from their profile.
Schema
model ReadHistory {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
articleId Int
readAt DateTime @default(now())
@@unique([userId, articleId])
}
@@unique([userId, articleId]) means a record is upserted on each visit — readAt is updated to the most recent visit, keeping the list non-duplicated.
Backend Routes
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/history |
session | Upsert a read record (body: { articleId }) |
GET |
/api/auth/history |
session | Paginated reading history (newest first) |
DELETE |
/api/auth/history |
session | Clear all reading history for current user |
Frontend
- Article detail page calls
POST /api/auth/historyafter the article body renders (fire-and-forget, no blocking). - Own profile gains a "تاریخچه خواندن" section listing articles with title, category, and
readAtdate. - A "پاک کردن تاریخچه" button with a confirmation dialog calls
DELETE /api/auth/history. - History tab is only visible on the user's own profile (not public).
8.3 — In-App Notifications
Goal
Users receive real-time notifications for: a writer they follow publishing a new article, someone replying to their comment, and (for writers) a new comment on their article. Notifications are stored in Postgres and delivered live via SSE.
Schema
enum NotificationType {
comment // someone commented on your article
comment_reply // someone replied to your comment
new_article // a writer you follow published
}
model Notification {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
type NotificationType
payload Json // { articleSlug, articleTitle, actorName, actorUsername, commentId? }
read Boolean @default(false)
createdAt DateTime @default(now())
}
Backend Routes
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/auth/notifications |
session | List notifications (paginated, unread first) |
POST |
/api/auth/notifications/read-all |
session | Mark all as read |
PATCH |
/api/auth/notifications/:id/read |
session | Mark one as read |
DELETE |
/api/auth/notifications/:id |
session | Delete a notification |
GET |
/api/auth/notifications/stream |
session | SSE stream — delivers notification events in real time |
SSE Notification Stream
GET /api/auth/notifications/stream keeps a persistent HTTP connection open. The server pushes an event whenever a new Notification row is inserted for that user. The client reconnects automatically on disconnect (native EventSource behavior).
Event format:
event: notification
data: {"id":42,"type":"comment","payload":{...},"createdAt":"..."}
Redis pub/sub is used internally to broadcast notification events from the route handler that creates them to the SSE connection handler holding the open stream. This allows multiple Fastify worker processes to deliver notifications correctly.
Frontend
- Notification bell icon in the header (visible when logged in) with an unread count badge.
- Clicking the bell opens a dropdown listing recent notifications (last 20).
- Each notification links to the relevant article.
- Unread notifications are visually distinct (bold, accent dot).
EventSourceopens/api/auth/notifications/stream; onnotificationevent, the bell badge increments and the dropdown list prepends the new item.- "علامتگذاری همه به عنوان خواندهشده" button calls
POST /api/auth/notifications/read-all.
8.4 — Follow Writers
Goal
Readers can follow any writer. When a followed writer publishes an article, followers receive a new_article notification.
Schema
model WriterFollow {
id Int @id @default(autoincrement())
follower User @relation("Follower", fields: [followerId], references: [id], onDelete: Cascade)
followerId Int
writer User @relation("Writer", fields: [writerId], references: [id], onDelete: Cascade)
writerId Int
createdAt DateTime @default(now())
@@unique([followerId, writerId])
}
Backend Routes
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/writers/:username/follow |
session | Follow a writer |
DELETE |
/api/writers/:username/follow |
session | Unfollow a writer |
GET |
/api/writers/:username/followers |
public | Follower count + list (paginated) |
GET |
/api/auth/following |
session | Writers the current user follows |
Business Rules
- A user may not follow themselves.
- Only users with role
writer,editor, oradmincan be followed (role check on the target user). - When an article is published (
status → published), the publish route queries all followers of the article's author and insertsNotificationrows of typenew_articlefor each, then publishes a Redis event to wake active SSE streams.
Frontend
- Writer profile page (
/profile/@username) shows follower count and a "دنبال کردن" / "دنبال میکنی" toggle button (visible when logged in and viewing another user). - Own profile shows a "نویسندگان دنبالشده" section with the list from
GET /api/auth/following.
8.5 — Real-Time Admin (SSE)
Goal
The admin dashboard shows live data: current visitor count (D1), a live activity feed of key events (D2), live article view counts (D4), and writers receive push alerts when their article is commented on or published (D3 — reuses the notification stream from 8.3).
SSE Admin Stream
GET /api/admin/stream — requires editor or admin role. Single endpoint multiplexes all admin real-time events.
Event types:
| Event | Payload | Trigger |
|---|---|---|
visitor_update |
{ count: number } |
Visitor counter changes (see below) |
activity |
{ type, actor, target, ts } |
Article published, user signed up, comment posted, reaction added |
view_update |
{ articleId, slug, views: number } |
Redis view buffer flushed for an article |
Visitor Counting (D1)
- When any client hits the public API (
/api/*), a Redis keyvisitors:activeis updated with a TTL-based sliding window (e.g., 5-minute window using a sorted set ofip:timestampentries). - A background tick (every 10 seconds) reads the sorted set, prunes expired entries, and publishes a
visitor_updateevent to Redis pub/sub. - The admin SSE handler subscribes and fans out to all open admin streams.
Live Activity Feed (D2)
Events are published to Redis pub/sub by the routes that perform the actions:
- Article publish route →
activity { type: "publish", actor: username, target: articleSlug } - Signup route →
activity { type: "signup", actor: username } - Comment POST route →
activity { type: "comment", actor: username, target: articleSlug } - Reaction POST route →
activity { type: "reaction", actor: username, target: articleSlug }
The admin SSE stream delivers these to the dashboard as they happen.
Live View Counter (D4)
The existing Redis view buffer (Phase 4) already batches view counts. Phase 8 extends publish-due.mjs (or a new flush-views.mjs script) to publish a view_update event to Redis pub/sub after each flush. The admin SSE stream delivers this to the dashboard.
Writer Notifications (D3)
Writers receive real-time alerts through the existing GET /api/auth/notifications/stream (8.3). No separate stream is needed. The admin SSE stream (admin role) additionally surfaces comment events in the live feed.
Frontend — Admin Dashboard
- Live visitor counter widget in the dashboard header:
"بازدیدکنندگان فعال: ۱۲"— updates in real time. - Activity feed panel: a vertical list of the last 50 events, prepended live as
activityevents arrive. Each entry shows actor, action type, target, and timestamp. - Article view counters in the articles table update in place when a
view_updateevent arrives for that article. - Admin SSE connection opens on dashboard mount, closes on unmount. A reconnect indicator shows if the stream drops.
8.6 — Admin Comment Moderation Panel
Goal
Editors and admins can review flagged comments, delete abusive ones, and clear flags on legitimate ones — all from a dedicated panel.
Frontend
- New "نظرات" tab in the admin sidebar.
- Default view: all comments (newest first), paginated.
- Toggle: "فقط گزارششده" — filters to
flagged = true. - Each row: article title, commenter name, comment body (truncated), flag count, timestamp, actions (Delete / Clear Flag).
- Bulk delete support for flagged comments.
Dependencies to Add
| Package | Location | Purpose |
|---|---|---|
| None new | — | SSE uses native Node/Fastify reply.raw; Redis pub/sub uses existing ioredis client |
No new npm dependencies are required. SSE is implemented with reply.raw (Node http.ServerResponse). Redis pub/sub uses the existing ioredis client with a second dedicated subscriber connection (pub/sub connections cannot be reused for other commands).
Schema Migration Summary
New models to add to backend/prisma/schema.prisma:
CommentCommentFlagReadHistoryNotification(withNotificationTypeenum)WriterFollow
Relations to add to existing User and Article models:
User.comments,User.commentFlags,User.readHistory,User.notifications,User.following,User.followersArticle.comments,Article.readHistory
After schema changes: npm run prisma:generate → backend build/type-check → npm run prisma:apply.
Phase 8 Checklist
Backend
- Add
Comment,CommentFlag,ReadHistory,Notification,WriterFollowto schema - Add relations to
UserandArticle - Run
prisma:generate+prisma:applyon Linux GET/POST /api/articles/:slug/commentsPOST /api/comments/:id/flagGET/DELETE/PUT /api/admin/commentsPOST/GET/DELETE /api/auth/historyGET/POST/PATCH/DELETE /api/auth/notificationsGET /api/auth/notifications/stream(SSE)POST/DELETE /api/writers/:username/followGET /api/writers/:username/followersGET /api/auth/followingGET /api/admin/stream(SSE — visitor, activity, view_update events)- Redis pub/sub subscriber connection for SSE fanout
- Visitor counting via Redis sorted set + background tick
- Activity event publishing from publish, signup, comment, and reaction routes
- View update publishing after Redis view buffer flush
- Notification insert + Redis event on comment post
- Notification insert + Redis event on comment reply
- Notification insert + Redis event on article publish (fan-out to followers)
Frontend
ArticleCommentSectioncomponent on article detail page- Comment count in article header
- Guest prompt to log in for commenting
- Comment flag button
- Notification bell in header with unread badge
- Notification dropdown (last 20, mark all read)
EventSourceconnection to/api/auth/notifications/stream- "دنبال کردن" toggle on writer profile page
- "نویسندگان دنبالشده" section on own profile
- "تاریخچه خواندن" section on own profile
- Fire-and-forget
POST /api/auth/historyon article open - Admin dashboard: live visitor counter widget
- Admin dashboard: live activity feed panel
- Admin dashboard: in-place view count updates on
view_updateevent EventSourceconnection to/api/admin/stream(admin/editor only)- Admin "نظرات" sidebar tab with flagged filter, delete/clear-flag actions, and bulk delete all flagged
Docs
- Update
MEMORY.md - Update
ARCH.md - Update
SECURITY.md(comment flagging, SSE auth) - Update
DEPLOY.mdfor SSE nginx handling and Prisma generate order - Mark
PHASE8.mdimplemented