feat: per-board activity log
Adds an audit trail per board, visible from a new clock-icon button on the toolbar. Useful for review-style work where someone wants to see who contributed which references and when. Logged events (high-signal only — canvas-edit noise intentionally skipped): - image / video / pdf added (whether dropped, pasted, or pulled from a URL) - board created / renamed / deleted - thread started, resolved, reopened - comment posted on a thread Backend: - new activity_logs table (id, board_id, user_id, denormalised actor name + email, action, target_type/id/label, metadata JSON, created_at) with an index on (board_id, created_at DESC). - logActivity helper resolves the user once at log time and stores their display name + email so entries survive deactivation/rename. - recordActivity wraps logActivity + a Socket.IO emit to the board's room so the panel updates live without polling. - GET /api/boards/:id/activity?limit=&before= for pagination (collection-membership gated, viewer+). Frontend: - ActivityPanel side-drawer: time-grouped feed (Today / Yesterday / older), per-action icons + tone colours (add/remove/edit/comment), pagination via "Load older", live append on Socket.IO 'activity:new'. - Relative timestamps refresh every 30s. - Wired into Editor + Toolbar. README updated; roadmap entry checked off.
This commit is contained in:
@@ -25,6 +25,7 @@ import { getStickyWidthForSize } from '../canvas/stickyPresets';
|
||||
import VideoControls from '../components/VideoControls';
|
||||
import ShortcutsHelp from '../components/ShortcutsHelp';
|
||||
import Minimap from '../components/Minimap';
|
||||
import ActivityPanel from '../components/ActivityPanel';
|
||||
import UploadPanel from '../components/UploadPanel';
|
||||
import ExportDialog from '../components/ExportDialog';
|
||||
import FeedbackPanel from '../components/feedback/FeedbackPanel';
|
||||
@@ -115,6 +116,7 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const [toasts, setToasts] = useState<{ id: string; text: string }[]>([]);
|
||||
const [showLayers, setShowLayers] = useState(false);
|
||||
const [showActivity, setShowActivity] = useState(false);
|
||||
const [showGrid, setShowGrid] = useState(true);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
@@ -1090,6 +1092,8 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
followingUserId={followingUserId}
|
||||
onToggleLayers={() => setShowLayers((v) => !v)}
|
||||
showLayers={showLayers}
|
||||
onToggleActivity={() => setShowActivity((v) => !v)}
|
||||
showActivity={showActivity}
|
||||
onToggleHelp={() => setShowHelp((v) => !v)}
|
||||
onExport={() => setShowExport(true)}
|
||||
onRefreshPreview={readOnly || isPublicView ? undefined : handleRefreshPreview}
|
||||
@@ -1539,6 +1543,11 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Activity log */}
|
||||
{showActivity && resolvedBoardId && (
|
||||
<ActivityPanel boardId={resolvedBoardId} onClose={() => setShowActivity(false)} />
|
||||
)}
|
||||
|
||||
{/* Layer panel */}
|
||||
{showLayers && (
|
||||
<LayerPanel
|
||||
|
||||
Reference in New Issue
Block a user