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.
- {pushraker-0.2.0/src/pushraker.egg-info → pushraker-0.2.2}/PKG-INFO +4 -3
- {pushraker-0.2.0 → pushraker-0.2.2}/README.md +3 -2
- {pushraker-0.2.0 → pushraker-0.2.2}/pyproject.toml +1 -1
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/__init__.py +1 -1
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/component.py +18 -7
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/discord.py +2 -2
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/email_html.py +1 -1
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/images.py +1 -1
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/klipper/pushraker.cfg +2 -2
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/web/index.html +104 -15
- {pushraker-0.2.0 → pushraker-0.2.2/src/pushraker.egg-info}/PKG-INFO +4 -3
- {pushraker-0.2.0 → pushraker-0.2.2}/tests/test_pushraker.py +25 -7
- {pushraker-0.2.0 → pushraker-0.2.2}/LICENSE +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/setup.cfg +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/__main__.py +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/printers/__init__.py +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/printers/snapmaker_u1.py +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker/settings.py +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/SOURCES.txt +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/dependency_links.txt +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/requires.txt +0 -0
- {pushraker-0.2.0 → pushraker-0.2.2}/src/pushraker.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
34
|
-
|
|
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
|
|
14
|
-
|
|
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.
|
|
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"
|
|
@@ -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 `
|
|
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 = "
|
|
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, ".
|
|
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="
|
|
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="
|
|
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 (
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
55
|
+
description: Report a print hook event to the pushraker component
|
|
56
56
|
gcode:
|
|
57
|
-
{action_call_remote_method("
|
|
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
|
|
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
|
-
|
|
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-
|
|
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 & 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
|
-
|
|
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 =
|
|
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 =
|
|
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').
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
34
|
-
|
|
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 "
|
|
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
|
|
309
|
-
|
|
310
|
-
os.
|
|
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["
|
|
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["
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|