Compare commits

...
1 Commits
Author SHA1 Message Date
thunderheronightandClaude Opus 5.5 20b1262fa9 0.4.1: clearer setup – config typos named, full path for splat-transform, PyAV 19 spotted
From issues #1 and #2:
- Unknown settings in config.toml (e.g. splat-transform instead of splat_transform) show up in the
  setup hints and in install.sh, with the right spelling.
- install.sh writes the full path of splat-transform into a new config or prints the line for an
  existing one: started from the desktop, the app does not see the shell's PATH.
- The setup guide's npm --prefix instructions match the path they tell you to configure.
- A pipeline not pinned below PyAV 19 is spotted before a run; a run failing on it says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:06:01 +02:00
9 changed files with 197 additions and 17 deletions

No files matched your search

+12
View File
@@ -2,6 +2,17 @@
All notable changes to Splat Workshop. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [Semantic Versioning](https://semver.org/).
## [0.4.1] – 2026-10-15
Setup made clearer – thanks to the first two issues from outside (#1, #2).
### Fixed
- **A typo in the config is pointed out**: a setting Splat Workshop does not know – such as `splat-transform =` instead of `splat_transform =` – used to be ignored silently, so splat-transform stayed "not found" without a reason. Now the setup hints name it, with the right spelling. `install.sh` does too.
- **splat-transform on the shell's PATH only**: `install.sh` checked the PATH of the shell it runs in, but Splat Workshop, started from the desktop or the tray, does not read `.bashrc`/`.zshrc` – the installer said ✓, the app said "not found". Now `install.sh` writes the full path into a new config, or prints the line to add to an existing one; the hint in the app explains why and how.
- **The npm instructions** in the setup guide installed into a folder whose path did not match the one given for the config. Now: `npm install -g --prefix ~/arcturus/node @playcanvas/splat-transform` (no root needed) and `~/arcturus/node/bin/splat-transform`.
- **The Arcturus pipeline and PyAV 19** ("unexpected keyword argument 'metadata_errors'"): Splat Workshop now spots a pipeline that is not pinned to PyAV below 19 before you start, and names the cause and the fix when a conversion fails because of it – instead of only a traceback. It does not change the pipeline itself.
## [0.4.0] – 2026-10-15
### Added
@@ -138,6 +149,7 @@ First release.
- **Floor plans** (experimental) in two styles with a measuring tool.
- Configurable video player; English and German interface.
[0.4.1]: https://github.com/thunderheronight/splat-workshop/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/thunderheronight/splat-workshop/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/thunderheronight/splat-workshop/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/thunderheronight/splat-workshop/compare/v0.2.3...v0.3.0
+10 -2
View File
@@ -88,7 +88,15 @@ Wandelt das Ergebnis in das kompakte SOG-Format, das Gaussian XR lädt:
npm install -g @playcanvas/splat-transform
```
Ohne globale Installation: `npm install --prefix ~/arcturus/node @playcanvas/splat-transform` und in der Config `splat_transform = "~/arcturus/node/bin/splat-transform"` eintragen.
Scheitert `npm install -g` an fehlenden Rechten, ins eigene Home installieren – ohne root:
```bash
npm install -g --prefix ~/arcturus/node @playcanvas/splat-transform
```
und in der Config unter `[paths]` `splat_transform = "~/arcturus/node/bin/splat-transform"` eintragen (mit Unterstrich – `splat-transform =` ist keine Einstellung und wird ignoriert; die Splat-Werkstatt weist darauf hin).
**Immer den vollen Pfad in die Config schreiben**, wenn splat-transform nur im PATH deiner Shell steht (`.bashrc`, `.zshrc`): Vom Desktop oder Tray gestartet liest die Splat-Werkstatt diese Dateien nicht und findet es sonst nicht. `install.sh` trägt die Zeile ein oder gibt sie aus.
## 5. Der Splat-Werkstatt sagen, wo alles liegt
@@ -106,7 +114,7 @@ Splat-Werkstatt neu starten (Tray → Beenden, dann wieder starten). Fehlt noch
| Meldung / Problem | Ursache / Lösung |
|---|---|
| `av.open() got an unexpected keyword argument 'metadata_errors'` | PyAV 19 hat diese Option entfernt. Ältere Version festlegen: im `# /// script`-Kopf oben in `av1_to_3dgs.py` und `av1_colmap.py` `"av>=18.1"` in `"av>=18.1,<19"` ändern. (Mit der Pipeline vom Herbst 2026 nötig; neuere Versionen haben es evtl. behoben.) |
| `av.open() got an unexpected keyword argument 'metadata_errors'` | PyAV 19 hat diese Option entfernt. Ältere Version festlegen: im `# /// script`-Kopf oben in `av1_to_3dgs.py` und `av1_colmap.py` `"av>=18.1"` in `"av>=18.1,<19"` ändern. (Mit der Pipeline vom Herbst 2026 nötig; neuere Versionen haben es evtl. behoben.) Die Splat-Werkstatt (ab 0.4.1) weist schon vor dem Start in ihren Einrichtungshinweisen darauf hin und nennt den Grund, falls eine Umwandlung daran scheitert. |
| `Could not select unique evenly spaced capture instants` | Aufnahme zu kurz. Die Pipeline braucht mindestens 100 Aufnahmezeitpunkte – mindestens 10 Sekunden aufnehmen, besser 30+. |
| Spirula findet keine GPU / nutzt llvmpipe | Vulkan-Treiber fehlt, siehe Schritt 1. |
| Die Szene taucht in Gaussian XR nicht auf | Sie muss als echter Ordner (kein Symlink) mit den Galeriebildern in `Scenes/` von Gaussian XR liegen – das macht der Knopf *Auf die Frame*. Ein Steam-Update oder „Dateien überprüfen“ von Gaussian XR kann den Ordner zurücksetzen; dann die Szene einfach neu übertragen. |
+10 -2
View File
@@ -88,7 +88,15 @@ Converts the result into the compact SOG format that Gaussian XR loads:
npm install -g @playcanvas/splat-transform
```
If you can't or don't want to install globally: `npm install --prefix ~/arcturus/node @playcanvas/splat-transform` and set `splat_transform = "~/arcturus/node/bin/splat-transform"` in the config.
If `npm install -g` fails with a permission error, install it into your home instead – no root needed:
```bash
npm install -g --prefix ~/arcturus/node @playcanvas/splat-transform
```
and set `splat_transform = "~/arcturus/node/bin/splat-transform"` under `[paths]` in the config (with an underscore – `splat-transform =` is not a setting and is ignored; Splat Workshop points that out).
**Always set the full path in the config** if splat-transform is only on the PATH of your shell (`.bashrc`, `.zshrc`): started from the desktop or the tray, Splat Workshop does not read those files and would not find it. `install.sh` writes or prints the line for you.
## 5. Tell Splat Workshop where everything is
@@ -106,7 +114,7 @@ Restart Splat Workshop (tray → Quit, then start it again). If something is sti
| Symptom | Cause / fix |
|---|---|
| `av.open() got an unexpected keyword argument 'metadata_errors'` | PyAV 19 removed that option. Pin the older version: in the `# /// script` header at the top of `av1_to_3dgs.py` and `av1_colmap.py`, change `"av>=18.1"` to `"av>=18.1,<19"`. (Was needed with the pipeline from autumn 2026; newer versions may have fixed it.) |
| `av.open() got an unexpected keyword argument 'metadata_errors'` | PyAV 19 removed that option. Pin the older version: in the `# /// script` header at the top of `av1_to_3dgs.py` and `av1_colmap.py`, change `"av>=18.1"` to `"av>=18.1,<19"`. (Was needed with the pipeline from autumn 2026; newer versions may have fixed it.) Splat Workshop (0.4.1 and newer) points this out in its setup hints before you start, and names it if a conversion fails because of it. |
| `Could not select unique evenly spaced capture instants` | Recording too short. The pipeline needs at least 100 capture instants – record at least 10 seconds, better 30+. |
| Spirula finds no GPU / uses llvmpipe | Vulkan driver missing, see step 1. |
| The scene does not appear in Gaussian XR | It must be a real folder in Gaussian XR's `Scenes/` (no symlink) with the gallery images – Splat Workshop's *To the Frame* button does this for you. A Steam update or “verify files” of Gaussian XR can reset that folder; just copy the scene again. |
+24 -3
View File
@@ -26,19 +26,40 @@ for tool in ffmpeg ffprobe uv ssh curl; do
command -v "$tool" >/dev/null && echo " ✓ $tool" || { echo " ✗ $tool"; missing=1; }
done
command -v splat-transform >/dev/null && echo " ✓ splat-transform" ||
echo " ! splat-transform not on PATH (npm install -g @playcanvas/splat-transform, or set it in the config)"
echo " ! splat-transform not on PATH (npm install -g --prefix ~/.local @playcanvas/splat-transform needs no root; or set its path in the config)"
python3 -c 'import PySide6' 2>/dev/null && echo " ✓ PySide6 (tray icon)" ||
echo " · PySide6 not installed – uv will fetch it on first start for the tray icon"
[ $missing = 0 ] || echo "Install the missing tools first (see README)."
mkdir -p "$CONF" "$APPS" "$ICONS"
created=0
if [ ! -f "$CONF/config.toml" ]; then
cp "$ROOT/config.example.toml" "$CONF/config.toml"
echo "Created $CONF/config.toml – set the paths to the Arcturus pipeline and Spirula, and your Frame's address."
created=1
echo "Created $CONF/config.toml from config.example.toml – set the paths to the Arcturus pipeline and Spirula, and your Frame's address."
else
echo "Keeping existing $CONF/config.toml"
echo "Keeping existing $CONF/config.toml (all settings with explanations: $ROOT/config.example.toml)"
fi
# Started from the desktop, Splat Workshop does not see a PATH set in .bashrc/.zshrc: give it the full path
st=$(command -v splat-transform || true)
conf_st=$(python3 -c 'import sys, tomllib
print(tomllib.load(open(sys.argv[1], "rb")).get("paths", {}).get("splat_transform", ""))' "$CONF/config.toml" 2>/dev/null || true)
if [ -n "$st" ] && [ -z "$conf_st" ]; then
if [ $created = 1 ]; then
sed -i "s|^splat_transform = \"\"|splat_transform = \"$st\"|" "$CONF/config.toml"
echo " ✓ splat_transform = \"$st\" written to the config"
else
echo " ! Add this line under [paths] in $CONF/config.toml – started from the desktop,"
echo " Splat Workshop does not see the PATH of your shell:"
echo " splat_transform = \"$st\""
fi
fi
# Settings Splat Workshop does not know (typos such as "splat-transform" instead of "splat_transform")
(cd "$ROOT" && python3 -c 'from splat_workshop import config
for key, hint in config.load().unknown:
print(f" ! Unknown setting \"{key}\" in the config" + (f" – did you mean \"{hint}\"?" if hint else " – it is ignored"))') || true
cp "$ROOT/web/icon.svg" "$ICONS/splat-workshop.svg"
entry() {
cat <<EOF
+51 -6
View File
@@ -5,7 +5,9 @@ Order: built-in defaults < ~/.config/splat-workshop/config.toml (or $SPLAT_WORKS
`python3 -m splat_workshop.config --shell` prints the values as shell assignments for scripts/.
"""
import difflib
import os
import re
import shlex
import shutil
import sys
@@ -32,9 +34,47 @@ DEFAULTS = {
}
def unknown_keys(data: dict) -> list[tuple[str, str]]:
"""Settings in the config file that Splat Workshop does not know (e.g. a typo: "splat-transform"),
as ("section.key", suggestion or "")."""
out = []
for section, values in data.items():
if section not in DEFAULTS:
close = difflib.get_close_matches(section, DEFAULTS, n=1, cutoff=0.6)
out.append((section, close[0] if close else ""))
continue
if not isinstance(values, dict):
continue
for key in values:
if key not in DEFAULTS[section]:
close = difflib.get_close_matches(key.replace("-", "_"), DEFAULTS[section], n=1, cutoff=0.6)
out.append((f"{section}.{key}", f"{section}.{close[0]}" if close else ""))
return out
# PyAV 19 removed av.open(metadata_errors=...), which the Arcturus pipeline (autumn 2026) uses
PYAV_FILES = ("av1_to_3dgs.py", "av1_colmap.py")
RX_AV_DEP = re.compile(r'^#\s*"av(?P<spec>[^"]*)"', re.M)
def pyav_unpinned(pipeline: Path) -> list[str]:
"""Pipeline scripts that use metadata_errors but allow PyAV 19 or newer."""
out = []
for name in PYAV_FILES:
try:
text = (pipeline / name).read_text(errors="replace")
except OSError:
continue
m = RX_AV_DEP.search(text)
if "metadata_errors" in text and m and "<19" not in m["spec"].replace(" ", ""):
out.append(name)
return out
class Config:
def __init__(self, data: dict, source: Path | None):
def __init__(self, data: dict, source: Path | None, unknown=()):
self.source = source
self.unknown = list(unknown)
base = source.parent if source else Path.home()
def path(v: str) -> Path:
@@ -63,12 +103,17 @@ class Config:
def problems(self) -> list[tuple[str, dict]]:
"""Missing prerequisites as (message key, params) for the setup hints in the UI."""
out = []
for key, suggestion in self.unknown:
out.append(("setup.unknown_key" if suggestion else "setup.unknown_key.plain",
{"key": key, "suggestion": suggestion, "file": str(self.source)}))
if not (self.pipeline / "av1_to_3dgs.py").is_file():
out.append(("setup.pipeline", {"path": str(self.pipeline)}))
elif bad := pyav_unpinned(self.pipeline):
out.append(("setup.pyav", {"files": ", ".join(bad), "path": str(self.pipeline)}))
if not os.access(self.spirula, os.X_OK):
out.append(("setup.spirula", {"path": str(self.spirula)}))
if not self.splat_transform:
out.append(("setup.splat_transform", {}))
out.append(("setup.splat_transform", {"file": str(self.source or CONFIG_FILE)}))
for tool in ("ffmpeg", "ffprobe", "uv"):
if not shutil.which(tool):
out.append(("setup.tool", {"tool": tool}))
@@ -103,12 +148,12 @@ def _merge(base: dict, over: dict) -> dict:
def load() -> Config:
data, source = DEFAULTS, None
data, source, unknown = DEFAULTS, None, []
if CONFIG_FILE.is_file():
with open(CONFIG_FILE, "rb") as fh:
data = _merge(DEFAULTS, tomllib.load(fh))
source = CONFIG_FILE
return Config(data, source)
own = tomllib.load(fh)
data, source, unknown = _merge(DEFAULTS, own), CONFIG_FILE, unknown_keys(own)
return Config(data, source, unknown)
def shell(cfg: Config) -> str:
+8 -1
View File
@@ -29,7 +29,7 @@ from pathlib import Path
from . import check, config
from .i18n import T, detect
VERSION = "0.4.0"
VERSION = "0.4.1"
CFG = config.load()
ROOT = config.ROOT
WEB = ROOT / "web"
@@ -594,9 +594,16 @@ def parse_output(text: str) -> dict:
info["done"] = True
if d[1] == "frame" and d[2]:
info["folder"] = d[2]
for needle, hint in KNOWN_FAILURES:
if needle in text:
info["hint"] = hint
return {"phase": phase, "pct": pct, "eta": eta, "info": info}
# Failures with a known cause: a line of the pipeline's output -> a hint in the interface (web/i18n.js)
KNOWN_FAILURES = (("unexpected keyword argument 'metadata_errors'", "hint.pyav"),)
def job_status(tail=True) -> dict | None:
st = load_state().get("job")
if not st:
+71
View File
@@ -0,0 +1,71 @@
"""Setup hints (0.4.1, from issues #1 and #2): typos in the config, splat-transform only on the shell's PATH,
an Arcturus pipeline that PyAV 19 breaks."""
import os
import subprocess
import pytest
from conftest import ROOT, server
from splat_workshop import config
UNPINNED = '''#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "av>=18.1",
# "numpy",
# ]
# ///
import av
av.open("x", metadata_errors="strict")
'''
def test_a_typo_in_the_config_is_named_with_the_right_spelling():
unknown = config.unknown_keys({"paths": {"splat-transform": "/x", "pipeline": "/p"}, "frmae": {"host": ""}})
assert unknown == [("paths.splat-transform", "paths.splat_transform"), ("frmae", "frame")]
c = config.Config(config._merge(config.DEFAULTS, {}), None, unknown)
keys = [k for k, _ in c.problems()]
assert keys[:2] == ["setup.unknown_key", "setup.unknown_key"]
def test_a_pipeline_that_pyav_19_breaks_is_found(tmp_path):
for name in config.PYAV_FILES:
(tmp_path / name).write_text(UNPINNED)
assert config.pyav_unpinned(tmp_path) == list(config.PYAV_FILES)
(tmp_path / "av1_colmap.py").write_text(UNPINNED.replace('"av>=18.1"', '"av>=18.1,<19"'))
assert config.pyav_unpinned(tmp_path) == ["av1_to_3dgs.py"]
c = config.Config(config._merge(config.DEFAULTS, {"paths": {"pipeline": str(tmp_path)}}), None)
hint = dict(c.problems())["setup.pyav"]
assert hint["files"] == "av1_to_3dgs.py"
def test_a_conversion_failing_on_pyav_says_why():
out = server.parse_output("Traceback ...\nTypeError: open() got an unexpected keyword argument 'metadata_errors'\n")
assert out["info"]["hint"] == "hint.pyav"
assert "hint" not in server.parse_output("Traceback ...\nValueError: something else\n")["info"]
@pytest.mark.parametrize("existing", [False, True])
def test_install_gives_the_full_path_of_splat_transform(tmp_path, existing):
"""install.sh finds splat-transform on the shell's PATH: a new config gets the full path, an existing one
a line to add – the app, started from the desktop, does not see that PATH."""
home, xdg, bin_ = tmp_path / "home", tmp_path / "xdg", tmp_path / "bin"
for d in (home, xdg, bin_):
d.mkdir()
(bin_ / "splat-transform").write_text("#!/bin/sh\n")
(bin_ / "splat-transform").chmod(0o755)
conf = xdg / "splat-workshop" / "config.toml"
if existing:
conf.parent.mkdir()
conf.write_text('[paths]\nsplat-transform = "/typo"\n')
env = {**os.environ, "HOME": str(home), "XDG_CONFIG_HOME": str(xdg), "XDG_DATA_HOME": str(tmp_path / "data"),
"PATH": f"{bin_}:{os.environ['PATH']}"}
env.pop("SPLAT_WORKSHOP_CONFIG", None)
r = subprocess.run([str(ROOT / "install.sh")], env=env, capture_output=True, text=True, timeout=120)
assert r.returncode == 0, r.stderr
line = f'splat_transform = "{bin_ / "splat-transform"}"'
if existing:
assert line in r.stdout and line not in conf.read_text()
assert 'Unknown setting "paths.splat-transform"' in r.stdout and '"paths.splat_transform"' in r.stdout
else:
assert line in conf.read_text() and "written to the config" in r.stdout
+10 -2
View File
@@ -15,7 +15,10 @@ const I18N = {
"setup.hint": "Settings are in {file}. Restart Splat Workshop after changing them.",
"setup.pipeline": "Arcturus pipeline not found in {path} (av1_to_3dgs.py). Download it from arcturus.vision/apps.",
"setup.spirula": "Spirula Studio not found at {path}. Build it as described in the pipeline README.",
"setup.splat_transform": "splat-transform not found – install it with: npm install -g @playcanvas/splat-transform",
"setup.splat_transform": "splat-transform not found. Install it (without root: npm install -g --prefix ~/.local @playcanvas/splat-transform) and put its full path into {file}, e.g. splat_transform = \"/home/you/.local/bin/splat-transform\" – started from the desktop, Splat Workshop does not see a PATH set in .bashrc or .zshrc.",
"setup.unknown_key": "Unknown setting “{key}” in {file} – did you mean “{suggestion}”? (Splat Workshop ignores it.)",
"setup.unknown_key.plain": "Unknown setting “{key}” in {file} – Splat Workshop ignores it.",
"setup.pyav": "The Arcturus pipeline in {path} will fail with “unexpected keyword argument 'metadata_errors'” (PyAV 19). In the # /// script header of {files}, change \"av>=18.1\" to \"av>=18.1,<19\" – see Troubleshooting in docs/arcturus-setup.md.",
"setup.tool": "{tool} is not installed.",
// job card
"job.cancel": "Cancel", "job.dismiss": "Hide", "job.log": "Log",
@@ -33,6 +36,7 @@ const I18N = {
"res.cancelled": "Cancelled. Start again with the same name and choose “Resume” to continue.",
"res.upload_failed": "The scene is finished, but copying to the Frame did not work (Frame off?). Use “To the Frame” in the list of finished scenes once it is on.",
"res.failed": "Failed – the last lines of the log show why.",
"hint.pyav": "Failed: the Arcturus pipeline does not work with PyAV 19 (“unexpected keyword argument 'metadata_errors'”). In the # /// script header of av1_to_3dgs.py and av1_colmap.py, change \"av>=18.1\" to \"av>=18.1,<19\", then resume – see Troubleshooting in docs/arcturus-setup.md.",
"confirm.cancel": "Really cancel the conversion? With “Resume” you can continue later from the last saved state.",
// step 1
"s1.title": "Choose a recording", "tab.frame": "On the Frame", "tab.local": "On this PC",
@@ -300,7 +304,10 @@ const I18N = {
"setup.hint": "Die Einstellungen stehen in {file}. Nach Änderungen die Splat-Werkstatt neu starten.",
"setup.pipeline": "Arcturus-Pipeline nicht gefunden in {path} (av1_to_3dgs.py). Download: arcturus.vision/apps.",
"setup.spirula": "Spirula Studio nicht gefunden unter {path}. Bauen wie in der Pipeline-README beschrieben.",
"setup.splat_transform": "splat-transform fehlt – installieren mit: npm install -g @playcanvas/splat-transform",
"setup.splat_transform": "splat-transform nicht gefunden. Installieren (ohne root: npm install -g --prefix ~/.local @playcanvas/splat-transform) und den vollen Pfad in {file} eintragen, z. B. splat_transform = \"/home/du/.local/bin/splat-transform\" – vom Desktop gestartet sieht die Splat-Werkstatt keinen PATH aus .bashrc oder .zshrc.",
"setup.unknown_key": "Unbekannte Einstellung „{key}“ in {file} – meintest du „{suggestion}“? (Die Splat-Werkstatt ignoriert sie.)",
"setup.unknown_key.plain": "Unbekannte Einstellung „{key}“ in {file} – die Splat-Werkstatt ignoriert sie.",
"setup.pyav": "Die Arcturus-Pipeline in {path} wird mit „unexpected keyword argument 'metadata_errors'“ scheitern (PyAV 19). Im # /// script-Kopf von {files} \"av>=18.1\" in \"av>=18.1,<19\" ändern – siehe Troubleshooting in docs/arcturus-setup.md.",
"setup.tool": "{tool} ist nicht installiert.",
"job.cancel": "Abbrechen", "job.dismiss": "Ausblenden", "job.log": "Protokoll",
"job.standby": "💤 Danach in den Standby", "job.standby.in": "💤 Standby in {s} s",
@@ -317,6 +324,7 @@ const I18N = {
"res.cancelled": "Abgebrochen. Mit gleichem Namen erneut starten und „Fortsetzen“ wählen, um weiterzumachen.",
"res.upload_failed": "Die Szene ist fertig gerechnet, aber das Kopieren auf die Frame hat nicht geklappt (Frame aus?). In der Liste der fertigen Szenen „Auf die Frame“ nutzen, sobald sie an ist.",
"res.failed": "Fehlgeschlagen – die letzten Zeilen im Protokoll zeigen warum.",
"hint.pyav": "Fehlgeschlagen: Die Arcturus-Pipeline läuft nicht mit PyAV 19 („unexpected keyword argument 'metadata_errors'“). Im # /// script-Kopf von av1_to_3dgs.py und av1_colmap.py \"av>=18.1\" in \"av>=18.1,<19\" ändern und fortsetzen – siehe Troubleshooting in docs/arcturus-setup.md.",
"confirm.cancel": "Umwandlung wirklich abbrechen? Mit „Fortsetzen“ geht es später ab dem letzten Zwischenstand weiter.",
"s1.title": "Aufnahme wählen", "tab.frame": "Auf der Frame", "tab.local": "Auf dem PC",
"dl.all": "Alle holen ({n} · {size})", "dl.all.title": "Alle Aufnahmen, die noch nicht auf dem PC sind, nacheinander holen",
+1 -1
View File
@@ -1016,7 +1016,7 @@ function renderJob() {
: tr("res.ok.local", {name: j.name, path: "runs/" + j.slug}); }
else if (j.result === "cancelled") { res.className = "result warn"; res.textContent = tr("res.cancelled"); }
else if (j.phase === 4) { res.className = "result warn"; res.textContent = tr("res.upload_failed"); }
else { res.className = "result bad"; res.textContent = tr("res.failed"); }
else { res.className = "result bad"; res.textContent = i.hint ? tr(i.hint) : tr("res.failed"); }
}
const log = $("#log"), atBottom = log.scrollTop + log.clientHeight >= log.scrollHeight - 20;
log.textContent = j.tail || ""; if (atBottom) log.scrollTop = log.scrollHeight;