Changeset 7881080 in Klonkt


Ignore:
Timestamp:
06/18/2026 03:28:34 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
8398935
Parents:
98ecf51
Message:

Hub #3: explicit is_primary flag + shared getPrimarySite() (DRY)

The "oldest site = primary site" assumption was duplicated independently in
resolveSite, hub.js, account.js and admin.js (fragile: if the oldest happened
to be an artist site, the hub home would be wrong). Now:

  • sites.is_primary column + backfill (marks the oldest if none is primary yet; ensurePrimarySite sets it on fresh installs) → existing behaviour exactly preserved.
  • one getPrimarySite() helper (is_primary, fallback oldest) replaces the 4 copies.
  • god can CHOOSE the primary/main site: ★ button + "primary" badge on /admin/sites (exactly one primary via a transaction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

Location:
src
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r98ecf51 r7881080  
    5050  ensureColumn('sites', 'allow_circle', 'INTEGER DEFAULT 1');
    5151
     52  // Eén EXPLICIETE primaire/hoofd-site (= de bedrijfs-/labelsite in hub-modus,
     53  // de enige site in solo) i.p.v. de fragiele "oudste = hoofd"-conventie die op
     54  // 4 plekken gedupliceerd stond. Backfill: markeer de oudste als er nog geen
     55  // primaire site is, zodat bestaand gedrag exact behouden blijft.
     56  ensureColumn('sites', 'is_primary', 'INTEGER DEFAULT 0');
     57  try {
     58    const hasPrimary = db.prepare('SELECT 1 FROM sites WHERE is_primary = 1 LIMIT 1').get();
     59    if (!hasPrimary) {
     60      const oldest = db.prepare('SELECT id FROM sites ORDER BY created_at ASC LIMIT 1').get();
     61      if (oldest) db.prepare('UPDATE sites SET is_primary = 1 WHERE id = ?').run(oldest.id);
     62    }
     63  } catch (e) { /* sites-tabel nog leeg/afwezig bij verse init — ensurePrimarySite regelt 't */ }
     64
    5265  // v9 audit additions —————————————————————————————————————————
    5366  // SEO/social columns the v9 template uses (most live in 001-init.sql already
  • src/middleware/site.js

    r98ecf51 r7881080  
    1212import db from '../config/database.js';
    1313import { getTenancy } from '../services/SettingsService.js';
     14
     15/**
     16 * De primaire/hoofd-site — ÉÉN bron van waarheid (vervangt de "oudste site ="
     17 * hoofd"-aanname die voorheen los in resolveSite/hub/account/admin stond).
     18 * Leest de expliciete is_primary-vlag; valt terug op de oudste als die (nog)
     19 * nergens staat, zodat bestaand gedrag exact behouden blijft.
     20 */
     21export function getPrimarySite() {
     22  return db.prepare('SELECT * FROM sites WHERE is_primary = 1 LIMIT 1').get()
     23      || db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get()
     24      || null;
     25}
    1426
    1527export function resolveSite(req, res, next) {
     
    4355  }
    4456
    45   // Solo (of hub zonder match): de primaire/owner-site (eerste aangemaakte).
    46   const defaultSite = db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
     57  // Solo (of hub zonder match): pin op de primaire/hoofd-site.
     58  const defaultSite = getPrimarySite();
    4759  if (defaultSite) {
    4860    res.locals.site = defaultSite;
  • src/routes/account.js

    r98ecf51 r7881080  
    2121import { v4 as uuid } from 'uuid';
    2222import db from '../config/database.js';
     23import { getPrimarySite } from '../middleware/site.js';
    2324import { renderPage } from '../middleware/render.js';
    2425import { requireAuth } from '../middleware/auth.js';
     
    8081  let site = db.prepare('SELECT id, title, tagline, slug, owner_id FROM sites WHERE owner_id = ? ORDER BY created_at LIMIT 1').get(user.id);
    8182  if (!site && user.role === 'god') {
    82     site = db.prepare('SELECT id, title, tagline, slug, owner_id FROM sites ORDER BY created_at LIMIT 1').get();
     83    site = getPrimarySite(); // primaire/hoofd-site als fallback
    8384  }
    8485  return site || null;
  • src/routes/admin-sites.js

    r98ecf51 r7881080  
    161161  const sites = db.prepare(`
    162162    SELECT s.id, s.slug, s.title, s.description, s.created_at,
    163            s.is_public, s.robots_index,
     163           s.is_public, s.robots_index, s.is_primary,
    164164           u.username AS owner_username,
    165165           (SELECT COUNT(*) FROM posts WHERE site_id = s.id) AS post_count
    166166    FROM sites s LEFT JOIN users u ON u.id = s.owner_id
    167     ORDER BY s.created_at DESC
     167    ORDER BY s.is_primary DESC, s.created_at DESC
    168168  `).all();
    169169
     
    354354});
    355355
     356// ==================== MAAK PRIMAIR ====================
     357// God kiest welke site de primaire/hoofd-site is (de label-/bedrijfssite in hub;
     358// in solo dé site). Precies één site is primair → eerst alles uit, dan deze aan.
     359router.post('/:slug/make-primary', requireGod, (req, res) => {
     360  const site = db.prepare('SELECT id FROM sites WHERE slug = ?').get(req.params.slug);
     361  if (!site) return res.redirect('/admin/sites?error=Niet+gevonden');
     362  db.transaction(() => {
     363    db.prepare('UPDATE sites SET is_primary = 0').run();
     364    db.prepare('UPDATE sites SET is_primary = 1 WHERE id = ?').run(site.id);
     365  })();
     366  res.redirect('/admin/sites?success=' + encodeURIComponent('Primaire site bijgewerkt'));
     367});
     368
    356369// ==================== DELETE ====================
    357370router.post('/:slug/delete', requireGod, (req, res) => {
  • src/routes/admin.js

    r98ecf51 r7881080  
    1010import { requireAuth } from '../middleware/auth.js';
    1111import { getTenancy } from '../services/SettingsService.js';
     12import { getPrimarySite } from '../middleware/site.js';
    1213
    1314const router = express.Router();
     
    5960  const tenancy = getTenancy();
    6061
    61   // De primaire/owner-site — in solo dé site, in hub de hoofdsite. Geeft de
     62  // De primaire/hoofd-site — in solo dé site, in hub de hoofdsite. Geeft de
    6263  // "Uiterlijk"-tegel z'n edit-link + de posts/concepten-lijst.
    63   const primarySite = db.prepare(
    64     'SELECT id, slug, title FROM sites ORDER BY created_at ASC LIMIT 1'
    65   ).get() || null;
     64  const primarySite = getPrimarySite();
    6665
    6766  const stats = {
  • src/routes/hub.js

    r98ecf51 r7881080  
    3535  `).all();
    3636
    37   // De hoofd-/labelsite (oudste = de bedrijfs-/hoofdaccount) is GEEN artiest;
    38   // die tonen we apart bovenaan, niet in de Artiesten-roster.
     37  // De hoofd-/labelsite (de expliciet primaire = de bedrijfs-/hoofdaccount) is
     38  // GEEN artiest; die tonen we apart bovenaan, niet in de Artiesten-roster.
    3939  const mainSite = db.prepare(`
    4040    SELECT s.id, s.slug, s.title, s.tagline, s.profile_photo, s.accent,
     
    4343    FROM sites s
    4444    LEFT JOIN users u ON u.id = s.owner_id
    45     ORDER BY s.created_at ASC
     45    WHERE s.is_primary = 1
    4646    LIMIT 1
    4747  `).get() || null;
  • src/services/ensurePrimarySite.js

    r98ecf51 r7881080  
    4040      language, palette, accent, profile_photo,
    4141      is_public, robots_index, require_login_to_comment, enable_audio_player,
    42       feed_view_default, comments_moderation_mode
    43     ) VALUES (?, ?, ?, '', '', ?, 'nl', 'sage', '#c2410c', NULL, 1, 1, 0, 1, 'grid', 'moderate')
     42      feed_view_default, comments_moderation_mode, is_primary
     43    ) VALUES (?, ?, ?, '', '', ?, 'nl', 'sage', '#c2410c', NULL, 1, 1, 0, 1, 'grid', 'moderate', 1)
    4444  `).run(siteId, slug, defaultTitle(), owner.id);
    4545
  • src/views/pages/admin-sites.ejs

    r98ecf51 r7881080  
    3636              <div class="ax-site-name">
    3737                <a href="/sites/<%= s.slug %>"><%= s.title %></a>
     38                <% if (s.is_primary) { %>
     39                  <span class="ax-pill ax-pill-primary" title="Hoofd-/labelsite van deze installatie">★ primair</span>
     40                <% } %>
    3841                <% if (s.is_public) { %>
    3942                  <span class="ax-pill ax-pill-ok" title="Publiek zichtbaar">✓ public</span>
     
    6770
    6871          <div class="ax-site-controls">
     72            <% if (!s.is_primary && (typeof canMutate === 'undefined' || canMutate)) { %>
     73              <form method="post" action="/admin/sites/<%= s.slug %>/make-primary" style="margin:0;display:inline"
     74                    onsubmit="return confirm('Deze site instellen als de primaire/hoofd-site?')">
     75                <button type="submit" class="ax-icon-btn" aria-label="Maak primair" title="Maak primaire/hoofd-site">★</button>
     76              </form>
     77            <% } %>
    6978            <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="Bewerken" title="Bewerken">✎</a>
    7079            <form method="post" action="/admin/sites/<%= s.slug %>/delete" class="ax-delete-form"
     
    198207  background: rgba(40,160,90,0.12);
    199208}
     209.ax-pill-primary {
     210  color: var(--accent);
     211  background: color-mix(in srgb, var(--accent) 14%, transparent);
     212  font-weight: 600;
     213}
    200214
    201215/* ─── Stats (2 mini-columns for sites) ────────────────────────── */
Note: See TracChangeset for help on using the changeset viewer.