diff options
| author | Danilo M. <danix@danix.xyz> | 2026-05-05 10:51:56 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-05-05 10:51:56 +0200 |
| commit | 54ad3e37c6e9d2242f932c192119dd6de64966cf (patch) | |
| tree | 84c7018152825a63c2f6b009f18634a8585d996a | |
| parent | 48b84da95d0736124f8be6849aa32df31bdb29aa (diff) | |
| download | publisher-1.6.tar.gz publisher-1.6.zip | |
feat: check OLLAMA_HOST reachability before starting translationv1.6
Non-blocking QThread check hits /api/tags on the resolved ollama host
before launching TranslationWorker. Shows QMessageBox.warning() if
unreachable; proceeds silently if reachable. Adds ollama_host field to
Config with env-var fallback to http://localhost:11434.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | HANDOFF.md | 8 | ||||
| -rw-r--r-- | TODO.md | 25 | ||||
| -rw-r--r-- | core/config.py | 7 | ||||
| -rw-r--r-- | tests/test_config.py | 27 | ||||
| -rw-r--r-- | tests/test_ollama_check_worker.py | 11 | ||||
| -rw-r--r-- | ui/main_window.py | 1 | ||||
| -rw-r--r-- | ui/translation_view.py | 35 | ||||
| -rw-r--r-- | workers/ollama_check_worker.py | 19 |
8 files changed, 115 insertions, 18 deletions
@@ -1,14 +1,14 @@ Who this is for: - Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He maintains a PyQt6 desktop app called my-publisher that centralizes his blog publishing workflow, and is continuing to extend it post-v1.3. + Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He maintains a PyQt6 desktop app called my-publisher that centralizes his blog publishing workflow, and is continuing to extend it post-v1.4. What we covered: - Four bugs were fixed this session. First, the categories field in FrontmatterEditor was displaying Python list repr (e.g. "['cat1', 'cat2']") instead of a comma-separated string, and saving it back as a corrupted TOML string. Fixed by adding an isinstance(val, list) branch in _build_ui() and generalising the save logic to detect list-typed fields by checking the original frontmatter value type rather than hardcoding "tags". Second, the article type combo box was defaulting to "Life" instead of the article's actual type, because ARTICLE_TYPES used titlecase while files store lowercase values. Fixed by lowercasing ARTICLE_TYPES and doing a case-insensitive match on load. Third, on save the type was being written back in titlecase instead of lowercase. Fixed by the same ARTICLE_TYPES change. Fourth, categories autocomplete was added to FrontmatterEditor: a MultiTokenCompleter (already used for tags) is now also attached to the categories QLineEdit, loading suggestions from docs/categories.txt via load_categories(). All 34 tests pass. Three commits were made and pushed. v1.4 was tagged and pushed. + npm build integration was planned and implemented this session. The blog repo uses Tailwind CSS and npm run build compiles main.css to main.min.css. A new NpmWorker (workers/npm_worker.py) was created following the existing HugoWorker pattern: QObject + QProcess, emits log_line(str) and finished(bool). HugoPanel (ui/hugo_panel.py) was updated to sequence npm run build before starting hugo server when Avvia is clicked, and a new "Build CSS" button was added to allow manual Tailwind rebuilds while the server is already running. Button states are managed across four states: idle, building, running, rebuilding. All 34 tests pass. Changes were committed, pushed to master, and tagged v1.5. What was confirmed: - All 34 tests pass after every change. No articles in the blog repo had corrupted categories fields — the corruption seen in earlier exploration was not present in the actual files. ARTICLE_TYPES is now ["life", "photo", "link", "quote", "tech"]. The save path for both tags and categories now uses isinstance(self._article.frontmatter.get(key), list) to detect list fields generically. MultiTokenCompleter loads categories from docs/categories.txt (a single non-lang-split file, unlike tags which have tags-it.txt and tags-en.txt). v1.4 is live on origin/master. + NpmWorker follows the exact same QObject+QProcess pattern as HugoWorker. Sequential order was chosen: build completes before hugo starts. If build fails before hugo was ever started, the UI returns to idle state and Avvia is re-enabled. The blog repo package.json lives at the same path as config.blog_repo, so no new config fields were needed. Closing the app while hugo is running triggers stop_server() via closeEvent in main_window.py, which calls terminate() + waitForFinished(3000) on the QProcess. v1.5 is live on origin/master. Still in progress: Nothing was left open in this session. Next steps: - Remaining TODO items include: (1) npm run build integration before launching hugo server, with a button to trigger it while the server is running; (2) transart backend availability check using OLLAMA_HOST — validate before translation starts or disable the translate button with a popup; (3) font size spinner in SetupDialog — add QSpinBox, save via Config.save(), re-apply with app.setFont(); (4) article editor language flag in the sidebar; (5) manage static pages in addition to articles.
\ No newline at end of file + Remaining TODO items include: (1) transart backend availability check using OLLAMA_HOST — validate before translation starts or disable the translate button with a popup; (2) font size spinner in SetupDialog — add QSpinBox, save via Config.save(), re-apply with app.setFont(); (3) article editor language flag in the sidebar; (4) manage static pages in addition to articles.
\ No newline at end of file @@ -1,8 +1,5 @@ # TODO -## hugo server -- [ ] we should add a way to run `npm run build` before launching the hugo server. We also need to be able to run it using a button in the interface when the server is already running. - ## save on exit - [ ] if a file has been modified, ask for confirmation before exiting the application to avoid losing local edits. @@ -17,15 +14,6 @@ ## article editor - [ ] in the article editor window, add a flag to the top of the sidebar to easily recognize what language the content is in. Useful for empty drafts where no content has been written yet. -## frontmatter -### taxonomies -- [✅] when modifying the frontmatter, the square brackets around the tags or categories lists are not maintained. -### article types -- [✅] the program should respect the article type already set in the frontmatter and don't default to "Life" if the article is set to something else. -- [✅] the theme expects the article type to be lowercase. The program currently writes them as uppercase -### categories dropdown -- [✅] when adding a category to the article, offer a dropdown of the categories or autocomplete like we do for tags. - ## Settings - [ ] **Font size spinner in UI** — `font_size` is in config but only editable via `~/.config/my-publisher/config.toml`. Add a `QSpinBox` to `SetupDialog` (or a dedicated Settings dialog) so it's changeable without editing the file. Save via `Config.save()`, re-apply with `app.setFont()`. File: `ui/setup_dialog.py`. @@ -52,3 +40,16 @@ ## keyboard shortcuts - [✅] The app should follow some standard keyboard shortcuts like Ctrl+q should quit the app. + +## hugo server +- [✅] we should add a way to run `npm run build` before launching the hugo server. We also need to be able to run it using a button in the interface when the server is already running. + +## frontmatter +### taxonomies +- [✅] when modifying the frontmatter, the square brackets around the tags or categories lists are not maintained. +### article types +- [✅] the program should respect the article type already set in the frontmatter and don't default to "Life" if the article is set to something else. +- [✅] the theme expects the article type to be lowercase. The program currently writes them as uppercase +### categories dropdown +- [✅] when adding a category to the article, offer a dropdown of the categories or autocomplete like we do for tags. + diff --git a/core/config.py b/core/config.py index 68a8b87..6b8662c 100644 --- a/core/config.py +++ b/core/config.py @@ -12,10 +12,15 @@ class Config: typora_bin: str = "typora" typora_args: str = "" font_size: int = 10 + ollama_host: str = "" def is_complete(self) -> bool: return bool(self.blog_repo) + def resolved_ollama_host(self) -> str: + import os + return self.ollama_host or os.environ.get("OLLAMA_HOST", "http://localhost:11434") + def save(self, path: Path = DEFAULT_CONFIG_PATH) -> None: path.parent.mkdir(parents=True, exist_ok=True) doc = tomlkit.document() @@ -24,6 +29,7 @@ class Config: doc.add("typora_bin", self.typora_bin) doc.add("typora_args", self.typora_args) doc.add("font_size", self.font_size) + doc.add("ollama_host", self.ollama_host) path.write_text(tomlkit.dumps(doc)) @classmethod @@ -38,4 +44,5 @@ class Config: typora_bin=str(data.get("typora_bin", defaults.typora_bin)), typora_args=str(data.get("typora_args", defaults.typora_args)), font_size=int(data.get("font_size", defaults.font_size)), + ollama_host=str(data.get("ollama_host", defaults.ollama_host)), ) diff --git a/tests/test_config.py b/tests/test_config.py index f19bfdd..f46a224 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -23,3 +23,30 @@ def test_config_is_complete_false_when_blog_repo_empty(tmp_path): def test_config_is_complete_true_when_all_set(): cfg = Config(blog_repo="/some/repo", transart_script="/bin/x", typora_bin="typora") assert cfg.is_complete() is True + +def test_config_ollama_host_default(tmp_path): + path = tmp_path / "config.toml" + cfg = Config.load(path) + assert cfg.ollama_host == "" + +def test_config_ollama_host_round_trips(tmp_path): + path = tmp_path / "config.toml" + cfg = Config(blog_repo="/repo", ollama_host="http://1.2.3.4:11434") + cfg.save(path) + loaded = Config.load(path) + assert loaded.ollama_host == "http://1.2.3.4:11434" + +def test_resolved_ollama_host_uses_env_when_field_empty(monkeypatch): + monkeypatch.setenv("OLLAMA_HOST", "http://env-host:11434") + cfg = Config(ollama_host="") + assert cfg.resolved_ollama_host() == "http://env-host:11434" + +def test_resolved_ollama_host_prefers_field_over_env(monkeypatch): + monkeypatch.setenv("OLLAMA_HOST", "http://env-host:11434") + cfg = Config(ollama_host="http://config-host:11434") + assert cfg.resolved_ollama_host() == "http://config-host:11434" + +def test_resolved_ollama_host_fallback_default(monkeypatch): + monkeypatch.delenv("OLLAMA_HOST", raising=False) + cfg = Config(ollama_host="") + assert cfg.resolved_ollama_host() == "http://localhost:11434" diff --git a/tests/test_ollama_check_worker.py b/tests/test_ollama_check_worker.py new file mode 100644 index 0000000..3c575db --- /dev/null +++ b/tests/test_ollama_check_worker.py @@ -0,0 +1,11 @@ +from workers.ollama_check_worker import OllamaCheckWorker + + +def test_worker_appends_api_tags_path(): + w = OllamaCheckWorker("http://localhost:11434") + assert w._url == "http://localhost:11434/api/tags" + + +def test_worker_strips_trailing_slash(): + w = OllamaCheckWorker("http://localhost:11434/") + assert w._url == "http://localhost:11434/api/tags" diff --git a/ui/main_window.py b/ui/main_window.py index 8aa2762..83a2eed 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -105,6 +105,7 @@ class MainWindow(QMainWindow): self.config.transart_script, self.config.typora_bin, self.config.typora_args, + ollama_host=self.config.ollama_host, parent=self, ) self._translation_view.push_master.connect(lambda: self._do_git_push("master")) diff --git a/ui/translation_view.py b/ui/translation_view.py index cf72ce5..06767cb 100644 --- a/ui/translation_view.py +++ b/ui/translation_view.py @@ -1,14 +1,16 @@ from __future__ import annotations +import os import subprocess from pathlib import Path from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QPlainTextEdit, QTextBrowser, + QPushButton, QPlainTextEdit, QTextBrowser, QMessageBox, ) from PyQt6.QtCore import pyqtSignal from core.models import Article from core.frontmatter import parse_frontmatter from workers.translation_worker import TranslationWorker +from workers.ollama_check_worker import OllamaCheckWorker import re def _strip_shortcodes(text: str) -> str: @@ -18,13 +20,17 @@ class TranslationView(QWidget): push_master = pyqtSignal() publish = pyqtSignal() - def __init__(self, transart_script: str, typora_bin: str, typora_args: str = "", parent=None): + def __init__(self, transart_script: str, typora_bin: str, typora_args: str = "", + ollama_host: str = "", parent=None): super().__init__(parent) self._transart_script = transart_script self._typora_bin = typora_bin self._typora_args = typora_args + self._ollama_host = ollama_host self._article: Article | None = None + self._pending_article: Article | None = None self._worker: TranslationWorker | None = None + self._check_worker: OllamaCheckWorker | None = None self._output_path: str = "" self._build_ui() @@ -72,6 +78,8 @@ class TranslationView(QWidget): layout.addWidget(self._action_bar) def start_translation(self, article: Article): + if self._check_worker and self._check_worker.isRunning(): + return if self._worker is not None: self._worker.log_line.disconnect() self._worker.finished.disconnect() @@ -84,6 +92,29 @@ class TranslationView(QWidget): direction = "it-en" if article.lang == "it" else "en-it" self._header.setText(f"Traduzione: {article.slug} ({article.lang.upper()} → {('EN' if article.lang == 'it' else 'IT')})") + self._log.appendPlainText("Controllo backend ollama...") + self._pending_article = article + resolved = self._ollama_host or os.environ.get("OLLAMA_HOST", "http://localhost:11434") + self._check_worker = OllamaCheckWorker(resolved, parent=self) + self._check_worker.reachable.connect(self._on_backend_checked) + self._check_worker.start() + + def _on_backend_checked(self, ok: bool, url: str): + if not ok: + self._log.appendPlainText(f"[ERRORE] Backend non raggiungibile: {url}") + QMessageBox.warning( + self, + "Backend non disponibile", + f"Il backend transart non è raggiungibile a:\n{url}\n\nAttiva RunPod e riprova.", + ) + self._pending_article = None + return + if self._pending_article is not None: + self._start_worker(self._pending_article) + self._pending_article = None + + def _start_worker(self, article: Article): + direction = "it-en" if article.lang == "it" else "en-it" self._worker = TranslationWorker( Path(self._transart_script), article.path, diff --git a/workers/ollama_check_worker.py b/workers/ollama_check_worker.py new file mode 100644 index 0000000..5f47492 --- /dev/null +++ b/workers/ollama_check_worker.py @@ -0,0 +1,19 @@ +from __future__ import annotations +import urllib.request +from PyQt6.QtCore import QThread, pyqtSignal + + +class OllamaCheckWorker(QThread): + reachable = pyqtSignal(bool, str) # (ok, url_tried) + + def __init__(self, host_url: str, timeout: int = 4, parent=None): + super().__init__(parent) + self._url = host_url.rstrip("/") + "/api/tags" + self._timeout = timeout + + def run(self): + try: + urllib.request.urlopen(self._url, timeout=self._timeout) + self.reachable.emit(True, self._url) + except Exception: + self.reachable.emit(False, self._url) |
