source: Klonkt/src/views/pages/admin.ejs@ fbfd7a1

main
Last change on this file since fbfd7a1 was fbfd7a1, checked in by Robin <roboburr@…>, 4 weeks ago

Je berichten verhuizen mee: FEP-1580 plus een webinterface ervoor

FEP-7628 verhuist je volgers en zegt zelf dat de inhoud een ander probleem is.
Dat probleem stond open: na een Move bleven je berichten op de oude instantie
staan, en elke reactie van een derde wees naar een URI die verdwijnt zodra dat
domein opgezegd wordt. FEP-1580 regelt dat, status DRAFT.

DE AUTORISATIE IS DE MOVE, NIET EEN CODE. De bronkant behandelt een ondertekend
verzoek namens de doel-actor alsof de bron-actor het zelf deed. Dat mag omdat
moveAccount() no_backreference weigert: moved_to staat er alleen als de
doel-actor ons al in alsoKnownAs had. Beide kanten hebben ooit ja gezegd, dus er
is geen tweede vertrouwensmechanisme nodig. Een typefout komt hier niet binnen,
want die haalt de move zelf niet. Dat dit veilig is leunt op de keyId-binding
uit shaer-xd8i: zonder die controle is "wie tekende dit" te zacht om je hele
geschiedenis aan af te geven.

NIEUWE IDS ZIJN GEEN BUG, DE VERTAALTABEL IS HET ANTWOORD. Een verhuisd bericht
krijgt een eigen URI, want het staat op een ander domein. De migration-collectie
mapt oud naar nieuw en derden lezen die om hun eigen verwijzingen bij te werken.
Zonder die collectie is de draad kapot, met die collectie is het een
verhuisbericht. Niet-publieke items staan er alleen in voor wie ze mocht zien:
een lijst met de URIs van je fan-only posts is een lek, ook zonder de inhoud.

Er gaat geen Create de deur uit. Je volgers hebben die berichten jaren geleden
al gezien; driehonderd posts die als nieuw de tijdlijn in klateren is geen
verhuizing maar spam.

Daarnaast /admin/migrate: exporteren, importeren en ophalen via de
webinterface, zodat verhuizen geen SSH-toegang meer vraagt. Importeren gaat
altijd eerst droog, met een verslag en pas daarna een knop die het echt doet.

Getest op twee draaiende instanties, A verhuisd naar B via de echte
moveAccount. Anoniem zag A 3 van de 4 berichten; ondertekend als de doel-actor
kwamen alle 4 mee, inclusief de fan-only. Titel, webadres en publicatiedatum
blijven staan. Media komt echt over: gedownload, in de mediatabel, B serveert
het. Migration-collectie 4 rijen totaal, 3 publiek.

Changed files:
src/services/ActivityPubService.js

  • isMoveTarget: het hele autorisatiepredicaat van de bronkant
  • outboxAudience en mayReadNote: de doel-actor krijgt onze eigen kijkrechten
  • buildActor adverteert migration en moves, ook leeg (de FEP wijst er apart op dat "niets verhuisd" anders niet te onderscheiden is van "kent dit niet")
  • signedGetJson geexporteerd, de ingest heeft hem nodig
  • isMoveTarget en signedGetJson in de default export (movedLock verstopte zich een dag eerder precies zo)

src/services/ap-core.js

  • FEP-1580-termen in de JSON-LD-context

src/services/ArchiveImportService.js

  • een import uit een zip vult dezelfde vertaaltabel; de spec wil dat een geexporteerde collectie identiek behandeld wordt

src/routes/activitypub.js

  • /ap/users/:slug/migration en /moves
  • de blocked-collectie gaat open voor de doel-actor, want zichtbaarheidsvoorkeuren moeten meeverhuizen

src/config/database.js

  • ap_migration en ap_moves, plus sites.migration_complete

src/server.js

  • /admin/migrate aangesloten

src/views/pages/admin.ejs

  • knop naar Migreren

src/services/i18n.js

  • mig.* in nl/en/de

New file:
src/services/MigrationService.js

  • de doelkant: ingest-routine, migration- en moves-collectie, statusvlag

src/routes/admin-migrate.js

  • exporteren, droog importeren, echt importeren, ophalen bij de oude Klonkt

src/views/pages/admin-migrate.ejs

  • de pagina

test/fep1580-migration.test.js

  • 22 tests over beide rollen, plus de regressietest bij 4101c89

remarks: FEP-8b32 ontbreekt volledig (shaer-j1v0), dus er staat geen
handtekening onder de moves-collectie en we zijn niet naleveringsklaar. Bewust
geen leeg proof-veld: een derde die het controleert wordt dan misleid. De DERDE
rol zit er ook niet in, Klonkt leest nog geen migration-collecties van anderen,
dus andermans verhuizing repareert onze verwijzingen nog niet. Alle betrokken
FEPs zijn DRAFT, ook 7628 die we al volgden; 1580 is vers en de auteur schrijft
zelf dat het een audit verdient.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 8.9 KB
Line 
1<div class="container admin-page">
2 <h1><%= t('admin.title') %></h1>
3 <%
4 var _premOn = (typeof premiumEnabled !== 'undefined' && premiumEnabled);
5 var _premActive = _premOn && (typeof isPremium !== 'undefined' && isPremium);
6 var _premCls = _premActive ? 'on' : (_premOn ? 'off' : 'free');
7 var _premLbl = _premActive ? ('★ ' + t('adash.prem_active'))
8 : (_premOn ? ('⚠ ' + t('adash.prem_unlinked'))
9 : t('adash.prem_layeroff'));
10 %>
11 <a class="prem-badge <%= _premCls %>" href="/admin/settings" title="<%= t('admin.b_settings') %>"><%= _premLbl %></a>
12 <style>
13 .prem-badge{display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;font-weight:700;letter-spacing:.02em;padding:.3rem .85rem;border-radius:999px;text-decoration:none;border:1px solid transparent;margin:.1rem 0 .3rem}
14 .prem-badge.on{background:rgba(232,176,75,.16);color:var(--accent);border-color:rgba(232,176,75,.45)}
15 .prem-badge.off{background:rgba(239,68,68,.12);color:#ef9a9a;border-color:rgba(239,68,68,.35)}
16 .prem-badge.free{background:var(--paper-2);color:var(--ink-muted);border-color:var(--rule)}
17 .prem-badge:hover{filter:brightness(1.08)}
18 </style>
19 <% if (typeof circlesOn !== 'undefined' && circlesOn) { %>
20 <p class="admin-tagline"><%= t('admin.tagline_cirkels') %></p>
21 <% } else { %>
22 <p class="admin-tagline"><%= t('admin.tagline_solo') %></p>
23 <% } %>
24
25 <nav class="admin-quick-links" aria-label="<%= t('nav.admin') %>">
26 <a href="/posts/new" class="btn"><%= t('admin.b_newpost') %></a>
27 <a href="/admin/media" class="btn"><%= t('admin.b_media') %></a>
28 <% if (primarySite) { %>
29 <a href="/admin/sites/<%= primarySite.slug %>/edit" class="btn"><%= t('admin.b_look') %></a>
30 <% } else { %>
31 <a href="/admin/sites/new" class="btn"><%= t('admin.b_makesite') %></a>
32 <% } %>
33 <% if (primarySite) { %><a href="/admin/seo" class="btn"><%= t('admin.b_seo') %></a><% } %>
34 <% if (primarySite) { %><a href="/admin/migrate" class="btn"><%= t('admin.b_migrate') %></a><% } %>
35 <a href="/admin/settings" class="btn"><%= t('admin.b_settings') %></a>
36 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
37 <a href="/admin/stats" class="btn"><%= t('admin.b_stats') %></a>
38 <a href="/admin/paid" class="btn"><%= t('admin.b_paid') %></a>
39 <a href="/admin/newsletter" class="btn"><%= t('admin.b_newsletter') %></a>
40 <% if (primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
41 <% if (primarySite) { %><a href="/downloads" class="btn" target="_blank"><%= t('admin.b_downloads') %></a><% } %>
42 <% if (primarySite) { %><a href="/links" class="btn" target="_blank"><%= t('admin.b_linkbio') %></a><% } %>
43 <a href="/admin/shows" class="btn"><%= t('admin.b_agenda') %></a>
44 <% } %>
45 <%# Fediverse pages (News / Notifications / My replies / Blocking) are reached via the
46 "Fediverse" nav button + the bell + the tabbed section — not duplicated here. %>
47 <a href="/admin/push" class="btn"><%= t('admin.b_push') %></a>
48 <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
49 <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
50 </nav>
51
52 <div class="admin-stats">
53 <div class="stat-card"><div class="stat-num"><%= stats.posts %></div><div class="stat-label"><%= t('admin.st_posts') %></div></div>
54 <div class="stat-card"><div class="stat-num"><%= stats.published %></div><div class="stat-label"><%= t('admin.st_published') %></div></div>
55 </div>
56
57 <% if (typeof posts !== 'undefined' && posts && posts.length) { %>
58 <% var _drafts = posts.filter(function(p){ return p.isDraft; }).length; %>
59 <h2><%= t('admin.sec_posts') %><% if (_drafts) { %> <span class="pl-draftcount"><%= _drafts %> <%= t('admin.draft').toLowerCase() %><%= _drafts === 1 ? '' : 's' %></span><% } %></h2>
60 <ul class="post-admin-list">
61 <% posts.forEach(function(p) { %>
62 <li class="pal-row<%= p.isDraft ? ' is-draft' : '' %>">
63 <a class="pal-title" href="<%= p.editUrl %>">
64 <% if (p.isDraft) { %><span class="pal-badge"><%= t('admin.draft') %></span><% } %>
65 <span class="pal-name"><%= p.title || '(zonder titel)' %></span>
66 </a>
67 <span class="pal-actions">
68 <a class="pal-link" href="<%= p.editUrl %>"><%= t('admin.edit') %></a>
69 <% if (!p.isDraft) { %><a class="pal-link" href="<%= p.viewUrl %>"><%= t('admin.view') %></a><% } %>
70 </span>
71 </li>
72 <% }); %>
73 </ul>
74 <% } %>
75
76</div>
77
78<style>
79.admin-page { max-width: 1000px; margin: 3rem auto; padding: 0 1rem; }
80.admin-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 0.25rem; }
81.admin-page h2 { font-family: var(--font-display, serif); font-size: 1.4rem; margin: 2rem 0 0.75rem; }
82.admin-tagline { color: var(--ink-muted); margin: 0 0 2rem; }
83.admin-tagline em { font-style: normal; opacity: 0.7; }
84
85/* Posts/concepten-lijst */
86.pl-draftcount { font-family: var(--font-ui, system-ui); font-size: 0.8rem; font-weight: 700; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.55rem; border-radius: 99px; vertical-align: middle; }
87.post-admin-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
88.pal-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.7rem 0.9rem; border: 1px solid var(--rule); border-radius: 10px; background: var(--paper-2); transition: border-color 120ms; }
89.pal-row:hover { border-color: var(--accent); }
90.pal-row.is-draft { border-left: 3px solid var(--accent); }
91.pal-title { display: inline-flex; align-items: center; gap: 0.6rem; min-width: 0; color: var(--ink); text-decoration: none; font-weight: 600; }
92.pal-title:hover { color: var(--accent); }
93.pal-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
94.pal-badge { flex-shrink: 0; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.5rem; border-radius: 99px; }
95.pal-actions { display: flex; gap: 0.75rem; flex-shrink: 0; }
96.pal-link { color: var(--ink-muted); font-size: 0.85rem; text-decoration: none; white-space: nowrap; }
97.pal-link:hover { color: var(--accent); text-decoration: underline; }
98
99/* Quick-links grid — mobile-first.
100 auto-fit + minmax means: as many equal columns as fit, each at least 140px.
101 Mobile (~360-440px): 2 per row.
102 Tablet (~480-700px): 3 per row.
103 Desktop (≥ ~720px): all 5 in a single row. */
104.admin-quick-links {
105 display: grid;
106 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
107 gap: 0.5rem;
108 margin: 0 0 1.5rem;
109 padding: 0;
110}
111.admin-quick-links .btn {
112 justify-content: center;
113 padding: 0.7rem 0.9rem;
114 font-weight: 600;
115 text-align: center;
116}
117.admin-quick-links .btn:hover {
118 background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
119 border-color: var(--accent);
120}
121
122.admin-stats {
123 display: grid;
124 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
125 gap: 0.75rem;
126 margin-bottom: 1rem;
127}
128.stat-card {
129 background: var(--paper-2);
130 border: 1px solid var(--rule);
131 border-radius: 8px;
132 padding: 1rem 1.25rem;
133 text-align: center;
134}
135.stat-num {
136 font-size: 2rem;
137 font-weight: 700;
138 color: var(--accent);
139 line-height: 1;
140 font-family: var(--font-display, serif);
141}
142.stat-label {
143 color: var(--ink-muted);
144 font-size: 0.85rem;
145 text-transform: uppercase;
146 letter-spacing: 0.05em;
147 margin-top: 0.25rem;
148}
149
150.admin-table {
151 width: 100%;
152 border-collapse: collapse;
153 background: var(--paper-2);
154 border: 1px solid var(--rule);
155 border-radius: 8px;
156 overflow: hidden;
157}
158.admin-table th, .admin-table td {
159 padding: 0.6rem 0.9rem;
160 text-align: left;
161 border-bottom: 1px solid var(--rule);
162}
163.admin-table th {
164 background: var(--paper);
165 font-weight: 600;
166 font-size: 0.85rem;
167 text-transform: uppercase;
168 letter-spacing: 0.05em;
169 color: var(--ink-muted);
170}
171.admin-table tr:last-child td { border-bottom: 0; }
172.admin-table code { background: var(--paper); padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.85rem; }
173
174.role-pill {
175 display: inline-block;
176 padding: 0.1rem 0.5rem;
177 border-radius: 10px;
178 font-size: 0.75rem;
179 text-transform: uppercase;
180 letter-spacing: 0.05em;
181 background: var(--paper);
182 color: var(--ink-muted);
183}
184.role-pill.role-god { background: var(--accent); color: white; }
185.role-pill.role-admin { background: var(--paper); color: var(--accent); border: 1px solid var(--accent); }
186
187.admin-todo {
188 margin: 2rem 0 0;
189 padding: 0.75rem 1rem;
190 background: var(--paper-2);
191 border-left: 3px solid var(--accent);
192 border-radius: 4px;
193 font-size: 0.9rem;
194 color: var(--ink-muted);
195}
196
197@media (max-width: 600px) {
198 .admin-table { font-size: 0.85rem; }
199 .admin-table th, .admin-table td { padding: 0.45rem 0.6rem; }
200}
201</style>
Note: See TracBrowser for help on using the repository browser.