ultimate-jekyll-manager 1.0.12 → 1.0.14

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/CHANGELOG.md CHANGED
@@ -15,6 +15,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
17
  ---
18
+ ## [1.0.13] - 2026-03-27
19
+ ### Added
20
+ - MRR stat card on admin dashboard calculated from brand config prices × subscriber counts
21
+ - `setStatSubValue` helper in admin-helpers.js for displaying sub-metrics on stat cards
22
+ - Green "+N in 30d" sub-values under Total Users and Push Subscribers stat cards
23
+ - New "Active users (30d)" stat card on admin users page
24
+
25
+ ### Changed
26
+ - Dashboard charts now use `getCountFromServer` queries per product × frequency instead of fetching all user docs
27
+ - Product list and billing frequencies derived dynamically from `/backend-manager/brand` API
28
+ - Consolidated "New users (30d)" from standalone card into sub-value under Total Users
29
+
30
+ ### Fixed
31
+ - Pacman-shaped spinners in stat cards caused by `spinner-border-sm` inheriting `<h3>` font size (added `fs-6`)
32
+
33
+ ### Removed
34
+ - `showUnauthenticated()` flows from all admin pages — pages now return early if no user
35
+
18
36
  ## [1.0.11] - 2026-03-24
19
37
  ### Added
20
38
  - Firestore version + transport test page at `/test/libraries/firestore` for diagnosing SDK connectivity across browsers
@@ -52,3 +52,17 @@ export function setStatValue(id, result) {
52
52
  console.error(`Failed to load ${id}:`, result.reason);
53
53
  }
54
54
  }
55
+
56
+ export function setStatSubValue(id, result, label) {
57
+ const $el = document.getElementById(id);
58
+ if (!$el) {
59
+ return;
60
+ }
61
+
62
+ if (result.status === 'fulfilled') {
63
+ const count = result.value.data().count;
64
+ $el.textContent = `+${count.toLocaleString()} ${label}`;
65
+ $el.classList.add('text-success');
66
+ $el.classList.remove('text-muted');
67
+ }
68
+ }
@@ -21,7 +21,6 @@ export default (Manager) => {
21
21
 
22
22
  webManager.auth().listen({ once: true }, async (state) => {
23
23
  if (!state.user) {
24
- showUnauthenticated();
25
24
  return;
26
25
  }
27
26
 
@@ -44,14 +43,3 @@ function initialize() {
44
43
  core.initialize();
45
44
  }
46
45
 
47
- // Show unauthenticated state
48
- function showUnauthenticated() {
49
- const $grid = document.getElementById('calendar-grid');
50
- $grid.innerHTML = `
51
- <div class="d-flex align-items-center justify-content-center h-100 text-muted">
52
- <div class="text-center">
53
- <p>Sign in to view the marketing calendar</p>
54
- </div>
55
- </div>
56
- `;
57
- }
@@ -4,8 +4,9 @@
4
4
 
5
5
  // Libraries
6
6
  import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
7
+ import fetch from 'wonderful-fetch';
7
8
  import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
8
- import { formatTimeAgo, capitalize, escapeHtml, setStatValue } from '__main_assets__/js/libs/admin-helpers.js';
9
+ import { formatTimeAgo, capitalize, escapeHtml, setStatValue, setStatSubValue } from '__main_assets__/js/libs/admin-helpers.js';
9
10
  import { Chart, DoughnutController, BarController, ArcElement, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
10
11
 
11
12
  // Register Chart.js components
@@ -25,7 +26,6 @@ export default (Manager) => {
25
26
 
26
27
  webManager.auth().listen({ once: true }, async (state) => {
27
28
  if (!state.user) {
28
- showUnauthenticated();
29
29
  return;
30
30
  }
31
31
 
@@ -37,18 +37,6 @@ export default (Manager) => {
37
37
  });
38
38
  };
39
39
 
40
- // Show unauthenticated state
41
- function showUnauthenticated() {
42
- // Replace all spinners with sign-in message
43
- document.querySelectorAll('.spinner-border').forEach((spinner) => {
44
- const container = spinner.closest('.card-body') || spinner.parentElement;
45
- spinner.replaceWith(Object.assign(document.createElement('span'), {
46
- className: 'text-muted small',
47
- textContent: 'Sign in to view',
48
- }));
49
- });
50
- }
51
-
52
40
  // Load all dashboard data in parallel
53
41
  async function loadDashboard() {
54
42
  const results = await Promise.allSettled([
@@ -74,54 +62,97 @@ async function loadStatCards() {
74
62
  const now = Math.floor(Date.now() / 1000);
75
63
  const thirtyDaysAgo = now - (30 * 24 * 60 * 60);
76
64
 
77
- const [totalUsers, newUsers, activeSubscriptions, pushSubscribers] = await Promise.allSettled([
65
+ const [totalUsers, newUsers, totalNotifications, newNotifications] = await Promise.allSettled([
78
66
  getCountFromServer(collection(db, 'users')),
79
67
  getCountFromServer(query(collection(db, 'users'), where('metadata.created.timestampUNIX', '>=', thirtyDaysAgo))),
80
- getCountFromServer(query(collection(db, 'users'), where('subscription.status', '==', 'active'), where('subscription.product.id', '!=', 'basic'))),
81
68
  getCountFromServer(collection(db, 'notifications')),
69
+ getCountFromServer(query(collection(db, 'notifications'), where('metadata.created.timestampUNIX', '>=', thirtyDaysAgo))),
82
70
  ]);
83
71
 
84
72
  setStatValue('stat-total-users', totalUsers);
85
- setStatValue('stat-new-users', newUsers);
86
- setStatValue('stat-subscriptions', activeSubscriptions);
87
- setStatValue('stat-notifications', pushSubscribers);
73
+ setStatSubValue('stat-new-users', newUsers, 'in 30d');
74
+ setStatValue('stat-notifications', totalNotifications);
75
+ setStatSubValue('stat-new-notifications', newNotifications, 'in 30d');
88
76
  }
89
77
 
90
78
  // ============================================
91
79
  // Subscriber Data (for charts)
92
80
  // ============================================
93
81
  async function loadSubscriberData() {
94
- const firestore = webManager.firestore();
82
+ const { collection, query, where, getCountFromServer } = await import('firebase/firestore');
83
+ const db = webManager.firebaseFirestore;
95
84
 
96
- const snapshot = await firestore.collection('users')
97
- .where('subscription.status', '==', 'active')
98
- .get();
85
+ // Fetch brand config to get product list and available frequencies
86
+ const brandConfig = await fetch(`${webManager.getApiUrl()}/backend-manager/brand`, {
87
+ response: 'json',
88
+ tries: 2,
89
+ });
90
+
91
+ const products = (brandConfig?.payment?.products || []).filter((p) => p.id !== 'basic');
92
+ const frequencyIds = [...new Set(products.flatMap((p) => Object.keys(p.prices || {})))];
99
93
 
100
- // Group by plan
94
+ // Run count queries for each product × frequency in parallel
95
+ const countQueries = products.flatMap((product) =>
96
+ frequencyIds.map((freq) =>
97
+ getCountFromServer(query(
98
+ collection(db, 'users'),
99
+ where('subscription.status', '==', 'active'),
100
+ where('subscription.product.id', '==', product.id),
101
+ where('subscription.payment.frequency', '==', freq),
102
+ )).then((snap) => ({ planId: product.id, frequency: freq, count: snap.data().count }))
103
+ )
104
+ );
105
+
106
+ const results = await Promise.all(countQueries);
107
+
108
+ // Build chart data from counts
101
109
  const plans = {};
102
110
  const frequencies = {};
103
111
 
104
- snapshot.docs.forEach((doc) => {
105
- const data = doc.data();
106
- const planId = data?.subscription?.product?.id || 'basic';
107
- const frequency = data?.subscription?.payment?.frequency || 'unknown';
112
+ results.forEach(({ planId, frequency, count }) => {
113
+ if (count === 0) {
114
+ return;
115
+ }
108
116
 
109
- // Plan counts
110
- plans[planId] = (plans[planId] || 0) + 1;
117
+ plans[planId] = (plans[planId] || 0) + count;
111
118
 
112
- // Frequency per plan
113
119
  if (!frequencies[planId]) {
114
- frequencies[planId] = { monthly: 0, annually: 0, other: 0 };
120
+ frequencies[planId] = Object.fromEntries(frequencyIds.map((f) => [f, 0]));
115
121
  }
116
- if (frequency === 'monthly' || frequency === 'annually') {
117
- frequencies[planId][frequency]++;
118
- } else {
119
- frequencies[planId].other++;
122
+ frequencies[planId][frequency] = count;
123
+ });
124
+
125
+ // Calculate MRR from counts × product prices
126
+ const MONTHS_PER_FREQUENCY = { daily: 1 / 30, weekly: 1 / 4, monthly: 1, annually: 12 };
127
+ let mrr = 0;
128
+ let totalSubscribers = 0;
129
+
130
+ results.forEach(({ planId, frequency, count }) => {
131
+ if (count === 0) {
132
+ return;
120
133
  }
134
+
135
+ const product = products.find((p) => p.id === planId);
136
+ const priceEntry = product?.prices?.[frequency];
137
+ const price = typeof priceEntry === 'object' ? (priceEntry?.amount || 0) : Number(priceEntry) || 0;
138
+ const months = MONTHS_PER_FREQUENCY[frequency] || 1;
139
+
140
+ mrr += (price / months) * count;
141
+ totalSubscribers += count;
121
142
  });
122
143
 
144
+ // Set MRR stat card
145
+ const $mrr = document.getElementById('stat-mrr');
146
+ if ($mrr) {
147
+ $mrr.textContent = `$${Math.round(mrr).toLocaleString()}`;
148
+ }
149
+ const $mrrCount = document.getElementById('stat-mrr-count');
150
+ if ($mrrCount) {
151
+ $mrrCount.textContent = `${totalSubscribers.toLocaleString()} subscriber${totalSubscribers === 1 ? '' : 's'}`;
152
+ }
153
+
123
154
  renderPlanChart(plans);
124
- renderFrequencyChart(frequencies);
155
+ renderFrequencyChart(frequencies, frequencyIds);
125
156
  }
126
157
 
127
158
  // ============================================
@@ -202,7 +233,7 @@ function renderPlanChart(plans) {
202
233
  });
203
234
  }
204
235
 
205
- function renderFrequencyChart(frequencies) {
236
+ function renderFrequencyChart(frequencies, frequencyIds) {
206
237
  const $loading = document.getElementById('chart-frequency-loading');
207
238
  const $canvas = document.getElementById('chart-frequency');
208
239
  if (!$canvas) {
@@ -229,23 +260,11 @@ function renderFrequencyChart(frequencies) {
229
260
  type: 'bar',
230
261
  data: {
231
262
  labels: planIds.map(capitalize),
232
- datasets: [
233
- {
234
- label: 'Monthly',
235
- data: planIds.map((id) => frequencies[id].monthly),
236
- backgroundColor: colors.palette[0],
237
- },
238
- {
239
- label: 'Annually',
240
- data: planIds.map((id) => frequencies[id].annually),
241
- backgroundColor: colors.palette[1],
242
- },
243
- {
244
- label: 'Other',
245
- data: planIds.map((id) => frequencies[id].other),
246
- backgroundColor: colors.palette[3],
247
- },
248
- ],
263
+ datasets: frequencyIds.map((freq, i) => ({
264
+ label: capitalize(freq),
265
+ data: planIds.map((id) => frequencies[id]?.[freq] || 0),
266
+ backgroundColor: colors.palette[i % colors.palette.length],
267
+ })),
249
268
  },
250
269
  options: {
251
270
  responsive: true,
@@ -29,7 +29,6 @@ export default (Manager) => {
29
29
 
30
30
  webManager.auth().listen({ once: true }, async (state) => {
31
31
  if (!state.user) {
32
- showUnauthenticated();
33
32
  return;
34
33
  }
35
34
 
@@ -40,14 +39,6 @@ export default (Manager) => {
40
39
  });
41
40
  };
42
41
 
43
- // Show unauthenticated state
44
- function showUnauthenticated() {
45
- const $empty = document.getElementById('docs-empty');
46
- if ($empty) {
47
- $empty.textContent = 'Sign in to view';
48
- }
49
- }
50
-
51
42
  // ============================================
52
43
  // Initialize
53
44
  // ============================================
@@ -5,7 +5,7 @@
5
5
  // Libraries
6
6
  import { FormManager } from '__main_assets__/js/libs/form-manager.js';
7
7
  import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
8
- import { formatTimeAgo, capitalize, escapeHtml, setStatValue } from '__main_assets__/js/libs/admin-helpers.js';
8
+ import { formatTimeAgo, capitalize, escapeHtml, setStatValue, setStatSubValue } from '__main_assets__/js/libs/admin-helpers.js';
9
9
  import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
10
10
 
11
11
  // State
@@ -26,7 +26,6 @@ export default (Manager) => {
26
26
 
27
27
  webManager.auth().listen({ once: true }, async (state) => {
28
28
  if (!state.user) {
29
- showUnauthenticated();
30
29
  return;
31
30
  }
32
31
 
@@ -38,16 +37,6 @@ export default (Manager) => {
38
37
  });
39
38
  };
40
39
 
41
- // Show unauthenticated state
42
- function showUnauthenticated() {
43
- document.querySelectorAll('.spinner-border').forEach((spinner) => {
44
- spinner.replaceWith(Object.assign(document.createElement('span'), {
45
- className: 'text-muted small',
46
- textContent: 'Sign in to view',
47
- }));
48
- });
49
- }
50
-
51
40
  // Initialize FormManager for search
52
41
  function initForm() {
53
42
  formManager = new FormManager('#user-search-form', {
@@ -73,15 +62,17 @@ async function loadStatCards() {
73
62
  const now = Math.floor(Date.now() / 1000);
74
63
  const thirtyDaysAgo = now - (30 * 24 * 60 * 60);
75
64
 
76
- const [totalUsers, activeSubs, newUsers] = await Promise.allSettled([
65
+ const [totalUsers, newUsers, activeSubs, activeUsers] = await Promise.allSettled([
77
66
  getCountFromServer(collection(db, 'users')),
78
- getCountFromServer(query(collection(db, 'users'), where('subscription.expires.timestampUNIX', '>=', now))),
67
+ getCountFromServer(query(collection(db, 'users'), where('metadata.created.timestampUNIX', '>=', thirtyDaysAgo))),
68
+ getCountFromServer(query(collection(db, 'users'), where('subscription.status', '==', 'active'), where('subscription.product.id', '!=', 'basic'))),
79
69
  getCountFromServer(query(collection(db, 'users'), where('metadata.updated.timestampUNIX', '>=', thirtyDaysAgo))),
80
70
  ]);
81
71
 
82
72
  setStatValue('stat-total-users', totalUsers);
73
+ setStatSubValue('stat-new-users', newUsers, 'in 30d');
83
74
  setStatValue('stat-active-subs', activeSubs);
84
- setStatValue('stat-new-users', newUsers);
75
+ setStatValue('stat-active-users', activeUsers);
85
76
  }
86
77
 
87
78
  // Search users by email prefix or UID prefix
@@ -32,7 +32,7 @@ prerender_icons:
32
32
  <!-- Stats Cards -->
33
33
  <div class="row g-3 mb-4">
34
34
  <!-- Total Users -->
35
- <div class="col-lg-3 col-md-6">
35
+ <div class="col-lg-4 col-md-6">
36
36
  <div class="card h-100">
37
37
  <div class="card-body text-center">
38
38
  <div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-primary bg-opacity-10 mx-auto mb-2">
@@ -40,44 +40,31 @@ prerender_icons:
40
40
  </div>
41
41
  <h6 class="text-muted mb-1">Total users</h6>
42
42
  <h3 class="mb-0" id="stat-total-users">
43
- <span class="spinner-border spinner-border-sm"></span>
44
- </h3>
45
- </div>
46
- </div>
47
- </div>
48
-
49
- <!-- New Users (30d) -->
50
- <div class="col-lg-3 col-md-6">
51
- <div class="card h-100">
52
- <div class="card-body text-center">
53
- <div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-success bg-opacity-10 mx-auto mb-2">
54
- {% uj_icon "user-plus", "fa-xl text-success" %}
55
- </div>
56
- <h6 class="text-muted mb-1">New users (30d)</h6>
57
- <h3 class="mb-0" id="stat-new-users">
58
- <span class="spinner-border spinner-border-sm"></span>
43
+ <span class="spinner-border spinner-border-sm fs-6"></span>
59
44
  </h3>
45
+ <small class="text-muted" id="stat-new-users"></small>
60
46
  </div>
61
47
  </div>
62
48
  </div>
63
49
 
64
- <!-- Active Subscriptions -->
65
- <div class="col-lg-3 col-md-6">
50
+ <!-- MRR -->
51
+ <div class="col-lg-4 col-md-6">
66
52
  <div class="card h-100">
67
53
  <div class="card-body text-center">
68
54
  <div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-info bg-opacity-10 mx-auto mb-2">
69
55
  {% uj_icon "credit-card", "fa-xl text-info" %}
70
56
  </div>
71
- <h6 class="text-muted mb-1">Active subscriptions</h6>
72
- <h3 class="mb-0" id="stat-subscriptions">
73
- <span class="spinner-border spinner-border-sm"></span>
57
+ <h6 class="text-muted mb-1">MRR</h6>
58
+ <h3 class="mb-0" id="stat-mrr">
59
+ <span class="spinner-border spinner-border-sm fs-6"></span>
74
60
  </h3>
61
+ <small class="text-muted" id="stat-mrr-count"></small>
75
62
  </div>
76
63
  </div>
77
64
  </div>
78
65
 
79
66
  <!-- Push Subscribers -->
80
- <div class="col-lg-3 col-md-6">
67
+ <div class="col-lg-4 col-md-6">
81
68
  <div class="card h-100">
82
69
  <div class="card-body text-center">
83
70
  <div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-warning bg-opacity-10 mx-auto mb-2">
@@ -85,8 +72,9 @@ prerender_icons:
85
72
  </div>
86
73
  <h6 class="text-muted mb-1">Push subscribers</h6>
87
74
  <h3 class="mb-0" id="stat-notifications">
88
- <span class="spinner-border spinner-border-sm"></span>
75
+ <span class="spinner-border spinner-border-sm fs-6"></span>
89
76
  </h3>
77
+ <small class="text-muted" id="stat-new-notifications"></small>
90
78
  </div>
91
79
  </div>
92
80
  </div>
@@ -58,8 +58,9 @@ prerender_icons:
58
58
  </div>
59
59
  <h6 class="text-muted mb-1">Total users</h6>
60
60
  <h3 class="mb-0" id="stat-total-users">
61
- <span class="spinner-border spinner-border-sm"></span>
61
+ <span class="spinner-border spinner-border-sm fs-6"></span>
62
62
  </h3>
63
+ <small class="text-muted" id="stat-new-users"></small>
63
64
  </div>
64
65
  </div>
65
66
  </div>
@@ -73,22 +74,22 @@ prerender_icons:
73
74
  </div>
74
75
  <h6 class="text-muted mb-1">Active subscriptions</h6>
75
76
  <h3 class="mb-0" id="stat-active-subs">
76
- <span class="spinner-border spinner-border-sm"></span>
77
+ <span class="spinner-border spinner-border-sm fs-6"></span>
77
78
  </h3>
78
79
  </div>
79
80
  </div>
80
81
  </div>
81
82
 
82
- <!-- New Users (30d) -->
83
+ <!-- Active Users (30d) -->
83
84
  <div class="col-lg-4 col-md-6">
84
85
  <div class="card h-100">
85
86
  <div class="card-body text-center">
86
87
  <div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-info bg-opacity-10 mx-auto mb-2">
87
88
  {% uj_icon "user-plus", "fa-xl text-info" %}
88
89
  </div>
89
- <h6 class="text-muted mb-1">New users (30d)</h6>
90
- <h3 class="mb-0" id="stat-new-users">
91
- <span class="spinner-border spinner-border-sm"></span>
90
+ <h6 class="text-muted mb-1">Active users (30d)</h6>
91
+ <h3 class="mb-0" id="stat-active-users">
92
+ <span class="spinner-border spinner-border-sm fs-6"></span>
92
93
  </h3>
93
94
  </div>
94
95
  </div>
@@ -46,156 +46,158 @@ sitemap:
46
46
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/12.11.0/firebase-app.js';
47
47
  import { getAuth, onAuthStateChanged } from 'https://www.gstatic.com/firebasejs/12.11.0/firebase-auth.js';
48
48
 
49
- const config = {{ page.resolved.web_manager.firebase.app.config | jsonify }};
50
-
51
- document.getElementById('ua').textContent = navigator.userAgent;
49
+ (async function() {
50
+ const config = {{ page.resolved.web_manager.firebase.app.config | jsonify }};
51
+
52
+ document.getElementById('ua').textContent = navigator.userAgent;
53
+
54
+ // Unregister all service workers first
55
+ if ('serviceWorker' in navigator) {
56
+ const regs = await navigator.serviceWorker.getRegistrations();
57
+ console.log(`[Setup] Found ${regs.length} service worker(s)`);
58
+ for (const reg of regs) {
59
+ const result = await reg.unregister();
60
+ console.log(`[Setup] Unregistered service worker: ${reg.scope} (success: ${result})`);
61
+ }
62
+ const swMsg = regs.length > 0
63
+ ? `Unregistered ${regs.length} service worker(s)`
64
+ : 'No service workers found';
65
+ document.getElementById('sw-status').textContent = swMsg;
66
+ document.getElementById('status').textContent = swMsg + '. Running tests...';
67
+ } else {
68
+ console.log('[Setup] Service workers not supported');
69
+ document.getElementById('sw-status').textContent = 'Service workers not supported';
70
+ }
52
71
 
53
- // Unregister all service workers first
54
- if ('serviceWorker' in navigator) {
55
- const regs = await navigator.serviceWorker.getRegistrations();
56
- console.log(`[Setup] Found ${regs.length} service worker(s)`);
57
- for (const reg of regs) {
58
- const result = await reg.unregister();
59
- console.log(`[Setup] Unregistered service worker: ${reg.scope} (success: ${result})`);
72
+ function makeLogger(id) {
73
+ const $log = document.getElementById(id + '-log');
74
+ const $box = document.getElementById(id);
75
+ return {
76
+ log: (msg) => {
77
+ console.log(`[${id}]`, msg);
78
+ $log.textContent += msg + '\n';
79
+ $log.scrollTop = $log.scrollHeight;
80
+ },
81
+ pass: () => { $box.classList.add('success'); return 'PASS'; },
82
+ fail: () => { $box.classList.add('fail'); return 'FAIL'; },
83
+ };
60
84
  }
61
- const swMsg = regs.length > 0
62
- ? `Unregistered ${regs.length} service worker(s)`
63
- : 'No service workers found';
64
- document.getElementById('sw-status').textContent = swMsg;
65
- document.getElementById('status').textContent = swMsg + '. Running tests...';
66
- } else {
67
- console.log('[Setup] Service workers not supported');
68
- document.getElementById('sw-status').textContent = 'Service workers not supported';
69
- }
70
-
71
- function makeLogger(id) {
72
- const $log = document.getElementById(id + '-log');
73
- const $box = document.getElementById(id);
74
- return {
75
- log: (msg) => {
76
- console.log(`[${id}]`, msg);
77
- $log.textContent += msg + '\n';
78
- $log.scrollTop = $log.scrollHeight;
79
- },
80
- pass: () => { $box.classList.add('success'); return 'PASS'; },
81
- fail: () => { $box.classList.add('fail'); return 'FAIL'; },
82
- };
83
- }
84
-
85
- const results = {};
86
-
87
- // REST baseline
88
- async function testRest(uid, token) {
89
- const t = makeLogger('rest');
90
- try {
91
- const url = `https://firestore.googleapis.com/v1/projects/${config.projectId}/databases/(default)/documents/users/${uid}`;
92
- t.log('Fetching via REST...');
93
- const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
94
- t.log(`Status: ${res.status}`);
95
- if (res.ok) {
96
- const data = await res.json();
97
- t.log('SUCCESS');
98
- results['REST'] = t.pass();
99
- } else {
100
- const text = await res.text();
101
- t.log('ERROR: ' + text.substring(0, 150));
102
- results['REST'] = t.fail();
103
- }
104
- } catch (e) { t.log('ERROR: ' + e.message); results['REST'] = t.fail(); }
105
- }
106
85
 
107
- // Generic version test using dynamic import
108
- async function testVersion(id, version, uid, useLP) {
109
- const t = makeLogger(id);
110
- try {
111
- const label = useLP ? 'initializeFirestore+forceLongPolling' : 'getFirestore';
112
- t.log(`Loading Firebase ${version}...`);
86
+ const results = {};
113
87
 
114
- const appMod = await import(`https://www.gstatic.com/firebasejs/${version}/firebase-app.js`);
115
- const fsMod = await import(`https://www.gstatic.com/firebasejs/${version}/firebase-firestore.js`);
88
+ // REST baseline
89
+ async function testRest(uid, token) {
90
+ const t = makeLogger('rest');
91
+ try {
92
+ const url = `https://firestore.googleapis.com/v1/projects/${config.projectId}/databases/(default)/documents/users/${uid}`;
93
+ t.log('Fetching via REST...');
94
+ const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
95
+ t.log(`Status: ${res.status}`);
96
+ if (res.ok) {
97
+ const data = await res.json();
98
+ t.log('SUCCESS');
99
+ results['REST'] = t.pass();
100
+ } else {
101
+ const text = await res.text();
102
+ t.log('ERROR: ' + text.substring(0, 150));
103
+ results['REST'] = t.fail();
104
+ }
105
+ } catch (e) { t.log('ERROR: ' + e.message); results['REST'] = t.fail(); }
106
+ }
116
107
 
117
- const appName = id;
118
- let app;
108
+ // Generic version test using dynamic import
109
+ async function testVersion(id, version, uid, useLP) {
110
+ const t = makeLogger(id);
119
111
  try {
120
- app = appMod.initializeApp(config, appName);
112
+ const label = useLP ? 'initializeFirestore+forceLongPolling' : 'getFirestore';
113
+ t.log(`Loading Firebase ${version}...`);
114
+
115
+ const appMod = await import(`https://www.gstatic.com/firebasejs/${version}/firebase-app.js`);
116
+ const fsMod = await import(`https://www.gstatic.com/firebasejs/${version}/firebase-firestore.js`);
117
+
118
+ const appName = id;
119
+ let app;
120
+ try {
121
+ app = appMod.initializeApp(config, appName);
122
+ } catch (e) {
123
+ app = appMod.getApp(appName);
124
+ }
125
+
126
+ let db;
127
+ if (useLP) {
128
+ t.log(`initializeFirestore + experimentalForceLongPolling...`);
129
+ db = fsMod.initializeFirestore(app, { experimentalForceLongPolling: true });
130
+ } else {
131
+ t.log(`getFirestore...`);
132
+ db = fsMod.getFirestore(app);
133
+ }
134
+
135
+ t.log(`Fetching users/${uid} (10s timeout)...`);
136
+
137
+ // Race against a timeout
138
+ const fetchPromise = fsMod.getDoc(fsMod.doc(db, 'users', uid));
139
+ const timeoutPromise = new Promise((_, reject) =>
140
+ setTimeout(() => reject(new Error('TIMEOUT after 10s')), 10000)
141
+ );
142
+
143
+ const snap = await Promise.race([fetchPromise, timeoutPromise]);
144
+
145
+ if (snap.exists()) {
146
+ t.log('SUCCESS: ' + JSON.stringify(snap.data()).substring(0, 100));
147
+ results[id] = t.pass();
148
+ } else {
149
+ t.log('Doc not found');
150
+ results[id] = t.fail();
151
+ }
121
152
  } catch (e) {
122
- app = appMod.getApp(appName);
123
- }
124
-
125
- let db;
126
- if (useLP) {
127
- t.log(`initializeFirestore + experimentalForceLongPolling...`);
128
- db = fsMod.initializeFirestore(app, { experimentalForceLongPolling: true });
129
- } else {
130
- t.log(`getFirestore...`);
131
- db = fsMod.getFirestore(app);
132
- }
133
-
134
- t.log(`Fetching users/${uid} (10s timeout)...`);
135
-
136
- // Race against a timeout
137
- const fetchPromise = fsMod.getDoc(fsMod.doc(db, 'users', uid));
138
- const timeoutPromise = new Promise((_, reject) =>
139
- setTimeout(() => reject(new Error('TIMEOUT after 10s')), 10000)
140
- );
141
-
142
- const snap = await Promise.race([fetchPromise, timeoutPromise]);
143
-
144
- if (snap.exists()) {
145
- t.log('SUCCESS: ' + JSON.stringify(snap.data()).substring(0, 100));
146
- results[id] = t.pass();
147
- } else {
148
- t.log('Doc not found');
149
- results[id] = t.fail();
150
- }
151
- } catch (e) {
152
- const code = e.code || '';
153
- if (code === 'permission-denied') {
154
- t.log('TRANSPORT OK (permission-denied = network works, just no auth)');
155
- results[id] = t.pass();
156
- } else {
157
- t.log(`ERROR: ${code} ${e.message}`);
158
- results[id] = t.fail();
153
+ const code = e.code || '';
154
+ if (code === 'permission-denied') {
155
+ t.log('TRANSPORT OK (permission-denied = network works, just no auth)');
156
+ results[id] = t.pass();
157
+ } else {
158
+ t.log(`ERROR: ${code} ${e.message}`);
159
+ results[id] = t.fail();
160
+ }
159
161
  }
160
162
  }
161
- }
162
163
 
163
- // Main
164
- const defaultApp = initializeApp(config);
165
- const auth = getAuth(defaultApp);
164
+ // Main
165
+ const defaultApp = initializeApp(config);
166
+ const auth = getAuth(defaultApp);
166
167
 
167
- onAuthStateChanged(auth, async (user) => {
168
- if (!user) {
169
- document.getElementById('status').textContent = 'Not signed in. Sign in at /dashboard first.';
170
- return;
171
- }
168
+ onAuthStateChanged(auth, async (user) => {
169
+ if (!user) {
170
+ document.getElementById('status').textContent = 'Not signed in. Sign in at /dashboard first.';
171
+ return;
172
+ }
172
173
 
173
- const uid = user.uid;
174
- const token = await user.getIdToken();
175
- document.getElementById('status').textContent = `Signed in: ${uid}. Running tests...`;
176
-
177
- // Run REST first
178
- await testRest(uid, token);
179
-
180
- // Run version tests (sequential to avoid interference)
181
- await testVersion('v10', '10.14.0', uid, false);
182
- await testVersion('v10lp', '10.14.0', uid, true);
183
- await testVersion('v11', '11.0.0', uid, false);
184
- await testVersion('v11lp', '11.0.0', uid, true);
185
- await testVersion('v12_0', '12.0.0', uid, false);
186
- await testVersion('v12_0lp', '12.0.0', uid, true);
187
- await testVersion('v12_11', '12.11.0', uid, false);
188
- await testVersion('v12_11lp', '12.11.0', uid, true);
189
-
190
- // Show summary
191
- document.getElementById('status').textContent = 'All tests complete.';
192
- const $summary = document.getElementById('summary');
193
- $summary.style.display = 'block';
194
- $summary.innerHTML = '<h2 style="margin-bottom:8px">Results Summary</h2>' +
195
- Object.entries(results).map(([k, v]) =>
196
- `<div style="color:${v === 'PASS' ? '#0f0' : '#f00'}">${v}: ${k}</div>`
197
- ).join('');
198
- });
174
+ const uid = user.uid;
175
+ const token = await user.getIdToken();
176
+ document.getElementById('status').textContent = `Signed in: ${uid}. Running tests...`;
177
+
178
+ // Run REST first
179
+ await testRest(uid, token);
180
+
181
+ // Run version tests (sequential to avoid interference)
182
+ await testVersion('v10', '10.14.0', uid, false);
183
+ await testVersion('v10lp', '10.14.0', uid, true);
184
+ await testVersion('v11', '11.0.0', uid, false);
185
+ await testVersion('v11lp', '11.0.0', uid, true);
186
+ await testVersion('v12_0', '12.0.0', uid, false);
187
+ await testVersion('v12_0lp', '12.0.0', uid, true);
188
+ await testVersion('v12_11', '12.11.0', uid, false);
189
+ await testVersion('v12_11lp', '12.11.0', uid, true);
190
+
191
+ // Show summary
192
+ document.getElementById('status').textContent = 'All tests complete.';
193
+ const $summary = document.getElementById('summary');
194
+ $summary.style.display = 'block';
195
+ $summary.innerHTML = '<h2 style="margin-bottom:8px">Results Summary</h2>' +
196
+ Object.entries(results).map(([k, v]) =>
197
+ `<div style="color:${v === 'PASS' ? '#0f0' : '#f00'}">${v}: ${k}</div>`
198
+ ).join('');
199
+ });
200
+ })();
199
201
  </script>
200
202
  </body>
201
203
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {