wiki-plugin-salon 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/salon.js CHANGED
@@ -87,8 +87,18 @@ window.plugins.salon = (function() {
87
87
  if (data.isOwner) {
88
88
  const configDiv = document.createElement('div');
89
89
  configDiv.style.cssText = 'border:1px solid rgba(196,181,253,0.2);border-radius:8px;padding:12px;margin-top:8px;background:rgba(124,58,237,0.06);font-family:system-ui;';
90
+ const pendingBadge = data.pendingCount > 0
91
+ ? `<span style="background:#f59e0b;color:#000;border-radius:9px;font-size:0.7rem;padding:1px 6px;margin-left:6px;">${data.pendingCount} pending</span>`
92
+ : '';
90
93
  configDiv.innerHTML = `
91
- <div style="font-size:0.8rem;color:#c4b5fd;margin-bottom:8px;font-weight:600;">Allyabase connection (owner only)</div>
94
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
95
+ <span style="font-size:0.8rem;color:#c4b5fd;font-weight:600;">Owner panel${pendingBadge}</span>
96
+ <a href="/plugin/salon/admin" target="_blank"
97
+ style="font-size:0.78rem;color:#7c3aed;text-decoration:none;border:1px solid rgba(124,58,237,0.4);border-radius:4px;padding:3px 8px;">
98
+ Admin ↗
99
+ </a>
100
+ </div>
101
+ <div style="font-size:0.8rem;color:#a0a0a0;margin-bottom:8px;">Allyabase URL</div>
92
102
  <div style="display:flex;gap:6px;align-items:center;">
93
103
  <input id="salon-url-input" value="${escapeHtml(data.allyabaseUrl || '')}" placeholder="https://dev.allyabase.com"
94
104
  style="flex:1;background:#0d001a;border:1px solid rgba(196,181,253,0.3);border-radius:4px;padding:6px 8px;color:#e0d0ff;font-size:0.8rem;">
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "wiki-plugin-salon",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Federated Wiki plugin — community gathering space with self-registration and plugin event feeds",
5
5
  "keywords": ["wiki", "plugin", "salon", "community", "planet-nine"],
6
6
  "author": "Planet Nine",
7
7
  "license": "MIT",
8
8
  "type": "commonjs",
9
9
  "main": "index.js",
10
- "dependencies": {
11
- "nodemailer": "^6.9.0"
12
- }
10
+ "dependencies": {}
13
11
  }
package/server/server.js CHANGED
@@ -93,14 +93,21 @@ function buildEventEmail(config, event, detail) {
93
93
 
94
94
  // ── Email ─────────────────────────────────────────────────────────────────────
95
95
 
96
- function createTransporter(config) {
97
- const nodemailer = require('nodemailer');
98
- return nodemailer.createTransport({
99
- host: config.minnieHost || 'localhost',
100
- port: parseInt(config.minniePort) || 2525,
101
- secure: false,
102
- tls: { rejectUnauthorized: false }
96
+ function getMinnieUrl(config) {
97
+ const base = config.allyabaseUrl ||
98
+ (config.joanUrl ? config.joanUrl.replace(/\/plugin\/allyabase\/joan$/, '') : '');
99
+ return base ? base.replace(/\/$/, '') + '/plugin/allyabase/minnie' : null;
100
+ }
101
+
102
+ async function sendViaMinnie(minnieUrl, { from, to, subject, text }) {
103
+ const resp = await fetch(`${minnieUrl}/send`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ from, to, subject, text }),
107
+ signal: AbortSignal.timeout(10000)
103
108
  });
109
+ if (!resp.ok) throw new Error(`Minnie /send failed: ${resp.status}`);
110
+ return resp.json();
104
111
  }
105
112
 
106
113
  async function notifyMembers(config, subject, text) {
@@ -108,12 +115,17 @@ async function notifyMembers(config, subject, text) {
108
115
  const recipients = Object.values(tenants).filter(t => t.status === 'active' && t.email);
109
116
  if (recipients.length === 0) return;
110
117
 
111
- const transporter = createTransporter(config);
112
- const from = `"${config.title}" <${config.fromEmail || 'salon@planetnine.app'}>`;
118
+ const minnieUrl = getMinnieUrl(config);
119
+ if (!minnieUrl) {
120
+ console.warn('salon: no Minnie URL configured — skipping email notifications');
121
+ return;
122
+ }
123
+
124
+ const from = config.fromEmail || 'salon@planetnine.app';
113
125
 
114
126
  for (const tenant of recipients) {
115
127
  try {
116
- await transporter.sendMail({ from, to: tenant.email, subject, text });
128
+ await sendViaMinnie(minnieUrl, { from, to: tenant.email, subject, text });
117
129
  } catch (err) {
118
130
  console.error(`salon: email failed for ${tenant.email}:`, err.message);
119
131
  }
@@ -632,15 +644,12 @@ function generateAdminPage(config, tenants) {
632
644
  <label>From address</label>
633
645
  <input name="fromEmail" value="${esc(config.fromEmail || 'salon@planetnine.app')}" placeholder="salon@planetnine.app">
634
646
 
635
- <label>Minnie host</label>
636
- <input name="minnieHost" value="${esc(String(config.minnieHost || 'localhost'))}" placeholder="localhost">
637
-
638
- <label>Minnie port</label>
639
- <input name="minniePort" type="number" value="${esc(String(config.minniePort || 2525))}" placeholder="2525">
640
-
641
- <label>Joan URL</label>
642
- <input name="joanUrl" value="${esc(config.joanUrl || 'http://localhost:3008')}" placeholder="http://localhost:3008">
643
- <div class="form-hint">Used to send OTP codes for member account recovery.</div>
647
+ <label>Allyabase URL</label>
648
+ <input name="allyabaseUrl" value="${esc(config.allyabaseUrl || (config.joanUrl ? config.joanUrl.replace(/\/plugin\/allyabase\/joan$/, '') : ''))}" placeholder="https://dev.allyabase.com">
649
+ <div class="form-hint">
650
+ Emails sent via <code>${esc((config.allyabaseUrl || (config.joanUrl ? config.joanUrl.replace(/\/plugin\/allyabase\/joan$/, '') : 'https://dev.allyabase.com')) + '/plugin/allyabase/minnie')}</code><br>
651
+ OTP codes sent via <code>${esc(config.joanUrl || 'https://dev.allyabase.com/plugin/allyabase/joan')}</code>
652
+ </div>
644
653
 
645
654
  <div style="margin-top:16px;">
646
655
  <button type="submit" class="btn btn-primary btn-sm">Save Settings</button>
@@ -1166,18 +1175,16 @@ function startServer(params) {
1166
1175
 
1167
1176
  app.post('/plugin/salon/config', owner, json, (req, res) => {
1168
1177
  const config = loadConfig();
1169
- const { registrationMode, title, description, fromEmail, minnieHost, minniePort, joanUrl, allyabaseUrl } = req.body;
1178
+ const { registrationMode, title, description, fromEmail, joanUrl, allyabaseUrl } = req.body;
1170
1179
  const modes = ['open', 'closed', 'grant'];
1171
1180
  if (registrationMode && modes.includes(registrationMode)) config.registrationMode = registrationMode;
1172
1181
  if (title && title.trim()) config.title = title.trim();
1173
1182
  if (description !== undefined) config.description = description.trim();
1174
1183
  if (fromEmail !== undefined) config.fromEmail = fromEmail.trim();
1175
- if (minnieHost !== undefined) config.minnieHost = minnieHost.trim();
1176
- if (minniePort !== undefined) config.minniePort = parseInt(minniePort) || 2525;
1177
1184
  if (joanUrl !== undefined) config.joanUrl = joanUrl.trim();
1178
- if (allyabaseUrl !== undefined) {
1179
- config.allyabaseUrl = allyabaseUrl.trim();
1180
- config.joanUrl = allyabaseUrl.trim().replace(/\/$/, '') + '/plugin/allyabase/joan';
1185
+ if (allyabaseUrl !== undefined && allyabaseUrl.trim()) {
1186
+ config.allyabaseUrl = allyabaseUrl.trim().replace(/\/$/, '');
1187
+ config.joanUrl = config.allyabaseUrl + '/plugin/allyabase/joan';
1181
1188
  }
1182
1189
  saveConfig(config);
1183
1190
  const derivedAllyabaseUrl = config.allyabaseUrl ||
@@ -1427,16 +1434,21 @@ function startServer(params) {
1427
1434
  const recipients = Object.values(tenants).filter(t => t.status === 'active' && t.email);
1428
1435
 
1429
1436
  if (recipients.length === 0) {
1430
- return res.json({ success: true, sent: 0 });
1437
+ return res.json({ success: true, sent: 0, warning: 'No active members with email addresses found' });
1438
+ }
1439
+
1440
+ const minnieUrl = getMinnieUrl(config);
1441
+ if (!minnieUrl) {
1442
+ return res.status(503).json({ error: 'Email not configured — save your Allyabase URL in settings first' });
1431
1443
  }
1432
1444
 
1433
- const transporter = createTransporter(config);
1434
- const from = `"${config.title}" <${config.fromEmail || 'salon@planetnine.app'}>`;
1445
+ const from = config.fromEmail || 'salon@planetnine.app';
1435
1446
  let sent = 0;
1447
+ const errors = [];
1436
1448
 
1437
1449
  for (const tenant of recipients) {
1438
1450
  try {
1439
- await transporter.sendMail({
1451
+ await sendViaMinnie(minnieUrl, {
1440
1452
  from,
1441
1453
  to: tenant.email,
1442
1454
  subject: subject.slice(0, 200),
@@ -1445,10 +1457,11 @@ function startServer(params) {
1445
1457
  sent++;
1446
1458
  } catch (err) {
1447
1459
  console.error(`salon: announce email failed for ${tenant.email}:`, err.message);
1460
+ errors.push(tenant.email);
1448
1461
  }
1449
1462
  }
1450
1463
 
1451
- res.json({ success: true, sent });
1464
+ res.json({ success: true, sent, ...(errors.length && { errors }) });
1452
1465
  });
1453
1466
 
1454
1467
  // ── Freyja federation page ──────────────────────────────────────────────────