boneIO 1.6.0.dev3__py3-none-any.whl → 1.6.0.dev5__py3-none-any.whl

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 (90) hide show
  1. boneio/core/cloud/__init__.py +2 -2
  2. boneio/core/cloud/data/docker-compose-cloud.yaml +4 -0
  3. boneio/core/cloud/data/docker-compose.yaml +4 -0
  4. boneio/core/cloud/data/init-certs-cloud.sh +6 -28
  5. boneio/core/cloud/registration.py +78 -3
  6. boneio/core/config/provenance.py +114 -0
  7. boneio/core/config/yaml_patch.py +121 -0
  8. boneio/core/containers.py +6 -1
  9. boneio/core/security/certificate.py +297 -0
  10. boneio/core/security/posture.py +89 -0
  11. boneio/migrations/assets/MANIFEST.sha256 +3 -2
  12. boneio/migrations/assets/docker/nodered/caddy/init-certs.sh +31 -32
  13. boneio/migrations/assets/helpers/boneio-containers +4 -1
  14. boneio/migrations/assets/issue.d/10-boneio.issue +1 -0
  15. boneio/migrations/plans/1.3.0.json +1 -1
  16. boneio/migrations/plans/1.3.0.sig +0 -0
  17. boneio/migrations/plans/1.4.0.json +1 -1
  18. boneio/migrations/plans/1.4.0.sig +2 -1
  19. boneio/migrations/plans/1.4.4.json +1 -1
  20. boneio/migrations/plans/1.4.4.sig +1 -1
  21. boneio/migrations/plans/1.6.12.json +1 -0
  22. boneio/migrations/plans/1.6.12.sig +1 -0
  23. boneio/migrations/plans/1.6.13.json +1 -0
  24. boneio/migrations/plans/1.6.13.sig +1 -0
  25. boneio/migrations/plans/1.6.4.json +1 -1
  26. boneio/migrations/plans/1.6.4.sig +0 -0
  27. boneio/migrations/plans/1.6.5.json +1 -1
  28. boneio/migrations/plans/1.6.5.sig +2 -2
  29. boneio/migrations/plans/manifest.json +1 -1
  30. boneio/migrations/plans/manifest.sig +1 -1
  31. boneio/migrations/versions/v1_3_0_baseline.py +3 -0
  32. boneio/migrations/versions/v1_4_0_fix_mqtt_sudoers.py +3 -0
  33. boneio/migrations/versions/v1_6_12_console_shows_ip.py +58 -0
  34. boneio/migrations/versions/v1_6_13_custom_certificate.py +52 -0
  35. boneio/migrations/versions/v1_6_4_ssh_login_hardening.py +3 -0
  36. boneio/runner.py +7 -0
  37. boneio/schema/schema.yaml +11 -0
  38. boneio/version.py +1 -1
  39. boneio/webui/app.py +23 -8
  40. boneio/webui/bind.py +193 -0
  41. boneio/webui/frontend-dist/assets/{AccountsView-iveRbldx.js → AccountsView-Bmey_Yr2.js} +1 -1
  42. boneio/webui/frontend-dist/assets/{ActionFields-DdCxR8lP.js → ActionFields-Dd53m3KH.js} +1 -1
  43. boneio/webui/frontend-dist/assets/{BackupSection-C1qj4qOG.js → BackupSection-BIhcVjYm.js} +1 -1
  44. boneio/webui/frontend-dist/assets/{BindingMatrix-Bj6KE0yY.js → BindingMatrix-D9Pr22Wp.js} +1 -1
  45. boneio/webui/frontend-dist/assets/{DeviceControlSection-CPmqccka.js → DeviceControlSection-DKC8tOXc.js} +1 -1
  46. boneio/webui/frontend-dist/assets/DiagnosticsView-C_oo-SRw.js +5 -0
  47. boneio/webui/frontend-dist/assets/{EditItemDialog-BH1onHq0.js → EditItemDialog-Dm4kPdmF.js} +1 -1
  48. boneio/webui/frontend-dist/assets/{EntityCard-yD9_Hl71.js → EntityCard-CkJgrIUC.js} +1 -1
  49. boneio/webui/frontend-dist/assets/{FactoryResetSection-E3xtkFLr.js → FactoryResetSection-sz3F--K9.js} +1 -1
  50. boneio/webui/frontend-dist/assets/{FormInputToggle-Spiz-w7l.js → FormInputToggle-C7Nq2Hp8.js} +1 -1
  51. boneio/webui/frontend-dist/assets/{HaDashboardWizard-f8fMg8FC.js → HaDashboardWizard-DEf4Sy_8.js} +1 -1
  52. boneio/webui/frontend-dist/assets/{HostnameSection-DrpKbp6I.js → HostnameSection-D0zvj1uy.js} +1 -1
  53. boneio/webui/frontend-dist/assets/{InputsView-W99g8hSl.js → InputsView-DkpdOPNR.js} +1 -1
  54. boneio/webui/frontend-dist/assets/{LocationSection-ZILUsfeR.js → LocationSection-D5EoVv-i.js} +2 -2
  55. boneio/webui/frontend-dist/assets/{LoggerForm-Chw9zm0N.js → LoggerForm-DHJUPaaV.js} +1 -1
  56. boneio/webui/frontend-dist/assets/{MigrationsSection-DSbw5UVz.js → MigrationsSection-BPvuefg6.js} +1 -1
  57. boneio/webui/frontend-dist/assets/{ModbusView-CdP0fXYx.js → ModbusView-Cch7ykif.js} +1 -1
  58. boneio/webui/frontend-dist/assets/{MqttPasswordsSection-BYwPAyFi.js → MqttPasswordsSection-dncPiufr.js} +1 -1
  59. boneio/webui/frontend-dist/assets/{NodeRedManagement-DRMiFlH2.js → NodeRedManagement-Cowu2_4i.js} +1 -1
  60. boneio/webui/frontend-dist/assets/{OutputsView-C9sDLF1P.js → OutputsView-C_PNR9px.js} +1 -1
  61. boneio/webui/frontend-dist/assets/{ScheduleSection-C2qLKNWR.js → ScheduleSection-ByEXYj3H.js} +1 -1
  62. boneio/webui/frontend-dist/assets/SecurityView-BGcKCB34.js +1 -0
  63. boneio/webui/frontend-dist/assets/{SensorView-DNtKJE4b.js → SensorView-DxwEx546.js} +1 -1
  64. boneio/webui/frontend-dist/assets/{SystemState-DbY5T6BB.js → SystemState-CvlqTKsV.js} +1 -1
  65. boneio/webui/frontend-dist/assets/{TemplatesView-6kNzVE3p.js → TemplatesView-D0PTisZV.js} +1 -1
  66. boneio/webui/frontend-dist/assets/{TilePicker-jd3hKaoZ.js → TilePicker-I46eKfzF.js} +1 -1
  67. boneio/webui/frontend-dist/assets/{TimezoneSection-BEuXBmWp.js → TimezoneSection-DRA9cBch.js} +1 -1
  68. boneio/webui/frontend-dist/assets/{UISettings-DIcCTtXU.js → UISettings-qOAoRlwa.js} +3 -3
  69. boneio/webui/frontend-dist/assets/{index-1pLHiRke.js → index-BDR1Gl2A.js} +24 -4
  70. boneio/webui/frontend-dist/assets/{monaco-C9XC2D1j.js → monaco-CQ8fwGzT.js} +1 -1
  71. boneio/webui/frontend-dist/index.html +1 -1
  72. boneio/webui/frontend-dist/schema/config.schema.json +13 -0
  73. boneio/webui/frontend-dist/schema/web.schema.json +13 -0
  74. boneio/webui/frontend-dist/sw.js +1 -1
  75. boneio/webui/routes/config_core.py +131 -1
  76. boneio/webui/routes/onboarding.py +9 -0
  77. boneio/webui/routes/security.py +297 -2
  78. boneio/webui/routes/system.py +8 -0
  79. boneio/webui/schema/config.schema.json +13 -0
  80. boneio/webui/schema/web.schema.json +13 -0
  81. boneio/webui/web_server.py +5 -1
  82. boneio/webui/websocket_manager.py +42 -2
  83. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/METADATA +3 -3
  84. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/RECORD +88 -78
  85. boneio/webui/frontend-dist/assets/DiagnosticsView-Bi1hrIcs.js +0 -5
  86. boneio/webui/frontend-dist/assets/SecurityView-DBkwBU2t.js +0 -1
  87. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/WHEEL +0 -0
  88. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/entry_points.txt +0 -0
  89. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/licenses/LICENSE +0 -0
  90. {boneio-1.6.0.dev3.dist-info → boneio-1.6.0.dev5.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  """Cloud registration module for boneIO Black PWA support."""
2
2
 
3
- from boneio.core.cloud.registration import CloudRegistration
3
+ from boneio.core.cloud.registration import CloudRegistration, set_enabled
4
4
 
5
- __all__ = ["CloudRegistration"]
5
+ __all__ = ["CloudRegistration", "set_enabled"]
@@ -20,6 +20,10 @@ services:
20
20
  - "${HTTPS_PORT:-8443}:443"
21
21
  environment:
22
22
  - HOST_HOSTNAME=${HOSTNAME}
23
+ # The port the host publishes 443 on. Caddy only sees 443 from
24
+ # inside the container, so a redirect built from what it sees
25
+ # would send browsers to a port nothing listens on.
26
+ - PUBLIC_HTTPS_PORT=${HTTPS_PORT:-8443}
23
27
  volumes:
24
28
  - ./caddy/502.html:/srv/502.html:ro
25
29
  - ./caddy/init-certs-cloud.sh:/init-certs-cloud.sh:ro
@@ -25,6 +25,10 @@ services:
25
25
  - "${HTTPS_PORT:-8443}:443"
26
26
  environment:
27
27
  - HOST_HOSTNAME=${HOSTNAME:-boneio}
28
+ # The port the host publishes 443 on. Caddy only sees 443 from
29
+ # inside the container, so a redirect built from what it sees
30
+ # would send browsers to a port nothing listens on.
31
+ - PUBLIC_HTTPS_PORT=${HTTPS_PORT:-8443}
28
32
  volumes:
29
33
  - ./caddy/502.html:/srv/502.html:ro
30
34
  - ./caddy/init-certs.sh:/init-certs.sh:ro
@@ -79,34 +79,12 @@ cat > /tmp/Caddyfile << EOF
79
79
 
80
80
  # HTTP - serve directly
81
81
  :80 {
82
- handle_errors {
83
- @502-504 expression {err.status_code} >= 502 && {err.status_code} <= 504
84
- handle @502-504 {
85
- root * /srv
86
- rewrite * /502.html
87
- file_server
88
- }
89
- }
90
-
91
- handle /nodered-status {
92
- header Content-Type application/json
93
- header X-NodeRed-Available "true"
94
- header Access-Control-Expose-Headers "X-NodeRed-Available"
95
- respond \`{"available": true}\` 200
96
- }
97
-
98
- handle /nodered/* {
99
- reverse_proxy node-red:1880 {
100
- header_up X-Forwarded-Proto {scheme}
101
- header_down X-NodeRed-Available "true"
102
- }
103
- }
104
-
105
- handle {
106
- reverse_proxy host.docker.internal:8090 {
107
- header_up X-Forwarded-Proto {scheme}
108
- }
109
- }
82
+ # Redirect to HTTPS. This port used to serve the panel in the clear —
83
+ # the login form, the token it hands back, and a configuration carrying
84
+ # passwords. The port is substituted by this shell as the file is
85
+ # written: Caddy only sees 443 from inside the container, while the
86
+ # host publishes it as 8443.
87
+ redir https://{host}:${PUBLIC_HTTPS_PORT:-8443}{uri}
110
88
  }
111
89
 
112
90
  ${CLOUD_BLOCK}
@@ -41,9 +41,11 @@ KEY_FILE = CERT_DIR / "privkey.pem"
41
41
 
42
42
  # Caddy configuration paths
43
43
  CADDY_CONFIG_DIR = _DOCKER_DIR / "caddy"
44
- CADDY_DEFAULT_CONFIG = CADDY_CONFIG_DIR / "Caddyfile"
45
- CADDY_CLOUD_CONFIG = CADDY_CONFIG_DIR / "Caddyfile.cloud"
46
- CADDY_ACTIVE_CONFIG = CADDY_CONFIG_DIR / "Caddyfile"
44
+ # No Caddyfile constants here on purpose. Caddy's configuration is not a file
45
+ # in this directory — init-certs.sh (or init-certs-cloud.sh) writes it to
46
+ # /tmp/Caddyfile inside the container on every start. Switching between local
47
+ # and cloud mode swaps the compose file, and with it which of those two scripts
48
+ # runs; there is no Caddyfile to copy over another.
47
49
 
48
50
  # Registration interval (1 hour)
49
51
  REGISTRATION_INTERVAL = 3600
@@ -637,3 +639,76 @@ class CloudRegistration:
637
639
  return "init-certs-cloud.sh" in content
638
640
  except Exception:
639
641
  return False
642
+
643
+
644
+ async def set_enabled(config_helper, enabled: bool, local_ip: str | None = None) -> str:
645
+ """Turn cloud registration on or off on a running device.
646
+
647
+ Registration used to begin only in :mod:`boneio.runner`, so switching it on
648
+ from the panel wrote a line to config.yaml and asked for a restart —
649
+ on a controller whose whole point is that it is running. Everything the
650
+ change needs is already here: :meth:`CloudRegistration.start` does no more
651
+ than launch the loop that registers the name, fetches the certificate,
652
+ swaps the compose template and recreates Caddy.
653
+
654
+ Idempotent in both directions, because the panel can be open twice and a
655
+ save can be repeated.
656
+
657
+ Args:
658
+ config_helper: The live :class:`ConfigHelper`.
659
+ enabled: The state asked for.
660
+ local_ip: This device's address on the LAN. Read from the system when
661
+ not given.
662
+
663
+ Returns:
664
+ A short word for what happened: ``started``, ``stopped``, ``unchanged``
665
+ or ``unavailable``.
666
+ """
667
+ existing = getattr(config_helper, "_cloud_reg", None)
668
+
669
+ if not enabled:
670
+ config_helper._cloud_registration = False
671
+ if existing is None:
672
+ return "unchanged"
673
+ try:
674
+ await existing.stop()
675
+ # Put the plain template back, or Caddy keeps trying to serve a
676
+ # certificate for a name that is no longer being renewed.
677
+ await existing._restore_local_config()
678
+ except Exception as err: # noqa: BLE001
679
+ _LOGGER.warning("Could not stop cloud registration cleanly: %s", err)
680
+ config_helper._cloud_reg = None
681
+ _LOGGER.info("Cloud registration stopped on request")
682
+ return "stopped"
683
+
684
+ if existing is not None:
685
+ config_helper._cloud_registration = True
686
+ return "unchanged"
687
+
688
+ serial = getattr(config_helper, "serial_number", None)
689
+ if not local_ip:
690
+ try:
691
+ from boneio.core.system.monitor import get_network_info
692
+
693
+ local_ip = (get_network_info() or {}).get("ip", "")
694
+ except Exception as err: # noqa: BLE001
695
+ _LOGGER.warning("Could not read this device's address: %s", err)
696
+ local_ip = ""
697
+
698
+ if not serial or not local_ip:
699
+ # The setting is saved either way; the next start picks it up. Saying
700
+ # so is better than reporting success for a service that did not begin.
701
+ _LOGGER.warning(
702
+ "Cloud registration is enabled but %s is not known yet; it will "
703
+ "start on the next boot.",
704
+ "the serial number" if not serial else "this device's address",
705
+ )
706
+ config_helper._cloud_registration = True
707
+ return "unavailable"
708
+
709
+ registration = CloudRegistration(serial_number=serial, local_ip=local_ip)
710
+ await registration.start()
711
+ config_helper._cloud_reg = registration
712
+ config_helper._cloud_registration = True
713
+ _LOGGER.info("Cloud registration started for %s (IP: %s)", serial, local_ip)
714
+ return "started"
@@ -0,0 +1,114 @@
1
+ """Tell a freshly flashed controller from one that has been in service.
2
+
3
+ The first-run wizard has two steps that are only appropriate on a blank
4
+ device. The import step offers to restore a configuration, which on a device
5
+ that already has one reads as "yours is gone". The devices step is worse than
6
+ misleading: it replaces the whole ``event`` section, so a controller with fifty
7
+ configured inputs loses them to a wizard its owner opened only to create an
8
+ account.
9
+
10
+ So the question has to be answered before those steps are offered, and the two
11
+ ways of getting it wrong are not equally bad:
12
+
13
+ * Treating an upgraded device as fresh costs the owner their configuration.
14
+ * Treating a fresh device as upgraded costs them a step in a wizard; they
15
+ can still import and bind inputs from the normal interface afterwards.
16
+
17
+ This module is therefore written to answer "is it fresh" and to say no when it
18
+ cannot tell. Freshness has to be demonstrated, not assumed.
19
+
20
+ The demonstration is that the configuration is still the factory one. The image
21
+ copies a variant out of ``~/.cache/boneio_configs/<variant>/`` verbatim, and
22
+ per-device work touches only ``mqtt.yaml`` (the broker password), so a
23
+ controller nobody has configured yet still has a ``config.yaml`` byte-identical
24
+ to its template.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import os
31
+ from pathlib import Path
32
+
33
+ _LOGGER = logging.getLogger(__name__)
34
+
35
+ #: Where the image leaves the per-variant templates it seeded the device from.
36
+ FACTORY_TEMPLATE_DIR = Path.home() / ".cache" / "boneio_configs"
37
+
38
+
39
+ def _digest(path: Path) -> bytes | None:
40
+ """Read a file for comparison.
41
+
42
+ Args:
43
+ path: File to read.
44
+
45
+ Returns:
46
+ Its contents, or None when it cannot be read.
47
+ """
48
+ try:
49
+ return path.read_bytes()
50
+ except OSError:
51
+ return None
52
+
53
+
54
+ def looks_factory_fresh(
55
+ config_path: str | os.PathLike[str],
56
+ template_dir: str | os.PathLike[str] | None = None,
57
+ ) -> bool:
58
+ """Whether this device still carries an untouched factory configuration.
59
+
60
+ Args:
61
+ config_path: The device's ``config.yaml``.
62
+ template_dir: Where the per-variant templates live. Defaults to
63
+ :data:`FACTORY_TEMPLATE_DIR`.
64
+
65
+ Returns:
66
+ True only when freshness can be positively shown: there is no
67
+ configuration at all, or the configuration is identical to one of the
68
+ templates the image ships. False whenever the answer is not certain.
69
+ """
70
+ config = Path(config_path)
71
+ current = _digest(config)
72
+ if current is None:
73
+ # Nothing to lose, and nothing that an import could overwrite.
74
+ _LOGGER.debug("No configuration at %s — treating the device as fresh", config)
75
+ return True
76
+
77
+ root = Path(template_dir) if template_dir is not None else FACTORY_TEMPLATE_DIR
78
+ try:
79
+ variants = sorted(p for p in root.iterdir() if p.is_dir())
80
+ except OSError:
81
+ # No templates to compare against: the honest answer is "cannot tell",
82
+ # and the safe reading of that is "assume it has been configured".
83
+ _LOGGER.debug("No factory templates under %s — assuming configured", root)
84
+ return False
85
+
86
+ for variant in variants:
87
+ if current == _digest(variant / "config.yaml"):
88
+ _LOGGER.debug("Configuration matches the %s template", variant.name)
89
+ return True
90
+
91
+ return False
92
+
93
+
94
+ def was_configured_before(
95
+ config_path: str | os.PathLike[str],
96
+ had_legacy_auth: bool = False,
97
+ template_dir: str | os.PathLike[str] | None = None,
98
+ ) -> bool:
99
+ """Whether the wizard should skip the steps that assume a blank device.
100
+
101
+ Args:
102
+ config_path: The device's ``config.yaml``.
103
+ had_legacy_auth: Whether a pre-1.6 ``web.auth`` block was found. It
104
+ proves the device is not fresh, but its absence proves nothing —
105
+ most 1.5.x controllers never had web authentication at all, which
106
+ is exactly the case this used to get wrong.
107
+ template_dir: Passed through to :func:`looks_factory_fresh`.
108
+
109
+ Returns:
110
+ True when the device already has a configuration of its own.
111
+ """
112
+ if had_legacy_auth:
113
+ return True
114
+ return not looks_factory_fresh(config_path, template_dir)
@@ -260,3 +260,124 @@ def ensure_section(config_file: str | Path, path: tuple[str, ...]) -> bool:
260
260
  file_path.write_text("".join(lines), encoding="utf-8")
261
261
  _LOGGER.info("Created section '%s' in %s", ".".join(path), file_path)
262
262
  return True
263
+
264
+
265
+ def _find_section_line(lines: list[str], path: tuple[str, ...]) -> int | None:
266
+ """Index of the line declaring a nested key, or None if it is not there.
267
+
268
+ Args:
269
+ lines: File lines.
270
+ path: Section path, outermost first.
271
+
272
+ Returns:
273
+ The index of the key's own line, or None.
274
+ """
275
+ depth = 0
276
+ for index, line in enumerate(lines):
277
+ parsed = _line_key(line)
278
+ if parsed is None:
279
+ continue
280
+ key, indent, _ = parsed
281
+ if depth and indent <= (depth - 1) * len(INDENT):
282
+ return None
283
+ if key == path[depth] and indent == depth * len(INDENT):
284
+ depth += 1
285
+ if depth == len(path):
286
+ return index
287
+ return None
288
+
289
+
290
+ def has_section(config_file: str | Path, path: tuple[str, ...]) -> bool:
291
+ """Whether a nested key is present in the file.
292
+
293
+ Separate from :func:`remove_section` so a caller can find out before doing
294
+ anything irreversible — such as writing a backup copy over the one it made
295
+ the last time it was asked.
296
+
297
+ Args:
298
+ config_file: Path to config.yaml.
299
+ path: Section path, outermost first.
300
+
301
+ Returns:
302
+ True when the key is there.
303
+
304
+ Raises:
305
+ YamlPatchError: If the path is empty.
306
+ """
307
+ if not path:
308
+ raise YamlPatchError("No section path given.")
309
+ lines = Path(config_file).read_text(encoding="utf-8").splitlines(keepends=True)
310
+ return _find_section_line(lines, path) is not None
311
+
312
+
313
+ def remove_section(config_file: str | Path, path: tuple[str, ...]) -> bool:
314
+ """Delete a nested mapping key and everything under it.
315
+
316
+ Written for one job: taking the pre-1.6 ``web.auth`` block out of a
317
+ configuration after its account has been migrated into the hashed store.
318
+ Nothing reads it any more, and it still holds the owner's password in
319
+ plain text — in the file, and in every backup and diagnostic bundle made
320
+ since.
321
+
322
+ Line-based rather than a parse-and-dump, for the usual reason: the file
323
+ belongs to the owner, and round-tripping it through a YAML library would
324
+ return their comments, ordering and ``!include`` directives as something
325
+ they did not write.
326
+
327
+ Comment lines immediately above the key are removed with it. They describe
328
+ the block, and leaving them behind stranded over an unrelated setting is
329
+ worse than taking one comment too many.
330
+
331
+ Args:
332
+ config_file: Path to config.yaml.
333
+ path: Section path, outermost first, e.g. ``("web", "auth")``.
334
+
335
+ Returns:
336
+ True if something was removed, False if the key was not there.
337
+
338
+ Raises:
339
+ YamlPatchError: If the path is empty.
340
+ """
341
+ if not path:
342
+ raise YamlPatchError("No section path given.")
343
+
344
+ file_path = Path(config_file)
345
+ lines = file_path.read_text(encoding="utf-8").splitlines(keepends=True)
346
+
347
+ start = _find_section_line(lines, path)
348
+ if start is None:
349
+ return False
350
+ key_indent = len(lines[start]) - len(lines[start].lstrip())
351
+
352
+ # Everything indented under the key belongs to it, blank lines included
353
+ # while more indented content follows.
354
+ end = start + 1
355
+ while end < len(lines):
356
+ line = lines[end]
357
+ if not line.strip():
358
+ following = next(
359
+ (i for i in range(end + 1, len(lines)) if lines[i].strip()), None
360
+ )
361
+ if following is None:
362
+ break
363
+ next_indent = len(lines[following]) - len(lines[following].lstrip())
364
+ if next_indent <= key_indent:
365
+ break
366
+ end += 1
367
+ continue
368
+ indent = len(line) - len(line.lstrip())
369
+ if indent <= key_indent:
370
+ break
371
+ end += 1
372
+
373
+ # Take the comment block sitting directly on top of it.
374
+ while start > 0:
375
+ above = lines[start - 1].strip()
376
+ if above.startswith("#"):
377
+ start -= 1
378
+ continue
379
+ break
380
+
381
+ del lines[start:end]
382
+ file_path.write_text("".join(lines), encoding="utf-8")
383
+ return True
boneio/core/containers.py CHANGED
@@ -55,7 +55,12 @@ _FALLBACK: dict[str, list[str]] = {
55
55
  "restart-caddy": ["docker", "compose", "restart", CADDY_SERVICE],
56
56
  "reload-caddy": [
57
57
  "docker", "compose", "exec", CADDY_SERVICE,
58
- "caddy", "reload", "--config", "/etc/caddy/Caddyfile",
58
+ # /tmp/Caddyfile, not /etc/caddy/Caddyfile: the container runs the
59
+ # file init-certs.sh writes on every start. /etc/caddy/Caddyfile is the
60
+ # stock config baked into the image, and reloading that swaps the
61
+ # working proxy for Caddy's welcome page — HTTPS goes down and stays
62
+ # down until the container is restarted.
63
+ "caddy", "reload", "--config", "/tmp/Caddyfile",
59
64
  ],
60
65
  }
61
66