Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b6a1f38
docs(gdpr): admin UI for author erasure — design spec
JohnMcLear May 3, 2026
b4c0714
docs(gdpr): admin UI for author erasure — implementation plan
JohnMcLear May 3, 2026
93b5eff
feat(authors): stamp lastSeen on globalAuthor writes
JohnMcLear May 3, 2026
1cd86ff
feat(authors): anonymizeAuthor({dryRun}) for preview
JohnMcLear May 3, 2026
b7eb8bf
docs(authors): restore lost rationale on anonymizeAuthor + document d…
JohnMcLear May 3, 2026
3cc9afc
feat(authors): authorManager.searchAuthors helper
JohnMcLear May 3, 2026
a15dbfe
fix(authors): tie-break searchAuthors sort on authorID
JohnMcLear May 3, 2026
340241f
feat(authors): admin-socket events for author erasure UI
JohnMcLear May 3, 2026
8a8ecfc
test(authors): clean up admin-socket test mutations
JohnMcLear May 3, 2026
8f9c2e5
feat(admin): types, ColorSwatch, and en.json for authors page
JohnMcLear May 3, 2026
21c9c51
feat(admin): /admin/authors page
JohnMcLear May 3, 2026
016648b
fix(authors): i18n the pagination controls
JohnMcLear May 3, 2026
35383ba
fix(authors): banner CSS, IconButton attribute drop, erase phase string
JohnMcLear May 3, 2026
51ef92a
test(admin): Playwright /admin/authors + fix i18n key shape
JohnMcLear May 4, 2026
7472e48
fix(test): JSONC-tolerant settings parse + sidebar count = 7
JohnMcLear May 4, 2026
69707ae
fix(authors): IconButton title, Dialog.Title, preview errors, setting…
JohnMcLear May 4, 2026
88610f6
fix(authors): action Qodo review — lastSeen, flag-gating, defensive p…
JohnMcLear May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions admin/public/ep_admin_authors/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"title": "Authors",
"search-placeholder": "Search by name or mapper",
"column.color": "Color",
"column.name": "Name",
"column.mapper": "Mapper",
"column.last-seen": "Last seen",
"column.author-id": "Author ID",
"column.actions": "Actions",
"show-erased": "Show erased authors",
"erase": "Erase",
"erase-disabled-tooltip": "Author erasure is disabled. Set gdprAuthorErasure.enabled = true in settings.json.",
"erased-stub": "(erased)",
"cap-warning": "Showing the first 1000 authors. Narrow your search to see more.",
"feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.",
"no-results": "No authors match this search.",
"confirm-preview-title": "Erase author {{name}}",
"confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.",
"confirm-irreversible": "This cannot be undone.",
"cancel": "Cancel",
"continue": "Continue",
"loading-preview": "Loading preview…",
"erasing": "Erasing…",
"erase-success-toast": "Author {{authorID}} erased.",
"erase-error-toast": "Erase failed: {{error}}",
"no-mappers": "—",
"never-seen": "—",
"prev-page": "Previous Page",
"next-page": "Next Page",
"page-counter": "{{current}} out of {{total}}"
}
15 changes: 10 additions & 5 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu, Bell} from "lucide-react";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu, Bell, Users} from "lucide-react";
import {UpdateBanner} from "./components/UpdateBanner";

const WS_URL = import.meta.env.DEV ? 'http://localhost:9001' : ''
export const App = () => {
const setSettings = useStore(state => state.setSettings);
const erasureEnabled = useStore(state => state.gdprAuthorErasureEnabled)
const {t} = useTranslation()
const navigate = useNavigate()
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true)
Expand Down Expand Up @@ -61,14 +62,15 @@ export const App = () => {
}
});

settingSocket.on('settings', (settings) => {
/* Check whether the settings.json is authorized to be viewed */
settingSocket.on('settings', (settings: any) => {
if (settings && typeof settings.flags === 'object' && settings.flags) {
useStore.getState().setGdprAuthorErasureEnabled(
!!settings.flags.gdprAuthorErasure);
}
if (settings.results === 'NOT_ALLOWED') {
console.log('Not allowed to view settings.json')
return;
}

/* Check to make sure the JSON is clean before proceeding */
if (isJSONClean(settings.results)) {
setSettings(settings.results);
} else {
Expand Down Expand Up @@ -105,6 +107,9 @@ export const App = () => {
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><NotepadText/><Trans
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
{erasureEnabled && (
<li><NavLink to={"/authors"}><Users/><Trans i18nKey="ep_admin_authors:title" ns="ep_admin_authors"/></NavLink></li>
)}
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
<li><NavLink to={"/update"}><Bell/><Trans i18nKey="update.page.title"/></NavLink></li>
</ul>
Expand Down
37 changes: 37 additions & 0 deletions admin/src/components/ColorSwatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type Props = {
color: string | number | null;
size?: number;
};

// Resolves the colorId stored on globalAuthor records into a CSS color.
// AuthorManager stores either a string hex (legacy) or an integer index
// into the palette returned by getColorPalette() — we re-derive the
// palette here rather than fetch it because the order is stable and the
// admin already has many other small constants inline.
const PALETTE = [
'#ffc7c7', '#fff1c7', '#e3ffc7', '#c7ffd5', '#c7ffff', '#c7d5ff',
'#e3c7ff', '#ffc7f1', '#ffa8a8', '#ffe699', '#cfff9e', '#99ffb3',
'#a3ffff', '#99b3ff', '#cc99ff', '#ff99e5', '#e7b1b1', '#e9dcAf',
'#cde9af', '#bfedcc', '#b1e7e7', '#c3cdee', '#d2b8ea', '#eec3e6',
'#e9cece', '#e7e0ca', '#d3e5c7', '#bce1c5', '#c1e2e2', '#c1c9e2',
'#cfc1e2', '#e0bdd9', '#baded3', '#a0f8eb', '#b1e7e0', '#c3c8e4',
'#cec5e2', '#b1d5e7', '#cda8f0', '#f0f0a8', '#f2f2a6', '#f5a8eb',
'#c5f9a9', '#ececbb', '#e7c4bc', '#daf0b2', '#b0a0fd', '#bce2e7',
'#cce2bb', '#ec9afe', '#edabbd', '#aeaeea', '#c4e7b1', '#d722bb',
'#f3a5e7', '#ffa8a8', '#d8c0c5', '#eaaedd', '#adc6eb', '#bedad1',
'#dee9af', '#e9afc2', '#f8d2a0', '#b3b3e6',
];

export const ColorSwatch = ({color, size = 14}: Props) => {
let resolved = '#ccc';
if (typeof color === 'string') {
resolved = color;
} else if (typeof color === 'number' && color >= 0 && color < PALETTE.length) {
resolved = PALETTE[color];
}
return <span style={{
display: 'inline-block', width: size, height: size,
background: resolved, border: '1px solid rgba(0,0,0,0.2)',
borderRadius: 3, verticalAlign: 'middle',
}}/>;
};
2 changes: 1 addition & 1 deletion admin/src/localization/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ i18n
.use(initReactI18next)
.init(
{
ns: ['translation','ep_admin_pads'],
ns: ['translation','ep_admin_pads','ep_admin_authors'],
fallbackLng: 'en'
}
)
Expand Down
2 changes: 2 additions & 0 deletions admin/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Toast from '@radix-ui/react-toast'
import {I18nextProvider} from "react-i18next";
import i18n from "./localization/i18n.ts";
import {PadPage} from "./pages/PadPage.tsx";
import {AuthorPage} from "./pages/AuthorPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
import {ShoutPage} from "./pages/ShoutPage.tsx";
import {UpdatePage} from "./pages/UpdatePage.tsx";
Expand All @@ -22,6 +23,7 @@ const router = createBrowserRouter(createRoutesFromElements(
<Route path="/settings" element={<SettingsPage/>}/>
<Route path="/help" element={<HelpPage/>}/>
<Route path="/pads" element={<PadPage/>}/>
<Route path="/authors" element={<AuthorPage/>}/>
<Route path="/shout" element={<ShoutPage/>}/>
<Route path="/update" element={<UpdatePage/>}/>
</Route><Route path="/login">
Expand Down
Loading
Loading