backend-manager 5.10.2 → 5.10.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.
@@ -42,7 +42,7 @@ const { renderMarkdown } = require('./lib/markdown-renderer.js');
42
42
  const { uploadAssets, USE_CDN_URLS, REPO_OWNER, REPO_NAME, RAW_BASE } = require('./lib/image-host.js');
43
43
  const { buildPublicConfig } = require('../../../routes/brand/get.js');
44
44
  const { writeArticle, publishArticle } = require('../../../libraries/content/ghostii.js');
45
- const { trackContentSource, contentSourceHash, resolveNewsletterSources } = require('../../../libraries/content/source-resolver.js');
45
+ const { trackContentSource, contentSourceHash, resolveSources } = require('../../../libraries/content/source-resolver.js');
46
46
 
47
47
  /**
48
48
  * Generate newsletter content from parent server sources.
@@ -60,7 +60,6 @@ const { trackContentSource, contentSourceHash, resolveNewsletterSources } = requ
60
60
  * @param {string} [opts.campaignId] - Stable ID used as the folder name in newsletter-assets.
61
61
  * Defaults to the Firestore campaign doc ID if available.
62
62
  * @param {object[]} [opts.sources] - Pre-fetched sources (bypasses parent server claim)
63
- * @param {boolean} [opts.skipClaim] - Don't call PUT to mark sources as used
64
63
  * @param {boolean} [opts.skipImages] - Skip SVG/PNG generation (use placeholders)
65
64
  * @param {boolean} [opts.publishArticle] - When the linked-article build runs (config.article.enabled),
66
65
  * actually COMMIT the post to the website repo via admin/post.
@@ -108,13 +107,20 @@ async function generate(Manager, assistant, settings, opts = {}) {
108
107
 
109
108
  const admin = Manager.libraries?.admin;
110
109
 
111
- sources = await resolveNewsletterSources({
110
+ // Unified resolver — same picking/fallback/dedup as the blog pipeline.
111
+ // config.sourceCount controls how many sources feed the issue (default 6).
112
+ const resolved = await resolveSources({
112
113
  sources: contentSources,
114
+ count: config.sourceCount || 6,
113
115
  categories,
114
116
  admin,
115
117
  Manager,
116
118
  assistant,
117
119
  });
120
+
121
+ sources = resolved
122
+ .filter((r) => r.type === 'feed' || r.type === 'parent')
123
+ .map((r) => toNewsletterSource(r, categories));
118
124
  }
119
125
 
120
126
  if (!sources?.length) {
@@ -378,40 +384,45 @@ async function generate(Manager, assistant, settings, opts = {}) {
378
384
  }
379
385
  }
380
386
 
381
- // 3d. Fallback alert email — sent to the brand's internal alerts inbox when
382
- // Beehiiv draft creation fails. The newsletter is fully generated and
383
- // archived to GitHub at this point, so the email contains everything
384
- // needed for a human to manually upload to Beehiiv: HTML URL (one-shot
385
- // paste), markdown URL (per-section blocks for ad insertion), summary
386
- // URL, and the tags to set. Failure of THIS email is logged but never
387
- // blocks the cron — the campaign doc is still written either way.
388
- if (beehiivFailureReason) {
389
- await sendBeehiivFallbackEmail(Manager, assistant, {
390
- brand,
391
- subject: structure.subject,
392
- preheader: structure.preheader,
393
- tags: Array.isArray(structure.tags) ? structure.tags : [],
394
- htmlUrl,
395
- previewUrl,
396
- markdownUrl,
397
- summaryUrl,
398
- folderUrl: assetsFolderUrl,
399
- reason: beehiivFailureReason,
400
- articles: (articleResult?.published) ? [{ title: articleResult.article?.title || structure.sections?.[0]?.title, url: articleResult.url }] : [],
401
- });
402
- }
387
+ // 3d. Newsletter report email — always sent to the brand's internal alerts
388
+ // inbox after generation completes. Contains everything needed for
389
+ // review and manual Beehiiv upload if needed.
390
+ await sendNewsletterReportEmail(Manager, assistant, {
391
+ brand,
392
+ subject: structure.subject,
393
+ preheader: structure.preheader,
394
+ tags: Array.isArray(structure.tags) ? structure.tags : [],
395
+ htmlUrl,
396
+ previewUrl,
397
+ markdownUrl,
398
+ summaryUrl,
399
+ folderUrl: assetsFolderUrl,
400
+ beehiivPostId,
401
+ beehiivFailureReason,
402
+ articles: (articleResult?.published) ? [{ title: articleResult.article?.title || structure.sections?.[0]?.title, url: articleResult.url }] : [],
403
+ sources: sources.map(s => ({
404
+ title: s.title || '',
405
+ url: (s.url && s.url.startsWith('http')) ? s.url : '',
406
+ from: s.source?.from || '',
407
+ })),
408
+ });
403
409
 
404
- // 4. Track all sources in the unified content-sources collection
410
+ // 4. Mark all sources used in the unified content-sources collection
411
+ // only NOW that the newsletter actually generated successfully.
412
+ // trackingData comes from the resolver; pre-fetched test sources
413
+ // without it get a best-effort fallback.
405
414
  const admin = Manager.libraries?.admin;
406
415
  if (admin) {
407
416
  for (const source of sources) {
408
- const origin = source.source?.from?.startsWith?.('http') ? `$feed:${source.source.from}` : '$parent';
409
- await trackContentSource(admin, {
417
+ const tracking = source.trackingData || {
410
418
  url: source.url || source.id,
411
- origin,
412
- feedUrl: origin.startsWith('$feed:') ? origin.slice(6) : undefined,
419
+ origin: '$parent',
413
420
  itemId: source.id,
414
421
  itemTitle: source.title || '',
422
+ };
423
+
424
+ await trackContentSource(admin, {
425
+ ...tracking,
415
426
  usedBy: 'newsletter',
416
427
  brandId: brand?.id || '',
417
428
  }).catch((e) => {
@@ -555,12 +566,14 @@ async function buildLinkedArticle({ Manager, assistant, brand, config, structure
555
566
  }
556
567
 
557
568
  try {
569
+ const sourceUrls = sources.map(s => s.url || s.id).filter(Boolean);
558
570
  const result = await publishArticle(assistant, {
559
571
  brand: publicConfig,
560
572
  article,
561
573
  id: Math.round(Date.now() / 1000),
562
574
  author: config?.article?.author,
563
575
  postPath: 'newsletter',
576
+ source: sourceUrls.join(', '),
564
577
  });
565
578
 
566
579
  if (result?.path) {
@@ -590,43 +603,52 @@ async function buildLinkedArticle({ Manager, assistant, brand, config, structure
590
603
  * @param {object} assistant
591
604
  * @param {object} args
592
605
  * @param {object} args.brand
593
- * @param {string} args.subject - The newsletter's subject (used in the alert subject)
606
+ * @param {string} args.subject
594
607
  * @param {string} args.preheader
595
608
  * @param {string[]} args.tags
596
- * @param {string} args.htmlUrl - GitHub raw URL to the fully-rendered HTML
597
- * @param {string} [args.markdownUrl] - GitHub raw URL to the per-section markdown
598
- * @param {string} [args.previewUrl] - GitHub Pages URL for browser-rendered HTML preview
599
- * @param {string} [args.summaryUrl] - GitHub raw URL to the 2-3 sentence summary
600
- * @param {string} [args.folderUrl] - GitHub folder URL (browseable archive)
601
- * @param {string} args.reason - Why Beehiiv upload failed (API error message)
609
+ * @param {string} [args.htmlUrl]
610
+ * @param {string} [args.markdownUrl]
611
+ * @param {string} [args.previewUrl]
612
+ * @param {string} [args.summaryUrl]
613
+ * @param {string} [args.folderUrl]
614
+ * @param {string} [args.beehiivPostId] - Beehiiv post ID (if upload succeeded)
615
+ * @param {string} [args.beehiivFailureReason] - Why Beehiiv upload failed (null if succeeded)
616
+ * @param {object[]} [args.articles] - Published linked articles
617
+ * @param {object[]} [args.sources] - Content sources used for generation
602
618
  */
603
- async function sendBeehiivFallbackEmail(Manager, assistant, args) {
604
- // Send TO and FROM the same internal alerts inbox — alerts@{brandDomain}.
605
- // The `sender: 'internal'` SENDERS entry already resolves the FROM address
606
- // to this; we mirror the same domain for the TO so it's a self-addressed
607
- // operational alert (no human inbox involved).
619
+ async function sendNewsletterReportEmail(Manager, assistant, args) {
608
620
  const brandDomain = Manager.config?.brand?.contact?.email?.split('@')[1];
609
621
 
610
622
  if (!brandDomain) {
611
- assistant.log('Newsletter generator: Beehiiv fallback email skipped — no brand.contact.email');
623
+ assistant.log('Newsletter generator: report email skipped — no brand.contact.email');
612
624
  return;
613
625
  }
614
626
 
615
627
  const alertsEmail = `alerts@${brandDomain}`;
628
+ const beehiivOk = !!args.beehiivPostId;
616
629
 
617
630
  try {
618
631
  const email = Manager.Email(assistant);
619
632
  const messageLines = [];
620
633
 
621
- messageLines.push(`The newsletter is generated and archived, but Beehiiv draft creation failed. It needs to be manually uploaded.`);
634
+ // --- Status ---
635
+ if (beehiivOk) {
636
+ messageLines.push(`The newsletter has been generated and uploaded to Beehiiv.`);
637
+ } else {
638
+ messageLines.push(`The newsletter has been generated but Beehiiv upload failed. It needs to be manually uploaded.`);
639
+ messageLines.push('');
640
+ messageLines.push('<strong>Beehiiv failure reason</strong>');
641
+ messageLines.push('<ul>');
642
+ messageLines.push(`<li>${args.beehiivFailureReason || 'unknown'}</li>`);
643
+ messageLines.push('</ul>');
644
+ }
622
645
  messageLines.push('');
623
646
 
624
- // --- Failure ---
625
- messageLines.push('<strong>Failure reason</strong>');
626
- messageLines.push('<ul>');
627
- messageLines.push(`<li>${args.reason}</li>`);
628
- messageLines.push('</ul>');
629
- messageLines.push('');
647
+ // --- Preview button (big, prominent) ---
648
+ if (args.previewUrl) {
649
+ messageLines.push(`<p style="text-align:center;margin:16px 0;"><a href="${args.previewUrl}" style="display:inline-block;padding:14px 32px;background:#1a1a2e;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;">Preview Newsletter</a></p>`);
650
+ messageLines.push('');
651
+ }
630
652
 
631
653
  // --- Details ---
632
654
  messageLines.push('<strong>Newsletter details</strong>');
@@ -640,14 +662,13 @@ async function sendBeehiivFallbackEmail(Manager, assistant, args) {
640
662
  messageLines.push('');
641
663
 
642
664
  // --- Full HTML ---
643
- messageLines.push('<strong>Full HTML</strong> — one-shot paste into Beehiiv');
644
- messageLines.push('<ul>');
645
- if (args.previewUrl) {
646
- messageLines.push(`<li><a href="${args.previewUrl}">Preview in browser</a></li>`);
665
+ if (args.htmlUrl) {
666
+ messageLines.push('<strong>Full HTML</strong> — one-shot paste into Beehiiv');
667
+ messageLines.push('<ul>');
668
+ messageLines.push(`<li><a href="${args.htmlUrl}">View raw HTML</a></li>`);
669
+ messageLines.push('</ul>');
670
+ messageLines.push('');
647
671
  }
648
- messageLines.push(`<li><a href="${args.htmlUrl}">View raw HTML</a></li>`);
649
- messageLines.push('</ul>');
650
- messageLines.push('');
651
672
 
652
673
  // --- Markdown ---
653
674
  if (args.markdownUrl) {
@@ -669,13 +690,33 @@ async function sendBeehiivFallbackEmail(Manager, assistant, args) {
669
690
 
670
691
  // --- Linked articles (only published ones) ---
671
692
  if (args.articles?.length) {
672
- messageLines.push('');
673
693
  messageLines.push('<strong>Linked articles</strong>');
674
694
  messageLines.push('<ul>');
675
695
  for (const article of args.articles) {
676
696
  messageLines.push(`<li><a href="${article.url}">${article.title || 'Article'}</a></li>`);
677
697
  }
678
698
  messageLines.push('</ul>');
699
+ messageLines.push('');
700
+ }
701
+
702
+ // --- Sources ---
703
+ if (args.sources?.length) {
704
+ messageLines.push(`<strong>Sources</strong> — ${args.sources.length} used`);
705
+ messageLines.push('<ul>');
706
+ for (const source of args.sources) {
707
+ const label = source.title || source.url || 'Untitled';
708
+ let fromLabel = '';
709
+ if (source.from) {
710
+ try { fromLabel = ` (${new URL(source.from).hostname})`; } catch { fromLabel = ` (${source.from})`; }
711
+ }
712
+ if (source.url) {
713
+ messageLines.push(`<li><a href="${source.url}">${label}</a>${fromLabel}</li>`);
714
+ } else {
715
+ messageLines.push(`<li>${label}${fromLabel}</li>`);
716
+ }
717
+ }
718
+ messageLines.push('</ul>');
719
+ messageLines.push('');
679
720
  }
680
721
 
681
722
  // --- All assets ---
@@ -686,29 +727,33 @@ async function sendBeehiivFallbackEmail(Manager, assistant, args) {
686
727
  messageLines.push('</ul>');
687
728
  }
688
729
 
730
+ const subjectLine = beehiivOk
731
+ ? `Newsletter generated: "${args.subject}"`
732
+ : `Newsletter ready for manual upload: "${args.subject}"`;
733
+
689
734
  await email.send({
690
- sender: 'internal', // resolves to alerts@{brandDomain}
735
+ sender: 'internal',
691
736
  to: alertsEmail,
692
- copy: false, // self-addressed operational alert — no CC/BCC clutter
693
- subject: `Newsletter ready for manual Beehiiv upload: "${args.subject}"`,
737
+ copy: false,
738
+ subject: subjectLine,
694
739
  template: 'card',
695
- categories: ['marketing/newsletter-manual-upload'],
740
+ categories: ['marketing/newsletter-report'],
696
741
  data: {
697
742
  email: {
698
- preview: `Beehiiv upload failed — newsletter awaiting manual upload from ${args.folderUrl || 'GitHub archive'}`,
743
+ preview: beehiivOk
744
+ ? `Newsletter generated and uploaded to Beehiiv`
745
+ : `Beehiiv upload failed — newsletter awaiting manual upload`,
699
746
  },
700
747
  content: {
701
- title: 'Newsletter Ready for Manual Upload',
748
+ title: beehiivOk ? 'Newsletter Generated' : 'Newsletter Ready for Manual Upload',
702
749
  message: messageLines.join('\n'),
703
750
  },
704
751
  },
705
752
  });
706
753
 
707
- assistant.log(`Newsletter generator: Beehiiv fallback alert sent to ${alertsEmail}`);
754
+ assistant.log(`Newsletter generator: report email sent to ${alertsEmail}`);
708
755
  } catch (e) {
709
- // Best-effort log and move on. We don't want a misconfigured email
710
- // setup to break the cron's Firestore write.
711
- assistant.error(`Newsletter generator: Beehiiv fallback email failed — ${e.message}`);
756
+ assistant.error(`Newsletter generator: report email failed ${e.message}`);
712
757
  }
713
758
  }
714
759
 
@@ -745,8 +790,41 @@ function aggregateTotals(filterMeta, structureMeta, images) {
745
790
  };
746
791
  }
747
792
 
748
- // fetchSources() removed — replaced by resolveNewsletterSources() from source-resolver.js
749
793
 
794
+ /**
795
+ * Map a resolved source (from resolveSources) to the newsletter item shape
796
+ * the structure generator expects. Parent items already arrive in that shape
797
+ * from the parent server; feed items get normalized.
798
+ *
799
+ * @param {object} resolved - resolved source ({ type, id, title, url, content, feedUrl?, raw, trackingData })
800
+ * @param {string[]} categories - the entry's configured categories
801
+ * @returns {object} newsletter source record (+ trackingData for used-marking)
802
+ */
803
+ function toNewsletterSource(resolved, categories) {
804
+ if (resolved.type === 'parent') {
805
+ return { ...resolved.raw, trackingData: resolved.trackingData };
806
+ }
807
+
808
+ // feed item
809
+ let hostname = '';
810
+ try { hostname = new URL(resolved.feedUrl).hostname; } catch (e) { /* ignore */ }
811
+
812
+ return {
813
+ id: resolved.id,
814
+ title: resolved.title,
815
+ subject: resolved.title,
816
+ category: (categories && categories[0]) || '',
817
+ categories: categories || [],
818
+ url: resolved.url,
819
+ source: {
820
+ from: hostname,
821
+ subject: resolved.title,
822
+ content: resolved.content,
823
+ },
824
+ ai: null,
825
+ trackingData: resolved.trackingData,
826
+ };
827
+ }
750
828
 
751
829
  /**
752
830
  * Generate a 20-character Firebase push ID (RTDB-style).
@@ -24,6 +24,11 @@ const IMAGE_REGEX = /(?:!\[(.*?)\]\((.*?)\))/img;
24
24
  const IMAGE_MAX_DIMENSION = 2048;
25
25
  const IMAGE_JPEG_QUALITY = 80;
26
26
 
27
+ // Non-JPG raster sources are converted to JPEG at ingest rather than rejected
28
+ // — stock CDNs beyond Unsplash (Pexels, Pixabay) commonly serve png/webp.
29
+ // Anything not in this list (and not already .jpg) is still rejected.
30
+ const CONVERTIBLE_IMAGE_EXTS = ['.png', '.webp'];
31
+
27
32
  module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
28
33
 
29
34
  // Require authentication
@@ -221,12 +226,35 @@ function applyImageCDNParams(src) {
221
226
  }
222
227
  }
223
228
 
229
+ if (url.hostname === 'images.pexels.com') {
230
+ if (!url.searchParams.has('w')) {
231
+ url.searchParams.set('w', String(IMAGE_MAX_DIMENSION));
232
+ }
233
+ if (!url.searchParams.has('auto')) {
234
+ url.searchParams.set('auto', 'compress');
235
+ }
236
+ }
237
+
224
238
  return url.toString();
225
239
  } catch (e) {
226
240
  return src;
227
241
  }
228
242
  }
229
243
 
244
+ // Helper: Build a readable message for a failed image download. Fetch errors
245
+ // can carry raw HTML bodies as the message (CDN 404 pages) — strip tags and
246
+ // truncate so callers surface "Could not download image (<url>): 404" instead
247
+ // of markup that downstream HTML rendering swallows.
248
+ function formatImageDownloadError(src, e) {
249
+ const reason = (e && typeof e.message === 'string' ? e.message : `${e}`)
250
+ .replace(/<[^>]*>/g, ' ')
251
+ .replace(/\s+/g, ' ')
252
+ .trim();
253
+ const truncated = reason.length > 200 ? `${reason.slice(0, 197)}...` : reason;
254
+
255
+ return `Could not download image (${src}): ${truncated || 'unknown error'}`;
256
+ }
257
+
230
258
  // Helper: Download image
231
259
  async function downloadImage(assistant, src, alt) {
232
260
  const fetch = assistant.Manager.require('wonderful-fetch');
@@ -242,6 +270,8 @@ async function downloadImage(assistant, src, alt) {
242
270
  const result = await fetch(url, {
243
271
  method: 'get',
244
272
  download: `${assistant.tmpdir}/${hyphenated}`,
273
+ }).catch(e => {
274
+ throw new Error(formatImageDownloadError(src, e));
245
275
  });
246
276
 
247
277
  result.filename = path.basename(result.path);
@@ -249,8 +279,13 @@ async function downloadImage(assistant, src, alt) {
249
279
 
250
280
  assistant.log('downloadImage(): Result', result.path);
251
281
 
282
+ // Convert supported non-JPG formats in place (mutates result.path/filename/ext)
283
+ if (CONVERTIBLE_IMAGE_EXTS.includes(result.ext)) {
284
+ await convertToJpeg(assistant, result);
285
+ }
286
+
252
287
  if (result.ext !== '.jpg') {
253
- throw new Error(`Images must be .jpg (not ${result.ext})`);
288
+ throw new Error(`Images must be .jpg, .png, or .webp (got ${result.ext}): ${src}`);
254
289
  }
255
290
 
256
291
  // Resize in place if the long edge exceeds IMAGE_MAX_DIMENSION
@@ -259,6 +294,36 @@ async function downloadImage(assistant, src, alt) {
259
294
  return result;
260
295
  }
261
296
 
297
+ // Helper: Convert a downloaded png/webp to progressive JPEG in place. Alpha is
298
+ // flattened onto white (JPEG has no transparency; default flatten is black).
299
+ // Mutates result.path/filename/ext to the new .jpg file and removes the
300
+ // original so the rest of the pipeline (resize, base64, GitHub path) only ever
301
+ // sees JPGs.
302
+ async function convertToJpeg(assistant, result) {
303
+ const sharp = assistant.Manager.require('sharp');
304
+ sharp.cache(false);
305
+
306
+ const newPath = result.path.replace(/\.[^.]+$/, '.jpg');
307
+ const buffer = await sharp(result.path)
308
+ .flatten({ background: '#ffffff' })
309
+ .jpeg({ quality: IMAGE_JPEG_QUALITY, progressive: true })
310
+ .toBuffer();
311
+
312
+ jetpack.write(newPath, buffer);
313
+
314
+ if (newPath !== result.path) {
315
+ jetpack.remove(result.path);
316
+ }
317
+
318
+ result.path = newPath;
319
+ result.filename = path.basename(newPath);
320
+ result.ext = '.jpg';
321
+
322
+ assistant.log(`convertToJpeg(): Converted to ${newPath}`);
323
+
324
+ return result;
325
+ }
326
+
262
327
  // Helper: Resize image in place if the long edge exceeds IMAGE_MAX_DIMENSION.
263
328
  // Re-encodes as progressive JPEG at IMAGE_JPEG_QUALITY. Short-circuits when the
264
329
  // source is already within the limit.
@@ -406,6 +471,9 @@ function formatClone(payload) {
406
471
 
407
472
  // Expose helpers + constants for tests
408
473
  module.exports.resizeImage = resizeImage;
474
+ module.exports.convertToJpeg = convertToJpeg;
475
+ module.exports.formatImageDownloadError = formatImageDownloadError;
476
+ module.exports.applyImageCDNParams = applyImageCDNParams;
409
477
  module.exports.IMAGE_MAX_DIMENSION = IMAGE_MAX_DIMENSION;
410
478
  module.exports.IMAGE_JPEG_QUALITY = IMAGE_JPEG_QUALITY;
411
479
 
@@ -2,5 +2,5 @@
2
2
  "env": {
3
3
  "TEST_EXTENDED_MODE": ""
4
4
  },
5
- "updatedAt": "2026-06-30T09:47:03.821Z"
5
+ "updatedAt": "2026-07-02T05:15:55.942Z"
6
6
  }
@@ -2,11 +2,11 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
2
2
  WARNING: sun.misc.Unsafe::objectFieldOffset has been called by akka.util.Unsafe (file:/Users/ian/.cache/firebase/emulators/firebase-database-emulator-v4.11.2.jar)
3
3
  WARNING: Please consider reporting this to the maintainers of class akka.util.Unsafe
4
4
  WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
5
- 02:47:10.430 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
6
- 02:47:10.526 [main] INFO com.firebase.server.forge.App$ - Listening at 127.0.0.1:9000
7
- 02:47:20.571 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - demo-backend-manager-default-rtdb successfully activated FBKV (SurveyIdle(0)) wait: 68ms, init: 0ms
8
- 02:47:20.599 [NamespaceSystem-blocking-namespace-operation-dispatcher-6] INFO com.firebase.core.namespace.StateManager - Namespace demo-backend-manager-default-rtdb status Active to Active
9
- 02:47:32.673 [Thread-0] INFO com.firebase.server.forge.App$ - Attempting graceful shutdown.
10
- 02:47:32.678 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.Terminator$Terminator - 1 actors left to terminate: demo-backend-manager-default-rtdb
11
- 02:47:32.681 [NamespaceSystem-akka.actor.default-dispatcher-5] INFO com.firebase.core.namespace.NamespaceActor - stopped namespace actor for demo-backend-manager-default-rtdb
12
- 02:47:32.684 [Thread-0] INFO com.firebase.server.forge.App$ - Graceful shutdown complete.
5
+ 22:16:03.306 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
6
+ 22:16:03.405 [main] INFO com.firebase.server.forge.App$ - Listening at 127.0.0.1:9000
7
+ 22:16:13.397 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - demo-backend-manager-default-rtdb successfully activated FBKV (SurveyIdle(0)) wait: 69ms, init: 0ms
8
+ 22:16:13.433 [NamespaceSystem-blocking-namespace-operation-dispatcher-6] INFO com.firebase.core.namespace.StateManager - Namespace demo-backend-manager-default-rtdb status Active to Active
9
+ 22:16:25.709 [Thread-0] INFO com.firebase.server.forge.App$ - Attempting graceful shutdown.
10
+ 22:16:25.716 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.Terminator$Terminator - 1 actors left to terminate: demo-backend-manager-default-rtdb
11
+ 22:16:25.720 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - stopped namespace actor for demo-backend-manager-default-rtdb
12
+ 22:16:25.723 [Thread-0] INFO com.firebase.server.forge.App$ - Graceful shutdown complete.