Răsfoiți Sursa

chore: coderabbit suggestions

jamesread 2 săptămâni în urmă
părinte
comite
3471e2fb12

+ 0 - 203
INSTALL-MACOS.md

@@ -1,203 +0,0 @@
-# Installing OliveTin on macOS
-
-OliveTin runs natively on macOS on both **Apple Silicon (M1/M2/M3/M4)** and
-**Intel** Macs. It is a single self-contained binary written in Go — there is no
-installer and no background dependencies to install.
-
----
-
-## 1. Choose the right download
-
-macOS builds are published on the
-[GitHub releases page](https://github.com/OliveTin/OliveTin/releases). Pick the
-archive that matches your Mac's processor:
-
-| Your Mac | Archive |
-|---|---|
-| Apple Silicon (M-series) | `OliveTin-darwin-arm64.tar.gz` |
-| Intel | `OliveTin-darwin-amd64.tar.gz` |
-
-Not sure which you have? Run this in Terminal:
-
-```sh
-uname -m
-```
-
-`arm64` → Apple Silicon, `x86_64` → Intel.
-
-> If you download the wrong architecture, macOS will refuse to run it with a
-> "Bad CPU type in executable" error.
-
----
-
-## 2. Extract and place the binary
-
-```sh
-# Move to your Downloads folder (adjust if needed)
-cd ~/Downloads
-
-# Extract — replace arm64 with amd64 on Intel
-tar -xzf OliveTin-darwin-arm64.tar.gz
-cd OliveTin-darwin-arm64
-```
-
-For a quick try-out you can run it straight from this folder. To install it
-system-wide, copy the binary somewhere on your `PATH`:
-
-```sh
-sudo cp OliveTin /usr/local/bin/OliveTin
-```
-
----
-
-## 3. Clear the Gatekeeper quarantine
-
-Because the binary is downloaded from the internet and is **not notarized by
-Apple**, macOS Gatekeeper will block the first run with a message like
-*"OliveTin can't be opened because Apple cannot check it for malicious
-software."*
-
-Remove the quarantine attribute so it will run:
-
-```sh
-xattr -dr com.apple.quarantine ./OliveTin
-```
-
-Alternatively, the first time only, you can right-click the binary in Finder →
-**Open**, or approve it under **System Settings → Privacy & Security**.
-
----
-
-## 4. Create a configuration file
-
-OliveTin looks for a file named `config.yaml` in its **config directory**, which
-defaults to the current directory (`.`). You can point elsewhere with
-`-configdir /path/to/dir`.
-
-A minimal `config.yaml` to confirm everything works:
-
-```yaml
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337
-logLevel: "INFO"
-
-actions:
-  - title: Hello macOS
-    icon: terminal
-    shell: echo "Hello from $(scutil --get ComputerName)!"
-    popupOnStart: execution-dialog-stdout-only
-```
-
-For a fuller, macOS-tuned starting point — with working examples for
-notifications (`osascript`), `caffeinate`, `pmset`, disk usage, the unified
-system log, and Docker — see the **`macos.config.yaml`** that ships alongside
-this guide. Copy it in place with:
-
-```sh
-cp macos.config.yaml config.yaml
-```
-
----
-
-## 5. Run OliveTin
-
-From the folder that contains both `OliveTin` and `config.yaml`:
-
-```sh
-./OliveTin
-```
-
-Or, if you installed it to `/usr/local/bin` and keep your config elsewhere:
-
-```sh
-OliveTin -configdir /usr/local/etc/OliveTin
-```
-
-Then open the web interface at:
-
-```
-http://localhost:1337
-```
-
-(or `http://<your-mac-hostname>:1337` from another device on your network).
-
-Press **Ctrl-C** in the Terminal to stop it.
-
----
-
-## 6. Run OliveTin as a background service (launchd)
-
-On Linux, OliveTin is managed by **systemd**. The macOS equivalent is
-**launchd**. A ready-to-use service definition ships next to this guide as
-[`app.olivetin.olivetin.plist`](app.olivetin.olivetin.plist).
-
-You have two choices:
-
-* **LaunchAgent** (recommended for a desktop Mac) — runs as *your* user, starts
-  when you log in, no root required.
-* **LaunchDaemon** — runs as root, starts at boot before any user logs in. Use
-  this for a headless / always-on Mac.
-
-### As a LaunchAgent (per-user)
-
-```sh
-# Edit the two paths inside the plist to match your install first!
-cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
-
-# Start now and on every login
-launchctl load ~/Library/LaunchAgents/app.olivetin.olivetin.plist
-```
-
-To stop and disable it:
-
-```sh
-launchctl unload ~/Library/LaunchAgents/app.olivetin.olivetin.plist
-```
-
-### As a LaunchDaemon (system-wide, at boot)
-
-```sh
-sudo cp app.olivetin.olivetin.plist /Library/LaunchDaemons/
-sudo chown root:wheel /Library/LaunchDaemons/app.olivetin.olivetin.plist
-sudo launchctl load /Library/LaunchDaemons/app.olivetin.olivetin.plist
-```
-
-The plist sets `KeepAlive` (restart on crash, equivalent to systemd's
-`Restart=always`) and `RunAtLoad` (start immediately). It also writes logs to
-`/usr/local/var/log/olivetin.log`.
-
----
-
-## Troubleshooting
-
-**"Bad CPU type in executable"** — you downloaded the wrong architecture. Get
-the `arm64` build for Apple Silicon, `amd64` for Intel (see step 1).
-
-**Gatekeeper still blocks it** — re-run the `xattr -dr com.apple.quarantine`
-command in step 3, or approve the app under **System Settings → Privacy &
-Security**.
-
-**It runs but the page won't load** — check that nothing else is using port
-1337 (`lsof -i :1337`), and that you're browsing to `http://` (not `https://`).
-
-**Reading the logs**
-
-* Running in Terminal: the log is printed directly to the window.
-* Running under launchd: tail the log file configured in the plist:
-
-  ```sh
-  tail -f /usr/local/var/log/olivetin.log
-  ```
-
-* You can raise detail by setting `logLevel: "DEBUG"` in `config.yaml`.
-
-**Still stuck?** Ask in the
-[OliveTin Discord](https://discord.gg/jhYWWpNJ3v) or open an issue on
-[GitHub](https://github.com/OliveTin/OliveTin/issues).
-
----
-
-## Next steps
-
-* [Create your first action](https://docs.olivetin.app/action_execution/create_your_first.html)
-* [Configuration reference](https://docs.olivetin.app/)
-* [Security & authentication](https://docs.olivetin.app/security/local.html)

+ 0 - 45
app.olivetin.olivetin.plist

@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<!--
-  launchd service definition for OliveTin on macOS.
-  This is the macOS equivalent of the Linux systemd unit (var/systemd/OliveTin.service).
-
-  As a per-user LaunchAgent (recommended, no root needed):
-      cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
-      launchctl load   ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # start now + at login
-      launchctl unload ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # stop + disable
-
-  As a system-wide LaunchDaemon (starts at boot, before login; needs root):
-      sudo cp app.olivetin.olivetin.plist /Library/LaunchDaemons/
-      sudo chown root:wheel /Library/LaunchDaemons/app.olivetin.olivetin.plist
-      sudo launchctl load /Library/LaunchDaemons/app.olivetin.olivetin.plist
-
-  Edit the two paths below to match where you installed the binary and config.
--->
-<plist version="1.0">
-<dict>
-    <key>Label</key>
-    <string>app.olivetin.olivetin</string>
-
-    <!-- Path to the OliveTin binary, then flags. -configdir is the directory
-         that contains your config.yaml. -->
-    <key>ProgramArguments</key>
-    <array>
-        <string>/usr/local/bin/OliveTin</string>
-        <string>-configdir</string>
-        <string>/Users/YOUR_USER/etc/OliveTin</string>
-    </array>
-
-    <!-- systemd's Restart=always -->
-    <key>KeepAlive</key>
-    <true/>
-
-    <!-- Start at load (login for a LaunchAgent, boot for a LaunchDaemon) -->
-    <key>RunAtLoad</key>
-    <true/>
-
-<!-- For LaunchAgent (per-user): use /Users/YOUR_USER/Library/Logs/olivetin.log
-     For LaunchDaemon (system-wide): use /usr/local/var/log/olivetin.log -->
-    <key>StandardOutPath</key>
-    <string>/Users/YOUR_USER/Library/Logs/olivetin.log</string></dict>
-</plist>

+ 0 - 262
config.macos.yaml

@@ -1,262 +0,0 @@
-# =============================================================================
-# OliveTin example configuration — macOS (Apple Silicon & Intel)
-# =============================================================================
-#
-# This is a macOS-flavoured version of the stock `example.config.yaml`. Every
-# action that differs from the Linux example carries a `# Linux equivalent:`
-# comment so you can see exactly what was changed and why.
-#
-# All commands here are tested against macOS with /bin/zsh (the default login
-# shell since macOS Catalina) and also work under /bin/bash. OliveTin runs the
-# `shell:` string with `sh -c` by default, so these are written to be portable
-# POSIX/zsh/bash one-liners.
-#
-# To use this file, copy it next to the OliveTin binary as `config.yaml`:
-#   cp macos.config.yaml config.yaml
-#   ./OliveTin
-#
-# Docs: https://docs.olivetin.app/
-# =============================================================================
-
-# The built-in micro proxy hosts the WebUI and REST API on a single port.
-# Listen on all addresses, port 1337. Open http://localhost:1337 after start.
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337
-
-# Choose from INFO (default), WARN and DEBUG.
-# Docs: https://docs.olivetin.app/advanced_configuration/logs.html
-logLevel: "INFO"
-
-# Actions are commands that OliveTin executes, normally shown as buttons.
-# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
-actions:
-  # The simplest possible action: run a command, flash the button for status.
-  # `ping` works identically on macOS and Linux.
-  - title: Ping the Internet
-    shell: ping -c 3 1.1.1.1
-    icon: ping
-    popupOnStart: execution-dialog-stdout-only
-    execOnStartup: true
-
-  # Show just the command output in a popup.
-  #
-  # Linux equivalent: shell: df -h /media
-  # macOS has no /media mountpoint. The root volume is `/`; external/USB disks
-  # mount under /Volumes. `df -h /` shows the boot disk; drop the path to list
-  # every mounted volume.
-  - title: Check disk space
-    icon: disk
-    shell: df -h /
-    popupOnStart: execution-dialog-stdout-only
-
-  # Show a fuller dialog with details about the command that ran.
-  #
-  # Linux equivalent: shell: dmesg | tail
-  # macOS `dmesg` requires root and is rarely useful. The unified logging
-  # system is the macOS way to read kernel/system messages. This shows the
-  # last 2 minutes of high-level system log entries.
-  - title: Recent system log
-    shell: log show --last 2m --style compact | tail -n 40
-    icon: logs
-    popupOnStart: execution-dialog
-
-  # A mini button that links to the logs, with rate limiting and an hourly cron.
-  # `date` is identical across platforms.
-  - title: date
-    shell: date
-    id: date
-    timeout: 6
-    icon: clock
-    popupOnStart: execution-button
-    maxRate:
-      - limit: 3
-        duration: 1m
-    execOnCron:
-      - "@hourly"
-
-  # ---------------------------------------------------------------------------
-  # macOS-native actions (no Linux equivalent — these showcase the platform)
-  # ---------------------------------------------------------------------------
-
-  # Send a real macOS Notification Center banner via AppleScript.
-  - title: Send a notification
-    icon: '&#128276;'   # bell
-    shell: osascript -e 'display notification "Triggered from OliveTin" with title "OliveTin" sound name "Glass"'
-    popupOnStart: execution-button
-
-  # Stop the Mac from sleeping for 1 hour (handy during long jobs/downloads).
-  # `caffeinate` is a built-in macOS utility. `-t` is seconds.
-  - title: Keep awake for 1 hour
-    icon: '&#9749;'     # hot beverage
-    shell: caffeinate -d -i -t 3600 &
-    popupOnStart: execution-button
-
-  # Put the displays to sleep immediately (the Mac stays running).
-  - title: Sleep the displays
-    icon: '&#128164;'   # zzz
-    shell: pmset displaysleepnow
-    popupOnStart: execution-button
-
-  # Battery / power summary using the built-in `pmset`.
-  - title: Power & battery status
-    icon: '&#128267;'   # battery
-    shell: pmset -g batt
-    popupOnStart: execution-dialog-stdout-only
-
-  # ---------------------------------------------------------------------------
-
-  # Prompt the user for input with `arguments`. `ping` again works as-is.
-  # Docs: https://docs.olivetin.app/action_examples/ping.html
-  - title: Ping host
-    id: ping_host
-    shell: ping {{ host }} -c {{ count }}
-    icon: ping
-    timeout: 100
-    popupOnStart: execution-dialog-stdout-only
-    arguments:
-      - name: host
-        title: Host
-        type: ascii_identifier
-        default: example.com
-        description: The host that you want to ping
-
-      - name: count
-        title: Count
-        type: int
-        default: 3
-        description: How many times do you want to ping?
-
-  # OliveTin can control Docker containers — `docker` is just a CLI app.
-  # On macOS this requires Docker Desktop (or colima/podman) to be installed
-  # and running. The command itself is identical to Linux.
-  # Docs: https://docs.olivetin.app/solutions/container-control-panel/index.html
-  - title: Restart Docker Container
-    icon: restart
-    shell: docker restart {{ container }}
-    arguments:
-      - name: container
-        title: Container name
-        choices:
-          - value: plex
-          - value: traefik
-          - value: grafana
-
-  # The special `confirmation` argument guards against accidental clicks.
-  # Docs: https://docs.olivetin.app/args/input_confirmation.html
-  #
-  # Linux equivalent: shell: rm -rf /opt/oldBackups/
-  # Using a path under the user's home is more natural on macOS.
-  - title: Delete old backups
-    icon: ashtonished
-    shell: rm -rf "$HOME/Backups/old/"
-    arguments:
-      - name: confirm
-        type: confirmation
-        title: Are you sure?!
-
-  # Run your own scripts, not just OS commands. `maxConcurrent` prevents
-  # parallel runs; `timeout` kills a command that runs too long.
-  #
-  # Linux equivalent: shell: /opt/backupScript.sh
-  # macOS convention is to keep personal scripts under your home directory.
-  - title: Run backup script
-    shell: "$HOME/bin/backupScript.sh"
-    shellAfterCompleted: "osascript -e 'display notification \"Backup finished with code {{ exitCode }}\" with title \"OliveTin\"'"
-    maxConcurrent: 1
-    timeout: 10
-    icon: backup
-    popupOnStart: execution-dialog
-
-  # Download themes using a script bundled with OliveTin. You still need to set
-  # `themeName` in this config to actually use the theme.
-  # Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
-  - title: Get OliveTin Theme
-    exec:
-      - "olivetin-get-theme"
-      - "{{ themeGitRepo }}"
-      - "{{ themeFolderName }}"
-    icon: theme
-    arguments:
-      - name: themeGitRepo
-        title: Theme's Git Repository
-        description: Find new themes at https://olivetin.app/themes
-        type: url
-
-      - name: themeFolderName
-        title: Theme's Folder Name
-        type: ascii_identifier
-
-  # Run actions on other servers over SSH. macOS ships with an OpenSSH client,
-  # so this works out of the box. The helper below is optional.
-  # Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
-  - title: "Setup easy SSH"
-    icon: ssh
-    shell: olivetin-setup-easy-ssh
-    popupOnStart: execution-dialog
-
-# Entities let you generate actions dynamically from "things" (servers,
-# containers, VMs) loaded from files on disk.
-# Docs: https://docs.olivetin.app/entities/intro.html
-#
-# entities:
-#   - file: entities/servers.yaml
-#     name: server
-
-# Dashboards organise actions into folders and fieldsets.
-# Docs: https://docs.olivetin.app/dashboards/intro.html
-#
-# dashboards:
-#   - title: My Mac
-#     contents:
-#       - title: Power & battery status
-#       - title: Sleep the displays
-
-# =============================================================================
-# Security - Authentication
-# =============================================================================
-
-# If "true", users must log in before doing anything.
-authRequireGuestsToLogin: false
-
-# The simplest auth: define users/passwords in this config. OliveTin also
-# supports header-based auth, OAuth2 and JWT (documented separately).
-# Docs: https://docs.olivetin.app/security/local.html
-#
-# Generating an argon2id hash on macOS:
-#   brew install argon2
-#   echo -n 'yourPassword' | argon2 "$(openssl rand -base64 16)" -id -e
-# (Linux equivalent typically uses the distro's `argon2` package directly.)
-authLocalUsers:
-  enabled: true
-#  users:
-#    - username: alice
-#      usergroup: admins
-#      password: "$argon2id$v=19$m=65536,t=4,p=2$puyxA0s555TSFx7hnFLCXA$PyhLGpZtvpMMvc2DgMWkM8OJMKO55euwV5gm//1iwx4"
-
-# =============================================================================
-# Security - Access Control
-# =============================================================================
-
-# Policies affect the whole app (eg: ability to view the log list).
-# Docs: https://docs.olivetin.app/security/acl.html
-defaultPolicy:
-  showDiagnostics: true
-  showLogList: true
-
-# Permissions affect individual actions.
-defaultPermissions:
-  view: true
-  exec: true
-  logs: true
-
-# ACLs match policy/permissions to users.
-accessControlLists:
-  - name: admin_acl
-    matchUsergroups: ["admins"]
-    policy:
-      showDiagnostics: true
-    permissions:
-      view: true
-      exec: true
-      logs: true
-
-# OliveTin has many more options not shown here. See docs.olivetin.app.

+ 110 - 96
docs/modules/ROOT/pages/install/macos_service.adoc

@@ -4,41 +4,21 @@ This option installs OliveTin as a launchd service, so it runs in the background
 
 Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and clear the Gatekeeper quarantine) and confirm OliveTin starts correctly by running `./OliveTin`.
 
-== Install the files
-
-Run these from the extracted archive directory:
-
-[source,shell]
-----
-# Create the application folder and a place for logs
-mkdir -p ~/Library/Application\ Support/OliveTin/var
-mkdir -p ~/Library/Logs/OliveTin
-
-# Copy in the binary, your config, and the bundled web UI
-cp OliveTin   ~/Library/Application\ Support/OliveTin/
-cp config.yaml ~/Library/Application\ Support/OliveTin/
-cp -R webui   ~/Library/Application\ Support/OliveTin/
-----
+== Choose LaunchAgent or LaunchDaemon
 
-This gives you the following layout, all owned by your user:
+launchd offers two ways to run a background service:
 
-[source]
-----
-~/Library/Application Support/OliveTin/
-├── OliveTin          # the binary
-├── config.yaml       # your configuration
-├── webui/            # the web interface assets (shipped in the archive)
-└── var/              # runtime data OliveTin writes (logs, etc.)
+* *LaunchAgent* - runs as your user and starts when you log in. No root required. Best for a desktop Mac.
+* *LaunchDaemon* - runs as root and starts at boot, before any user logs in. Best for a headless, always-on Mac.
 
-~/Library/Logs/OliveTin/olivetin.log   # service stdout/stderr
-----
+Follow one complete flow below. Both use the same plist structure; only the install locations and `launchctl` domain differ.
 
-=== Create the service definition
+== Service definition
 
 Create a file named `app.olivetin.olivetin.plist` with the contents below.
 
 [IMPORTANT]
-launchd does *not* expand `~`, so the paths must be absolute. Replace `YOUR_USERNAME` with the output of `whoami` in every path.
+launchd does *not* expand `~`, so every path in the plist must be absolute. Replace `YOUR_USERNAME` with the output of `whoami` when using the LaunchAgent paths.
 
 [source,xml]
 ----
@@ -73,118 +53,161 @@ launchd does *not* expand `~`, so the paths must be absolute. Replace `YOUR_USER
 </plist>
 ----
 
-`WorkingDirectory` makes the relative `webui` and `var` folders resolve inside the application folder, `KeepAlive` restarts OliveTin if it exits (like systemd's `Restart=always`), and `RunAtLoad` starts it as soon as the service is loaded.
+For a LaunchDaemon, use the same keys but substitute the paths shown in the table:
 
-=== Register and start the service
+[cols="1,1,1"]
+|===
+| Plist entry | LaunchAgent | LaunchDaemon
 
-[source,shell]
-----
-cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
-launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.olivetin.olivetin.plist
-----
+| Binary (`ProgramArguments[0]`)
+| `/Users/YOUR_USERNAME/Library/Application Support/OliveTin/OliveTin`
+| `/usr/local/bin/OliveTin`
+
+| `-configdir` and `WorkingDirectory`
+| `/Users/YOUR_USERNAME/Library/Application Support/OliveTin`
+| `/usr/local/etc/OliveTin`
+
+| `StandardOutPath` / `StandardErrorPath`
+| `/Users/YOUR_USERNAME/Library/Logs/OliveTin/olivetin.log`
+| `/usr/local/var/log/olivetin.log`
+|===
+
+`WorkingDirectory` makes the relative `webui` and `var` folders resolve inside the config directory, `KeepAlive` restarts OliveTin if it exits (like systemd's `Restart=always`), and `RunAtLoad` starts it as soon as the service is loaded.
 
 [NOTE]
-OliveTin looks for `config.yaml` in the directory given by the `-configdir` flag, which defaults to the current directory. The service definition below passes `-configdir /usr/local/etc/OliveTin` explicitly.
+OliveTin looks for `config.yaml` in the directory given by the `-configdir` flag. The plist passes `-configdir` explicitly so the service does not depend on the process working directory alone.
 
-== Choose LaunchAgent or LaunchDaemon
+[NOTE]
+`launchctl bootstrap`/`bootout` replace the deprecated `launchctl load`/`unload`.
 
-launchd offers two ways to run a background service:
+== LaunchAgent (per-user)
 
-* *LaunchAgent* - runs as your user and starts when you log in. No root required. Best for a desktop Mac.
-* *LaunchDaemon* - runs as root and starts at boot, before any user logs in. Best for a headless, always-on Mac.
+=== Install the files
 
-== Create the service definition
+Run these from the extracted archive directory:
 
-Create a file named `app.olivetin.olivetin.plist` with the following contents. Adjust the two paths if you installed OliveTin elsewhere.
-sudo cp -R webui /usr/local/etc/OliveTin/
+[source,shell]
 ----
+# Create the application folder and a place for logs
+mkdir -p ~/Library/Application\ Support/OliveTin/var
+mkdir -p ~/Library/Logs/OliveTin
 
-[NOTE]
-OliveTin looks for `config.yaml` in the directory given by the `-configdir` flag, which defaults to the current directory. The service definition below passes `-configdir /usr/local/etc/OliveTin` explicitly, and sets `WorkingDirectory` so the `webui` and `var` folders resolve there.
+# Copy in the binary, your config, and the bundled web UI
+cp OliveTin   ~/Library/Application\ Support/OliveTin/
+cp config.yaml ~/Library/Application\ Support/OliveTin/
+cp -R webui   ~/Library/Application\ Support/OliveTin/
+----
 
-=== Create the service definition
+This gives you the following layout, all owned by your user:
 
-Create a file named `app.olivetin.olivetin.plist` with the following contents. Adjust the paths if you installed OliveTin elsewhere.
+[source]
+----
+~/Library/Application Support/OliveTin/
+├── OliveTin          # the binary
+├── config.yaml       # your configuration
+├── webui/            # the web interface assets (shipped in the archive)
+└── var/              # runtime data OliveTin writes (logs, etc.)
 
-[source,xml]
+~/Library/Logs/OliveTin/olivetin.log   # service stdout/stderr
 ----
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>Label</key>
-    <string>app.olivetin.olivetin</string>
 
-    <key>ProgramArguments</key>
-    <array>
-        <string>/usr/local/bin/OliveTin</string>
-        <string>-configdir</string>
-        <string>/usr/local/etc/OliveTin</string>
-    </array>
+=== Register and start
 
-    <key>WorkingDirectory</key>
-    <string>/usr/local/etc/OliveTin</string>
+[source,shell]
+----
+cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.olivetin.olivetin.plist
+----
 
-    <key>KeepAlive</key>
-    <true/>
+=== Verify
 
-    <key>RunAtLoad</key>
-    <true/>
+Open http://localhost:1337 in a browser. If the page does not load, check the service log:
 
-    <key>StandardOutPath</key>
-    <string>/usr/local/var/log/olivetin.log</string>
-    <key>StandardErrorPath</key>
-    <string>/usr/local/var/log/olivetin.log</string>
-</dict>
-</plist>
+[source,shell]
+----
+tail -f ~/Library/Logs/OliveTin/olivetin.log
+----
+
+=== Stop and disable
+
+[source,shell]
+----
+launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/app.olivetin.olivetin.plist
 ----
 
-`KeepAlive` restarts OliveTin if it exits (like systemd's `Restart=always`), and `RunAtLoad` starts it as soon as the service is loaded.
+=== Restart after a change
 
-=== Register and start the service
+After editing `config.yaml` or replacing the binary, restart the service so the change takes effect:
 
 [source,shell]
 ----
-mkdir -p /usr/local/var/log
-cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
-launchctl load ~/Library/LaunchAgents/app.olivetin.olivetin.plist
+launchctl kickstart -k gui/$(id -u)/app.olivetin.olivetin
 ----
 
-To stop and disable it:
+If you changed the *plist* itself, `kickstart` is not enough - boot the service out and back in so launchd re-reads it (`bootstrap` errors if the service is still loaded):
 
 [source,shell]
 ----
-launchctl unload ~/Library/LaunchAgents/app.olivetin.olivetin.plist
+launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/app.olivetin.olivetin.plist
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.olivetin.olivetin.plist
 ----
 
-=== As a LaunchDaemon (system-wide, at boot)
+== LaunchDaemon (system-wide)
+
+=== Install the files
+
+Run these from the extracted archive directory:
 
 [source,shell]
 ----
-sudo launchctl kickstart -k system/app.olivetin.olivetin
+sudo mkdir -p /usr/local/bin /usr/local/etc/OliveTin/var /usr/local/var/log
+
+sudo cp OliveTin   /usr/local/bin/OliveTin
+sudo cp config.yaml /usr/local/etc/OliveTin/
+sudo cp -R webui   /usr/local/etc/OliveTin/
 ----
 
-sudo launchctl bootstrap system /Library/LaunchDaemons/app.olivetin.olivetin.plist
+This gives you the following layout:
+
+[source]
 ----
+/usr/local/bin/OliveTin
+/usr/local/etc/OliveTin/
+├── config.yaml
+├── webui/
+└── var/
 
-[NOTE]
-`bootstrap`/`bootout` replace the deprecated `launchctl load`/`unload`. The domain target for a LaunchDaemon is `system`.
+/usr/local/var/log/olivetin.log   # service stdout/stderr
+----
 
-To stop and disable it:
+=== Register and start
 
 [source,shell]
 ----
-sudo launchctl bootout system /Library/LaunchDaemons/app.olivetin.olivetin.plist
+sudo cp app.olivetin.olivetin.plist /Library/LaunchDaemons/
+sudo chown root:wheel /Library/LaunchDaemons/app.olivetin.olivetin.plist
 sudo launchctl bootstrap system /Library/LaunchDaemons/app.olivetin.olivetin.plist
 ----
 
-== Verify
+=== Verify
+
+Open http://localhost:1337 in a browser. If the page does not load, check the service log:
+
+[source,shell]
+----
+tail -f /usr/local/var/log/olivetin.log
+----
+
+=== Stop and disable
+
+[source,shell]
+----
 sudo launchctl bootout system /Library/LaunchDaemons/app.olivetin.olivetin.plist
 ----
 
 === Restart after a change
 
-After editing `config.yaml` or replacing the binary, restart the service so the change takes effect. To restart in place:
+After editing `config.yaml` or replacing the binary, restart the service so the change takes effect:
 
 [source,shell]
 ----
@@ -199,13 +222,4 @@ sudo launchctl bootout system /Library/LaunchDaemons/app.olivetin.olivetin.plist
 sudo launchctl bootstrap system /Library/LaunchDaemons/app.olivetin.olivetin.plist
 ----
 
-=== Verify
-
-Open http://localhost:1337 in a browser. If the page does not load, check the service log:
-
-[source,shell]
-----
-tail -f /usr/local/var/log/olivetin.log
-----
-
 include::partial$install/post_generic.adoc[]

Fișier diff suprimat deoarece este prea mare
+ 275 - 274
frontend/package-lock.json


+ 10 - 10
frontend/package.json

@@ -6,7 +6,7 @@
 	"source": "index.html",
 	"devDependencies": {
 		"process": "^0.11.10",
-		"stylelint": "^17.12.0",
+		"stylelint": "^17.13.0",
 		"stylelint-config-standard": "^40.0.0"
 	},
 	"scripts": {
@@ -22,21 +22,21 @@
 	],
 	"license": "AGPL-3.0-only",
 	"dependencies": {
-		"@connectrpc/connect": "^2.1.1",
-		"@connectrpc/connect-web": "^2.1.1",
-		"@hugeicons/core-free-icons": "^4.1.4",
-		"@hugeicons/vue": "^1.0.5",
+		"@connectrpc/connect": "^2.1.2",
+		"@connectrpc/connect-web": "^2.1.2",
+		"@hugeicons/core-free-icons": "^4.2.0",
+		"@hugeicons/vue": "^1.0.6",
 		"@vitejs/plugin-vue": "^6.0.7",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
-		"picocrank": "^1.15.0",
+		"picocrank": "^1.16.0",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^32.1.0",
-		"vite": "^8.0.14",
-		"vue": "^3.5.34",
-		"vue-i18n": "^11.4.4",
-		"vue-router": "^5.0.7"
+		"vite": "^8.0.16",
+		"vue": "^3.5.38",
+		"vue-i18n": "^11.4.5",
+		"vue-router": "^5.1.0"
 	}
 }

+ 10 - 1
frontend/resources/vue/ActionButton.vue

@@ -71,6 +71,7 @@ const rateLimitExpires = ref(0)
 const isRateLimited = ref(false)
 const rateLimitMessage = ref('')
 let rateLimitInterval = null
+let isComponentMounted = true
 
 // Animation classes
 const buttonClasses = ref([])
@@ -217,9 +218,12 @@ async function pollExecutionUntilDone (trackingId) {
   const pollTimeoutMs = 10 * 60 * 1000
   const deadline = Date.now() + pollTimeoutMs
 
-  while (Date.now() < deadline) {
+  while (Date.now() < deadline && isComponentMounted) {
     try {
       const result = await window.client.executionStatus({ executionTrackingId: trackingId })
+      if (!isComponentMounted) {
+        return
+      }
       if (result.logEntry) {
         applyExecutionLogEntry(result.logEntry)
         if (result.logEntry.executionFinished) {
@@ -230,6 +234,10 @@ async function pollExecutionUntilDone (trackingId) {
       console.error('Failed to poll execution status:', err)
     }
 
+    if (!isComponentMounted) {
+      return
+    }
+
     await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
   }
 }
@@ -350,6 +358,7 @@ onMounted(() => {
 })
 
 onUnmounted(() => {
+  isComponentMounted = false
   if (rateLimitInterval) {
 	clearInterval(rateLimitInterval)
 	rateLimitInterval = null

+ 10 - 0
frontend/resources/vue/Dashboard.vue

@@ -83,6 +83,7 @@ const loadingTime = ref(0)
 const initError = ref(null)
 let loadingTimer = null
 let checkInitInterval = null
+let dashboardRequestId = 0
 
 const isDirectory = computed(() => {
     if (!dashboard.value || !window.initResponse) {
@@ -106,6 +107,7 @@ function goBack() {
 }
 
 async function getDashboard() {
+    const requestId = ++dashboardRequestId
     let title = props.title
 
     // If no specific title was provided or it's the placeholder 'default',
@@ -126,6 +128,10 @@ async function getDashboard() {
 
         const ret = await window.client.getDashboard(request)
 
+        if (requestId !== dashboardRequestId) {
+            return
+        }
+
         if (!ret || !ret.dashboard) {
             throw new Error('No dashboard found')
         }
@@ -146,6 +152,10 @@ async function getDashboard() {
         // Set attribute to indicate dashboard is loaded successfully
         document.body.setAttribute('loaded-dashboard', title || 'default')
     } catch (e) {
+        if (requestId !== dashboardRequestId) {
+            return
+        }
+
         // On error, provide a safe fallback state
         console.error('Failed to load dashboard', e)
         dashboard.value = { title: title || 'Default', contents: [] }

+ 10 - 3
frontend/resources/vue/views/ArgumentForm.vue

@@ -58,7 +58,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { requestReconnectNow } from '../../../js/websocket.js'
 
@@ -76,6 +76,7 @@ const formErrors = ref({})
 const actionArguments = ref([])
 const popupOnStart = ref('')
 const formReady = ref(false)
+let isComponentMounted = true
 
 // Computed properties
 
@@ -145,8 +146,10 @@ async function setup() {
       }
     }
 
-    formReady.value = true
-    document.body.setAttribute('loaded-argument-form', props.bindingId)
+    if (isComponentMounted) {
+      formReady.value = true
+      document.body.setAttribute('loaded-argument-form', props.bindingId)
+    }
   } catch (err) {
     console.error('Failed to load argument form:', err)
   }
@@ -486,6 +489,10 @@ onMounted(() => {
   setup()
 })
 
+onBeforeUnmount(() => {
+  isComponentMounted = false
+})
+
 onUnmounted(() => {
   document.body.removeAttribute('loaded-argument-form')
 })

+ 19 - 1
integration-tests/runner.mjs

@@ -105,11 +105,29 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
     if (this.ot.exitCode != null) {
       console.log('      OliveTin local process tried stop(), but it already exited with code', this.ot.exitCode)
     } else {
+      const stopTimeoutMs = 5000
       const closed = new Promise((resolve) => {
         this.ot.once('close', resolve)
       })
+
       this.ot.kill('SIGTERM')
-      await closed
+
+      const didStopGracefully = await Promise.race([
+        closed.then(() => true),
+        new Promise((resolve) => setTimeout(() => resolve(false), stopTimeoutMs))
+      ])
+
+      if (!didStopGracefully) {
+        console.log('      OliveTin local process did not exit after SIGTERM, sending SIGKILL')
+        if (this.ot.exitCode == null) {
+          this.ot.kill('SIGKILL')
+        }
+        await Promise.race([
+          closed,
+          new Promise((resolve) => setTimeout(resolve, stopTimeoutMs))
+        ])
+      }
+
       console.log('      OliveTin local process killed')
     }
 

+ 3 - 3
integration-tests/tests/datetime/datetime.mjs

@@ -5,7 +5,7 @@ import {
   getRootAndWait,
   getActionButton,
   takeScreenshotOnFailure,
-  waitForArgumentFormPage,
+  waitForArgumentFormReady,
   waitForLogsPage,
 } from '../../lib/elements.js'
 
@@ -29,7 +29,7 @@ describe('config: datetime', function () {
 
     await btn.click()
 
-    await waitForArgumentFormPage()
+    await waitForArgumentFormReady()
 
     // Find the datetime input field
     const datetimeInput = await webdriver.findElement(By.id('datetime'))
@@ -54,7 +54,7 @@ describe('config: datetime', function () {
 
     await btn.click()
 
-    await waitForArgumentFormPage()
+    await waitForArgumentFormReady()
 
     // Find the datetime input field
     const datetimeInput = await webdriver.findElement(By.id('datetime'))

+ 5 - 2
service/internal/servicehost/servicehost_windows.go

@@ -53,7 +53,10 @@ func (m *otWindowsService) Execute(args []string, r <-chan svc.ChangeRequest, st
 func setupLogging() {
 	logsDir := path.Join(GetConfigFilePath(), "logs")
 
-	os.MkdirAll(logsDir, 0755)
+	if err := os.MkdirAll(logsDir, 0755); err != nil {
+		log.Errorf("Failed to create logs directory %v: %v", logsDir, err)
+		return
+	}
 
 	timestamp := time.Now().Format("2006-01-02_15-04-05")
 
@@ -64,7 +67,7 @@ func setupLogging() {
 	f, err := os.Create(filename)
 
 	if err != nil {
-		log.Infof("Failed to open log file: %v", err)
+		log.Errorf("Failed to open log file: %v", err)
 	} else {
 		log.Infof("Switching to log file: %v", f.Name())
 		log.SetOutput(f)

+ 7 - 6
var/macos/app.olivetin.olivetin.plist

@@ -6,8 +6,8 @@
 
   As a per-user LaunchAgent (recommended, no root needed):
       cp app.olivetin.olivetin.plist ~/Library/LaunchAgents/
-      launchctl bootstrap   ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # start now + at login
-      launchctl bootout ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # stop + disable
+      launchctl load   ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # start now + at login
+      launchctl unload ~/Library/LaunchAgents/app.olivetin.olivetin.plist   # stop + disable
 
   As a system-wide LaunchDaemon (starts at boot, before login; needs root):
       sudo cp app.olivetin.olivetin.plist /Library/LaunchDaemons/
@@ -27,7 +27,7 @@
     <array>
         <string>/usr/local/bin/OliveTin</string>
         <string>-configdir</string>
-        <string>/usr/local/etc/OliveTin</string>
+        <string>/Users/YOUR_USER/etc/OliveTin</string>
     </array>
 
     <!-- systemd's Restart=always -->
@@ -38,10 +38,11 @@
     <key>RunAtLoad</key>
     <true/>
 
-    <!-- Where stdout/stderr go; tail these for troubleshooting -->
+<!-- For LaunchAgent (per-user): use /Users/YOUR_USER/Library/Logs/olivetin.log
+     For LaunchDaemon (system-wide): use /usr/local/var/log/olivetin.log -->
     <key>StandardOutPath</key>
-    <string>/usr/local/var/log/olivetin.log</string>
+    <string>/Users/YOUR_USER/Library/Logs/olivetin.log</string>
     <key>StandardErrorPath</key>
-    <string>/usr/local/var/log/olivetin.log</string>
+    <string>/Users/YOUR_USER/Library/Logs/olivetin.log</string>
 </dict>
 </plist>

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff