pushraker 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (23) hide show
  1. {pushraker-0.2.0/src/pushraker.egg-info → pushraker-0.2.2}/PKG-INFO +4 -3
  2. {pushraker-0.2.0 → pushraker-0.2.2}/README.md +3 -2
  3. {pushraker-0.2.0 → pushraker-0.2.2}/pyproject.toml +1 -1
  4. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/__init__.py +1 -1
  5. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/component.py +18 -7
  6. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/discord.py +2 -2
  7. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/email_html.py +1 -1
  8. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/images.py +1 -1
  9. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/klipper/pushraker.cfg +2 -2
  10. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/web/index.html +104 -15
  11. {pushraker-0.2.0 → pushraker-0.2.2/src/pushraker.egg-info}/PKG-INFO +4 -3
  12. {pushraker-0.2.0 → pushraker-0.2.2}/tests/test_pushraker.py +25 -7
  13. {pushraker-0.2.0 → pushraker-0.2.2}/LICENSE +0 -0
  14. {pushraker-0.2.0 → pushraker-0.2.2}/setup.cfg +0 -0
  15. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/__main__.py +0 -0
  16. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/printers/__init__.py +0 -0
  17. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/printers/snapmaker_u1.py +0 -0
  18. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/settings.py +0 -0
  19. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/SOURCES.txt +0 -0
  20. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/dependency_links.txt +0 -0
  21. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/requires.txt +0 -0
  22. {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/top_level.txt +0 -0
  23. {pushraker-0.2.0 → pushraker-0.2.2}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pushraker
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Push notifications for Klipper printers: a Moonraker component with pictures, sent through Apprise (ntfy, Pushover, Telegram, Discord, webhooks, email)
5
5
  Author: noon92
6
6
  License-Expression: GPL-3.0-or-later
@@ -30,8 +30,8 @@ own web page, live, with no Moonraker or Klipper restart.
30
30
 
31
31
  It grew out of the Push Notifications feature of the
32
32
  [Snapmaker U1 Extended Firmware](https://github.com/paxx12-snapmaker-u1/SnapmakerU1-Extended-Firmware),
33
- where it is built in; the user documentation lives there
34
- for now: [notifications](https://snapmakeru1-extended-firmware.pages.dev/notifications).
33
+ where it is built in. User documentation: the
34
+ [Push Notifications guide](docs/guide.md).
35
35
 
36
36
  **Status: 0.2.** The component has been in
37
37
  use on the Snapmaker U1 for a while; the page, the settings file and the
@@ -107,6 +107,7 @@ it out again; `--purge` also removes the settings and secrets.
107
107
  - `src/pushraker/web/index.html` — the settings page
108
108
  - `src/pushraker/klipper/pushraker.cfg` — the Klipper macro file
109
109
  - `src/pushraker/__main__.py` — `python -m pushraker install|uninstall|status`
110
+ - `docs/guide.md` — the user guide
110
111
  - `tests/` — `python tests/test_pushraker.py` (the component against a
111
112
  stand-in Moonraker; also runs `test_settings.py`)
112
113
 
@@ -10,8 +10,8 @@ own web page, live, with no Moonraker or Klipper restart.
10
10
 
11
11
  It grew out of the Push Notifications feature of the
12
12
  [Snapmaker U1 Extended Firmware](https://github.com/paxx12-snapmaker-u1/SnapmakerU1-Extended-Firmware),
13
- where it is built in; the user documentation lives there
14
- for now: [notifications](https://snapmakeru1-extended-firmware.pages.dev/notifications).
13
+ where it is built in. User documentation: the
14
+ [Push Notifications guide](docs/guide.md).
15
15
 
16
16
  **Status: 0.2.** The component has been in
17
17
  use on the Snapmaker U1 for a while; the page, the settings file and the
@@ -87,6 +87,7 @@ it out again; `--purge` also removes the settings and secrets.
87
87
  - `src/pushraker/web/index.html` — the settings page
88
88
  - `src/pushraker/klipper/pushraker.cfg` — the Klipper macro file
89
89
  - `src/pushraker/__main__.py` — `python -m pushraker install|uninstall|status`
90
+ - `docs/guide.md` — the user guide
90
91
  - `tests/` — `python tests/test_pushraker.py` (the component against a
91
92
  stand-in Moonraker; also runs `test_settings.py`)
92
93
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pushraker"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Push notifications for Klipper printers: a Moonraker component with pictures, sent through Apprise (ntfy, Pushover, Telegram, Discord, webhooks, email)"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -4,4 +4,4 @@
4
4
 
5
5
  """Push notifications for Klipper printers, as a Moonraker component."""
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.2.2"
@@ -9,7 +9,7 @@
9
9
  # * Moonraker job events: started, paused, resumed, error (and complete /
10
10
  # cancelled as a fallback when the print hooks did not report them).
11
11
  # * The Klipper print hooks (`_PRINT_END_NTFY`, `_CANCEL_PRINT_NTFY`), via
12
- # the `push_notification` remote method, so complete/cancelled snapshots
12
+ # the `pushraker` remote method (`push_notification` in 0.1), so complete/cancelled snapshots
13
13
  # are taken before the printer parks.
14
14
  # * Snapmaker's exception manager: coded faults and warnings, including
15
15
  # those that never reach print_stats (Klipper shutdowns, idle faults).
@@ -97,7 +97,9 @@ if TYPE_CHECKING:
97
97
  # The printer's own fault event (see printers.py); listened for from the
98
98
  # start, it simply never fires on a printer without one
99
99
  EXCEPTION_EVENTS = printers.EXCEPTION_EVENTS
100
- REMOTE_METHOD = "push_notification"
100
+ REMOTE_METHOD = "pushraker"
101
+ # The 0.1 name, still answered (a Klipper macro file from 0.1 calls it)
102
+ OLD_REMOTE_METHOD = "push_notification"
101
103
  MILESTONES = (25, 50, 75)
102
104
  ROW_IDS = (
103
105
  "started", "layer_started", "first_layer", "progress_25", "progress_50", "progress_75",
@@ -660,7 +662,9 @@ class PushNotifications:
660
662
  "~/printer_data")
661
663
  config_dir = os.path.dirname(app_args.get("config_file") or "") \
662
664
  or os.path.join(data_path, "config")
663
- self._low_path = os.path.join(data_path, ".push_notifications.json")
665
+ self._low_path = os.path.join(data_path, ".pushraker.json")
666
+ # 0.1 kept it under this name; moved on first read
667
+ self._old_low_path = os.path.join(data_path, ".push_notifications.json")
664
668
  # Where the settings come from: pushraker.conf (the page edits it,
665
669
  # the component re-reads it), or, when the [pushraker]
666
670
  # section itself carries notification options, those (the old
@@ -699,6 +703,7 @@ class PushNotifications:
699
703
  self.server.register_event_handler(
700
704
  "server:klippy_ready", self._on_klippy_ready)
701
705
  self.server.register_remote_method(REMOTE_METHOD, self._on_remote)
706
+ self.server.register_remote_method(OLD_REMOTE_METHOD, self._on_remote)
702
707
  self.server.register_event_handler(
703
708
  "server:gcode_response", self._on_gcode_response)
704
709
  if self.store is not None:
@@ -1385,7 +1390,7 @@ class PushNotifications:
1385
1390
  if not data.startswith(b"\xff\xd8") or len(data) > SNAPSHOT_MAX:
1386
1391
  logging.warning("[pushraker] snapshot is not a JPEG")
1387
1392
  return None
1388
- tmpdir = tempfile.mkdtemp(prefix="push_notifications_")
1393
+ tmpdir = tempfile.mkdtemp(prefix="pushraker_")
1389
1394
  path = os.path.join(tmpdir, "snapshot.jpg")
1390
1395
  try:
1391
1396
  with open(path, "wb") as f:
@@ -1525,7 +1530,7 @@ class PushNotifications:
1525
1530
  try:
1526
1531
  name = self._image_name(title, filename or self._job_filename,
1527
1532
  os.path.splitext(path)[1] or ".jpg")
1528
- tmpdir = tempfile.mkdtemp(prefix="push_notifications_")
1533
+ tmpdir = tempfile.mkdtemp(prefix="pushraker_")
1529
1534
  dest = os.path.join(tmpdir, name)
1530
1535
  shutil.copyfile(path, dest)
1531
1536
  return dest
@@ -1581,7 +1586,7 @@ class PushNotifications:
1581
1586
  """Emails about one print go in one thread: the same subject
1582
1587
  (printer and file name; the notification's title moves into the
1583
1588
  email) and References to the same thread id. Others keep their
1584
- title as subject. Sent as an HTML card (push_notifications_email)
1589
+ title as subject. Sent as an HTML card (email_html)
1585
1590
  with the image inline. The headers are set right before each attempt
1586
1591
  (the channel sends one at a time)."""
1587
1592
  thread = self._thread_for(rid)
@@ -3353,6 +3358,12 @@ class PushNotifications:
3353
3358
 
3354
3359
  def _low_load(self) -> Dict[str, float]:
3355
3360
  if self._low_sent is None:
3361
+ if (not os.path.exists(self._low_path)
3362
+ and os.path.exists(self._old_low_path)):
3363
+ try:
3364
+ os.replace(self._old_low_path, self._low_path)
3365
+ except OSError:
3366
+ pass
3356
3367
  try:
3357
3368
  with open(self._low_path) as f:
3358
3369
  data = json.load(f)
@@ -4188,7 +4199,7 @@ class PushNotifications:
4188
4199
  def _render_rows(self, rows: List[Dict[str, Any]]) -> Optional[str]:
4189
4200
  if images is None:
4190
4201
  return None
4191
- tmpdir = tempfile.mkdtemp(prefix="push_notifications_")
4202
+ tmpdir = tempfile.mkdtemp(prefix="pushraker_")
4192
4203
  try:
4193
4204
  return images.render(rows, os.path.join(tmpdir, "spools.jpg"))
4194
4205
  except Exception as e:
@@ -2,7 +2,7 @@
2
2
  # SPDX-PackageHomePage: https://github.com/noon92/pushraker
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2026 @noon92
4
4
 
5
- # Discord senders for push_notifications, registered as Apprise plugins so
5
+ # Discord senders for pushraker, registered as Apprise plugins so
6
6
  # they get the same queues, retries and fallbacks as the other services:
7
7
  #
8
8
  # discordhook://<webhook id>/<webhook token>?name=U1&ping=<mention>
@@ -30,7 +30,7 @@ from apprise.decorators import notify
30
30
  API = "https://discord.com/api/v10"
31
31
  USER_AGENT = ("DiscordBot (https://github.com/paxx12-snapmaker-u1/"
32
32
  "SnapmakerU1-Extended-Firmware, 1)")
33
- TIMEOUT = (10., 60.) # connect, read: same as push_notifications
33
+ TIMEOUT = (10., 60.) # connect, read: same as the component
34
34
  # Embed bar colours, matching the spool pictures
35
35
  COLOURS = {"info": 0x5F6269, "success": 0x24743A, "warning": 0xC46800,
36
36
  "failure": 0xB02626}
@@ -2,7 +2,7 @@
2
2
  # SPDX-PackageHomePage: https://github.com/noon92/pushraker
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2026 @noon92
4
4
 
5
- # HTML emails for push_notifications (and the Firmware Config test): a card
5
+ # HTML emails for pushraker: a card
6
6
  # with a colour bar for the kind of notification, the title, the text, the
7
7
  # image inline, and the links as buttons. Inline styles and tables only, so
8
8
  # it looks the same in Gmail, Outlook and phone mail apps. Apprise adds a
@@ -2,7 +2,7 @@
2
2
  # SPDX-PackageHomePage: https://github.com/noon92/pushraker
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2026 @noon92
4
4
 
5
- # Spool pictures for the Spoolman notifications of push_notifications.
5
+ # Spool pictures for pushraker's Spoolman notifications.
6
6
  #
7
7
  # One row per toolhead: its spool, drawn in the filament's colour (or
8
8
  # colours: wedges and stripes for a multi-colour filament) and filled
@@ -52,9 +52,9 @@ gcode:
52
52
  M400
53
53
 
54
54
  [gcode_macro _NTFY_SEND]
55
- description: Report a print hook event to the push_notifications component
55
+ description: Report a print hook event to the pushraker component
56
56
  gcode:
57
- {action_call_remote_method("push_notification", event=params.EVENT)}
57
+ {action_call_remote_method("pushraker", event=params.EVENT)}
58
58
 
59
59
  [gcode_macro _PRINT_END_NTFY]
60
60
  description: Notify print complete (call at the start of PRINT_END)
@@ -2,7 +2,7 @@
2
2
  <!-- SPDX-License-Identifier: GPL-3.0-or-later -->
3
3
  <!-- SPDX-PackageHomePage: https://github.com/noon92/pushraker -->
4
4
  <!-- SPDX-FileCopyrightText: Copyright (c) 2026 @noon92 -->
5
- <!-- pushraker's settings page. Talks to Moonraker's push_notifications
5
+ <!-- pushraker's settings page. Talks to Moonraker's pushraker
6
6
  component (GET/POST /server/pushraker/config, POST .../test),
7
7
  through the same origin (nginx proxies /server/ to Moonraker, as the
8
8
  Fluidd and Mainsail setups do) or Moonraker's own port. -->
@@ -20,6 +20,8 @@
20
20
  .container { max-width: 900px; margin: 0 auto; }
21
21
  h1 { color: var(--accent); margin-bottom: 16px; font-size: 22px; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
22
22
  h1 small { font-size: 12px; color: var(--muted); font-weight: normal; }
23
+ h1 small a { margin-right: 10px; }
24
+ .nt-guide { font-size: 12px; font-weight: 500; margin-left: 10px; }
23
25
  .card { background: var(--card); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
24
26
  .btn { border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; min-height: 34px; }
25
27
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
@@ -29,16 +31,33 @@
29
31
  .btn-secondary:hover:not(:disabled) { background: #666; }
30
32
  .pr-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; position: sticky; bottom: 0; background: var(--bg); padding: 12px 0; }
31
33
  .pr-status { font-size: 13px; color: var(--muted); }
32
- .pr-console { background: #111; color: #ddd; font-family: monospace; font-size: 12px; padding: 12px; border-radius: 6px; white-space: pre-wrap; max-height: 320px; overflow: auto; margin-bottom: 16px; display: none; }
34
+ /* Save & Apply is a group of its own: a gap from Cancel and Send Test
35
+ (on a phone: one line, those two at the left, it at the right) */
36
+ .btn-save { background: #2e7d32; color: #fff; font-size: 15px; min-height: 42px; padding: 10px 24px; margin-left: 40px; }
37
+ @media (max-width: 600px) {
38
+ .btn-save { margin-left: auto; padding: 10px 16px; white-space: nowrap; }
39
+ .pr-actions .btn { white-space: nowrap; }
40
+ .pr-status { flex-basis: 100%; }
41
+ }
42
+ .btn-save:hover:not(:disabled) { background: #388e3c; }
43
+ .pr-console { background: #111; color: #ddd; font-family: monospace; font-size: 12px; padding: 12px; border-radius: 6px; white-space: pre-wrap; max-height: 320px; overflow: auto; margin-bottom: 16px; display: none; scroll-margin-bottom: 90px; }
33
44
  .pr-console .err { color: var(--error); font-weight: 600; }
34
45
  .pr-console .ok { color: var(--success); }
35
- .pr-error { color: var(--error); font-size: 13px; margin: 8px 0; font-weight: 500; }
46
+ .pr-error { color: var(--error); font-size: 13px; margin: 8px 0; font-weight: 500; scroll-margin-bottom: 90px; }
36
47
  .pr-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 10; padding: 20px; }
37
48
  .pr-modal.open { display: flex; }
38
49
  .pr-modal-box { background: var(--card); border-radius: 8px; padding: 20px; max-width: 480px; width: 100%; }
39
50
  .pr-modal-box h3 { color: #fff; margin-bottom: 12px; font-size: 16px; }
40
51
  .pr-modal-box p { white-space: pre-wrap; margin-bottom: 20px; font-size: 14px; line-height: 1.5; }
41
52
  .pr-modal-box .pr-buttons { display: flex; gap: 10px; justify-content: flex-end; }
53
+ .pr-discard .pr-modal-box { border: 3px solid #d32f2f; }
54
+ .pr-discard h3 { color: #ff5252; font-size: 24px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; }
55
+ .pr-discard p { color: #ff8a80; font-size: 18px; font-weight: 700; }
56
+ .pr-discard .btn { font-size: 15px; min-height: 42px; padding: 10px 18px; }
57
+ .btn-keep { background: #2e7d32; color: #fff; }
58
+ .btn-keep:hover { background: #388e3c; }
59
+ .btn-discard { background: #c62828; color: #fff; }
60
+ .btn-discard:hover:not(:disabled) { background: #d32f2f; }
42
61
  .pr-loading { color: var(--muted); padding: 24px; text-align: center; }
43
62
  .pr-hidden { display: none !important; }
44
63
  .nt { font-size: 14px; }
@@ -106,7 +125,7 @@
106
125
  </head>
107
126
  <body>
108
127
  <div class="container">
109
- <h1><span>Push notifications</span><small id="pr-version"></small></h1>
128
+ <h1><span>Push notifications</span><small><a href="https://github.com/noon92/pushraker/blob/main/docs/guide.md" target="_blank">Guide</a><span id="pr-version"></span></small></h1>
110
129
  <div class="card">
111
130
  <div class="pr-loading" id="pr-loading">Loading settings…</div>
112
131
  <div class="nt pr-hidden" id="nt-root">
@@ -254,7 +273,7 @@
254
273
  </div>
255
274
  </div>
256
275
 
257
- <div class="nt-section">Email</div>
276
+ <div class="nt-section">Email<a class="nt-guide" href="https://github.com/noon92/pushraker/blob/main/docs/guide.md#email" target="_blank">Guide</a></div>
258
277
  <label class="nt-enable nt-sec"><input type="checkbox" id="nt-email-enabled"> Send email notifications</label>
259
278
  <div class="nt-grid" id="nt-email">
260
279
  <div class="nt-field">
@@ -300,7 +319,7 @@
300
319
  </div>
301
320
  </div>
302
321
 
303
- <div class="nt-section">Webhook</div>
322
+ <div class="nt-section">Webhook<a class="nt-guide" href="https://github.com/noon92/pushraker/blob/main/docs/guide.md#webhook" target="_blank">Guide</a></div>
304
323
  <label class="nt-enable nt-sec"><input type="checkbox" id="nt-wh-enabled"> Send every notification to a webhook</label>
305
324
  <div class="nt-grid" id="nt-webhook">
306
325
  <div class="nt-field">
@@ -384,11 +403,23 @@
384
403
  <div class="pr-console" id="pr-console"></div>
385
404
  <div class="pr-error" id="pr-error" style="display:none"></div>
386
405
  <div class="pr-actions">
387
- <button type="button" class="btn btn-primary" id="pr-save">Save &amp; Apply</button>
406
+ <button type="button" class="btn btn-secondary" id="pr-close">Cancel</button>
388
407
  <button type="button" class="btn btn-secondary" id="pr-test">Send Test</button>
408
+ <button type="button" class="btn btn-save" id="pr-save">Save &amp; Apply</button>
389
409
  <span class="pr-status" id="pr-status"></span>
390
410
  </div>
391
411
 
412
+ <div class="pr-modal pr-discard" id="pr-discard" role="alertdialog" aria-labelledby="pr-discard-title">
413
+ <div class="pr-modal-box">
414
+ <h3 id="pr-discard-title">⚠ Warning: your changes will be lost</h3>
415
+ <p>You are leaving without saving. The changes you made on this page will NOT be kept.</p>
416
+ <div class="pr-buttons">
417
+ <button type="button" class="btn btn-discard" id="pr-discard-yes" disabled>Discard changes</button>
418
+ <button type="button" class="btn btn-keep" id="pr-discard-no">Keep editing</button>
419
+ </div>
420
+ </div>
421
+ </div>
422
+
392
423
  <div class="pr-modal" id="pr-modal">
393
424
  <div class="pr-modal-box">
394
425
  <h3 id="pr-modal-title"></h3>
@@ -557,6 +588,14 @@
557
588
  el('nt-table').classList.toggle('nt-no-email', !email);
558
589
  }
559
590
 
591
+ // The user guide, and the section of it for each push service
592
+ const GUIDE = 'https://github.com/noon92/pushraker/blob/main/docs/guide.md';
593
+ const guideLink = (anchor, text) =>
594
+ ` <a href="${GUIDE}#${anchor}" target="_blank">${text || 'Setup guide'}</a>`;
595
+ const PROVIDER_GUIDE = {
596
+ ntfy: ['ntfy-server', 'Hosting your own ntfy'], gotify: ['gotify'],
597
+ telegram: ['telegram'], discord_dm: ['discord'], discord: ['discord']
598
+ };
560
599
  const PROVIDER_HINTS = {
561
600
  ntfy: '<a href="https://ntfy.sh" target="_blank">ntfy</a>: free and open source; use the public server or host your own.',
562
601
  gotify: '<a href="https://gotify.net" target="_blank">Gotify</a>: free and open source, on your own server.<div class="nt-public">Text only: picture attachments not supported by Gotify.</div>',
@@ -672,7 +711,8 @@
672
711
  function updateProvider() {
673
712
  const provider = el('nt-provider').value;
674
713
  el('nt-ntfy').style.display = provider === 'ntfy' ? '' : 'none';
675
- el('nt-provider-hint').innerHTML = PROVIDER_HINTS[provider] || '';
714
+ const g = PROVIDER_GUIDE[provider];
715
+ el('nt-provider-hint').innerHTML = (PROVIDER_HINTS[provider] || '') + (g ? guideLink(g[0], g[1]) : '');
676
716
  el('nt-pushover').style.display = provider === 'pushover' ? '' : 'none';
677
717
  el('nt-gotify').style.display = provider === 'gotify' ? '' : 'none';
678
718
  el('nt-gt-token').placeholder = state.has_secret && state.provider === 'gotify' ? 'Saved (leave empty to keep)' : '';
@@ -950,6 +990,7 @@
950
990
  function showError(msg) {
951
991
  el('pr-error').textContent = msg || '';
952
992
  el('pr-error').style.display = msg ? '' : 'none';
993
+ if (msg) el('pr-error').scrollIntoView({ block: 'nearest', behavior: 'smooth' });
953
994
  }
954
995
 
955
996
  function showConsole(lines, ok) {
@@ -958,8 +999,11 @@
958
999
  const c = l.startsWith('!! ') ? 'err' : (/^(Sent to|Test notification sent|Settings saved)/.test(l) ? 'ok' : '');
959
1000
  return `<div class="${c}">${esc(l.replace(/^!! /, ''))}</div>`;
960
1001
  }).join('');
961
- box.style.display = lines.length ? '' : 'none';
1002
+ box.style.display = lines.length ? 'block' : 'none';
962
1003
  box.scrollTop = box.scrollHeight;
1004
+ // The console is at the end of the page: bring it into view, or
1005
+ // on a long page it is never seen
1006
+ if (lines.length) box.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
963
1007
  }
964
1008
 
965
1009
  function busy(on, what) {
@@ -977,8 +1021,8 @@
977
1021
  const data = await api('/server/pushraker/config', 'POST', payload);
978
1022
  const notes = (data.notes || []).slice();
979
1023
  render(data);
980
- showConsole(['Settings saved and applied.'].concat(notes), true);
981
- el('pr-status').textContent = data.meta.summary || 'Saved';
1024
+ showConsole(['Settings saved and applied. Nothing restarts: they are in effect now, during a print job too.'].concat(notes), true);
1025
+ el('pr-status').textContent = 'Saved and applied';
982
1026
  } catch (e) {
983
1027
  showError(e.message);
984
1028
  el('pr-status').textContent = '';
@@ -996,10 +1040,11 @@
996
1040
  try {
997
1041
  const data = await api('/server/pushraker/test', 'POST', payload);
998
1042
  showConsole(data.lines || [], data.ok);
999
- el('pr-status').textContent = data.ok ? 'Test sent' : 'Test failed';
1043
+ if (data.ok) el('pr-status').textContent = 'Test sent';
1044
+ else el('pr-status').innerHTML = 'Test failed.' + guideLink('troubleshooting', 'Troubleshooting');
1000
1045
  } catch (e) {
1001
1046
  showConsole(['!! ' + e.message], false);
1002
- el('pr-status').textContent = 'Test failed';
1047
+ el('pr-status').innerHTML = 'Test failed.' + guideLink('troubleshooting', 'Troubleshooting');
1003
1048
  } finally {
1004
1049
  busy(false);
1005
1050
  }
@@ -1022,14 +1067,58 @@
1022
1067
  };
1023
1068
  return;
1024
1069
  }
1025
- el('pr-loading').textContent = `Could not load the settings from Moonraker: ${e.message}. Is the push_notifications component enabled?`;
1070
+ el('pr-loading').textContent = `Could not load the settings from Moonraker: ${e.message}. Is the pushraker component enabled ([pushraker] in moonraker.conf)?`;
1026
1071
  }
1027
1072
  }
1028
1073
 
1029
1074
  el('pr-save').onclick = save;
1030
1075
  el('pr-test').onclick = test;
1076
+ // Close without saving: embedded (an iframe in a host page's modal),
1077
+ // tell the host, which closes it; on its own, go back to the page it
1078
+ // was opened from, or close the tab, or go to the printer's start page
1079
+ let closing = false;
1080
+ const dirty = () => !!state && JSON.stringify(collect(false)) !== saved;
1081
+ // With unsaved changes, a warning first: the discard button only
1082
+ // works after 5 seconds, so a misclick can't throw the changes away
1083
+ function confirmDiscard() {
1084
+ return new Promise(resolve => {
1085
+ const box = el('pr-discard'), yes = el('pr-discard-yes'), no = el('pr-discard-no');
1086
+ let left = 5, timer = null;
1087
+ const tick = () => {
1088
+ yes.disabled = left > 0;
1089
+ yes.textContent = left > 0 ? `Discard changes (${left})` : 'Discard changes';
1090
+ if (left-- > 0) timer = setTimeout(tick, 1000);
1091
+ };
1092
+ const done = v => {
1093
+ clearTimeout(timer);
1094
+ document.removeEventListener('keydown', esc);
1095
+ box.classList.remove('open');
1096
+ resolve(v);
1097
+ };
1098
+ const esc = e => { if (e.key === 'Escape') done(false); };
1099
+ yes.onclick = () => { if (!yes.disabled) done(true); };
1100
+ no.onclick = () => done(false);
1101
+ box.onclick = e => { if (e.target === box) done(false); };
1102
+ document.addEventListener('keydown', esc);
1103
+ box.classList.add('open');
1104
+ no.focus();
1105
+ tick();
1106
+ });
1107
+ }
1108
+ el('pr-close').onclick = async () => {
1109
+ if (dirty() && !(await confirmDiscard())) return;
1110
+ closing = true;
1111
+ if (window.parent !== window) {
1112
+ window.parent.postMessage({ type: 'pushraker:close' }, '*');
1113
+ return;
1114
+ }
1115
+ if (history.length > 1) { history.back(); return; }
1116
+ window.close();
1117
+ setTimeout(() => { location.href = '/'; }, 300);
1118
+ };
1031
1119
  window.addEventListener('beforeunload', e => {
1032
- if (state && JSON.stringify(collect(false)) !== saved) { e.preventDefault(); e.returnValue = ''; }
1120
+ if (closing) return;
1121
+ if (dirty()) { e.preventDefault(); e.returnValue = ''; }
1033
1122
  });
1034
1123
  load();
1035
1124
  })();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pushraker
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Push notifications for Klipper printers: a Moonraker component with pictures, sent through Apprise (ntfy, Pushover, Telegram, Discord, webhooks, email)
5
5
  Author: noon92
6
6
  License-Expression: GPL-3.0-or-later
@@ -30,8 +30,8 @@ own web page, live, with no Moonraker or Klipper restart.
30
30
 
31
31
  It grew out of the Push Notifications feature of the
32
32
  [Snapmaker U1 Extended Firmware](https://github.com/paxx12-snapmaker-u1/SnapmakerU1-Extended-Firmware),
33
- where it is built in; the user documentation lives there
34
- for now: [notifications](https://snapmakeru1-extended-firmware.pages.dev/notifications).
33
+ where it is built in. User documentation: the
34
+ [Push Notifications guide](docs/guide.md).
35
35
 
36
36
  **Status: 0.2.** The component has been in
37
37
  use on the Snapmaker U1 for a while; the page, the settings file and the
@@ -107,6 +107,7 @@ it out again; `--purge` also removes the settings and secrets.
107
107
  - `src/pushraker/web/index.html` — the settings page
108
108
  - `src/pushraker/klipper/pushraker.cfg` — the Klipper macro file
109
109
  - `src/pushraker/__main__.py` — `python -m pushraker install|uninstall|status`
110
+ - `docs/guide.md` — the user guide
110
111
  - `tests/` — `python tests/test_pushraker.py` (the component against a
111
112
  stand-in Moonraker; also runs `test_settings.py`)
112
113
 
@@ -230,7 +230,7 @@ class Config:
230
230
  return self.server
231
231
 
232
232
  def get_name(self):
233
- return "push_notifications"
233
+ return "pushraker"
234
234
 
235
235
  def get(self, key, default=None):
236
236
  return self.values.get(key, default)
@@ -305,9 +305,10 @@ def make(values, services):
305
305
  email)]}."""
306
306
  server = Server()
307
307
  os.makedirs(os.path.join(WORK, "data"), exist_ok=True)
308
- state = os.path.join(WORK, "data", ".push_notifications.json")
309
- if os.path.exists(state):
310
- os.remove(state)
308
+ for state in (".pushraker.json", ".push_notifications.json"):
309
+ state = os.path.join(WORK, "data", state)
310
+ if os.path.exists(state):
311
+ os.remove(state)
311
312
  c = pn.PushNotifications(Config(server, dict(
312
313
  dict(name="U1", settle_time="0.1", settings="options"), **values)))
313
314
  c.rows = {rid: ("snapshot" if not rid.startswith(("filament", "spool", "slot"))
@@ -777,7 +778,7 @@ async def test_faults_over_a_job():
777
778
  STATE["print_stats"] = ps("printing", exception=ex)
778
779
  await events(JobEvent.RESUMED, {}, STATE["print_stats"])
779
780
  # ...nor a cancel from the print hook
780
- await server.remote["push_notification"](event="cancelled")
781
+ await server.remote["pushraker"](event="cancelled")
781
782
  await asyncio.sleep(0.6)
782
783
  titles = [e["title"].split(" ", 1)[1] for e in log]
783
784
  check("a resumed fault doesn't colour a later pause or cancel",
@@ -808,7 +809,7 @@ async def test_faults_over_a_job():
808
809
  STATE["print_stats"] = ps("paused", exception=ex)
809
810
  c._faults_sent.clear()
810
811
  await events(JobEvent.PAUSED, {}, STATE["print_stats"])
811
- await server.remote["push_notification"](event="cancelled")
812
+ await server.remote["pushraker"](event="cancelled")
812
813
  await asyncio.sleep(0.6)
813
814
  titles = [e["title"].split(" ", 1)[1] for e in log]
814
815
  check("cancel after a reported fault-pause: no second error",
@@ -2242,6 +2243,23 @@ async def test_config_lines():
2242
2243
  SECRETS.clear()
2243
2244
 
2244
2245
 
2246
+ async def test_old_names():
2247
+ """0.1's names keep working: the remote method, the low-spool memory."""
2248
+ data = os.path.join(WORK, "data")
2249
+ server, c = make({}, {})
2250
+ check("remote method answered under both names",
2251
+ "pushraker" in server.remote and "push_notification" in server.remote,
2252
+ sorted(server.remote))
2253
+ old = os.path.join(data, ".push_notifications.json")
2254
+ with open(old, "w") as f:
2255
+ json.dump({"low_sent": {"7": 123.0}}, f)
2256
+ c._low_sent = None
2257
+ got = c._low_load()
2258
+ check("0.1's low-spool memory is moved to .pushraker.json",
2259
+ got == {"7": 123.0} and not os.path.exists(old)
2260
+ and os.path.exists(os.path.join(data, ".pushraker.json")), got)
2261
+
2262
+
2245
2263
  async def run_component_tests():
2246
2264
  for t in (test_order_and_priority, test_expiry_and_independence,
2247
2265
  test_fault_dedup, test_same_preparation_everywhere,
@@ -2249,7 +2267,7 @@ async def run_component_tests():
2249
2267
  test_faults_over_a_job, test_spool_switch, test_ace, test_file_pauses, test_file_mode, test_generic_printer, test_status_report, test_time_left, test_calibration, test_gotify, test_image_names, test_fault_emoji, test_send_gap, test_macro_notify, test_queue_line, test_chamber, test_filament_change, test_backup_order, test_console,
2250
2268
  test_driver_hot,
2251
2269
  test_print_by_object,
2252
- test_no_secrets_in_logs, test_config_lines):
2270
+ test_no_secrets_in_logs, test_config_lines, test_old_names):
2253
2271
  try:
2254
2272
  await t()
2255
2273
  except Exception as e: # report and carry on
File without changes
File without changes