Skip to content

Commit e85f797

Browse files
committed
fix: ux + logs sse
1 parent 2a3210a commit e85f797

10 files changed

Lines changed: 58 additions & 59 deletions

File tree

.github/workflows/deploy-api.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ jobs:
177177
WorkingDirectory=${APP_ROOT}
178178
EnvironmentFile=${APP_ROOT}/.env
179179
# Affichage virtuel : Chromium avec VINTED_BROWSER_HEADLESS=false a besoin d'un DISPLAY (xauth requis pour xvfb-run).
180-
ExecStart=/usr/bin/xvfb-run -a --server-args="-screen 0 1920x1080x24" ${APP_ROOT}/.venv/bin/uvicorn main:app --host 127.0.0.1 --port ${PORT} --workers 2 --proxy-headers
180+
ExecStart=/usr/bin/xvfb-run -a --server-args="-screen 0 1920x1080x24" ${APP_ROOT}/.venv/bin/uvicorn main:app --host 127.0.0.1 --port ${PORT} --workers 1 --proxy-headers
181181
Restart=always
182182
RestartSec=5
183183

api/routes/articles.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,12 @@ def start_ebay_batch(
216216
detail=f"Article {aid} needs at least one HTTPS image for eBay.",
217217
)
218218

219-
# Enregistre les canaux SSE **avant** de retourner 202 : `BackgroundTasks`
220-
# ne démarre qu'après l'envoi de la réponse, et le front ouvre
221-
# `GET /articles/{id}/listing-progress` dès réception → sans ces registers
222-
# upfront, l'ouverture du SSE gagne la course et on renvoie l'erreur
223-
# « Aucune session de publication pour cet article. » alors que la publication
224-
# va en fait se dérouler juste après sans logs visibles côté UI.
219+
# Register SSE channels **before** returning 202: FastAPI `BackgroundTasks`
220+
# only start after the response is sent, and the front-end opens
221+
# `GET /articles/{id}/listing-progress` as soon as it gets the response →
222+
# without these upfront registers, the SSE wins the race and we reply with
223+
# "Aucune session de publication pour cet article." even though the publish
224+
# is about to run right after, with no logs visible in the UI.
225225
for aid in unique_ids:
226226
vinted_progress_hub.register(aid)
227227

api/services/ebay_oauth_service.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
class EbayOAuthError(RuntimeError):
25-
"""Erreur OAuth eBay exploitable côté UI (code + message lisibles)."""
25+
"""User-facing eBay OAuth error (stable code + human-readable message)."""
2626

2727
def __init__(self, code: str, message: str, *, status: int | None = None) -> None:
2828
super().__init__(code)
@@ -35,8 +35,9 @@ def __str__(self) -> str:
3535

3636

3737
def _describe_ebay_oauth_error(status: int, body: str) -> EbayOAuthError:
38-
"""Transforme un body 4xx eBay (`{"error": "...", "error_description": "..."}`)
39-
en exception `RuntimeError` lisible, avec un code applicatif stable pour l'UI."""
38+
"""Turn an eBay 4xx body (`{"error": "...", "error_description": "..."}`)
39+
into a readable `RuntimeError` with a stable application code for the UI.
40+
User-facing messages are kept in French (shown verbatim in the app UI)."""
4041
payload: dict[str, Any] = {}
4142
try:
4243
parsed = json.loads(body) if body else {}
@@ -135,15 +136,14 @@ async def exchange_authorization_code(code: str, *, app: AppSettings | None = No
135136

136137

137138
async def refresh_user_access_token(refresh_token: str, *, app: AppSettings | None = None) -> dict[str, Any]:
138-
"""Rafraîchit l'access token utilisateur.
139-
140-
On ne force **volontairement pas** le paramètre ``scope`` : eBay renvoie alors
141-
un token portant les scopes tels qu'ils ont été consentis au moment de la
142-
connexion. C'est indispensable quand ``EBAY_SCOPES`` évolue (ajout de
143-
``sell.fulfillment`` par exemple) : les utilisateurs déjà connectés avant
144-
l'ajout continuent de publier (inventory + account) sans reconsentement ;
145-
les fonctionnalités nécessitant le nouveau scope détecteront son absence et
146-
proposeront la reconnexion.
139+
"""Refresh the user access token.
140+
141+
We intentionally do **not** send the ``scope`` parameter: eBay then returns
142+
a token with the scopes as originally consented. This is required when
143+
``EBAY_SCOPES`` evolves (e.g. when ``sell.fulfillment`` was added): users
144+
already connected before the change keep publishing (inventory + account)
145+
without re-consenting; features that need the new scope detect its absence
146+
and prompt the user to reconnect.
147147
"""
148148
s = app or get_settings()
149149
if not ebay_oauth_configured(s):

api/services/ebay_publish_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,10 @@ async def publish_article_to_ebay(
286286
await _emit_ebay(progress, "auth", "Connecting to the eBay API…")
287287
token = await ensure_ebay_access_token(db, user, app=s)
288288
except Exception as exc:
289-
# Inclut RuntimeError (ebay_not_connected, ebay_oauth_not_configured, …)
290-
# et EbayOAuthError (invalid_scope, invalid_grant, …). On ne laisse plus
291-
# fuir httpx.HTTPStatusError : le log « auth → error » doit toujours être
292-
# émis pour que le Journal des publications affiche la vraie raison.
289+
# Covers RuntimeError (ebay_not_connected, ebay_oauth_not_configured, …)
290+
# and EbayOAuthError (invalid_scope, invalid_grant, …). We must not let
291+
# httpx.HTTPStatusError escape: the "auth → error" log has to be emitted
292+
# so the publish journal always shows the real reason.
293293
logger.warning("eBay token acquisition failed for user=%s: %s", user.id, exc)
294294
detail = str(exc) or exc.__class__.__name__
295295
await _emit_ebay(progress, "error", "Could not obtain an eBay token.", detail=detail)

api/services/vinted_progress_session_service.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ async def finish(cls, article_id: int, payload: dict[str, Any]) -> None:
8080

8181
@classmethod
8282
async def event_stream(cls, article_id: int) -> AsyncIterator[dict[str, Any]]:
83-
# Petit filet de sécurité : si une route a enregistré la session via
84-
# `BackgroundTasks` (donc après le retour HTTP) ou si le front ouvre le
85-
# SSE très vite après un POST 202, on laisse jusqu'à ~1,5 s pour que
86-
# `register()` soit appelé. Au-delà on considère qu'il n'y a vraiment
87-
# pas de publication en cours pour cet article.
83+
# Small safety net: if a route registers the session via
84+
# `BackgroundTasks` (i.e. after the HTTP response) or if the front opens
85+
# the SSE right after a 202, allow up to ~1.5s for `register()` to be
86+
# called. After that we consider that no publish is actually running
87+
# for this article.
8888
s = cls._sessions.get(article_id)
8989
if s is None:
9090
for _ in range(15):

web/app/components/DesktopUpdaterPanel.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ async function installUpdate() {
104104
errorMessage.value = null
105105
resetDownloadProgress()
106106
107-
// Sur Windows, l'installeur NSIS réécrit `goupix-vinted-worker.exe` :
108-
// il faut impérativement tuer le sidecar (et ses enfants Chromium lancés
109-
// par nodriver) AVANT, sinon l'install échoue avec « Error opening file
110-
// for writing ». Le kill côté Rust propage un taskkill /F /T sur l'arbre.
107+
// On Windows, the NSIS installer overwrites `goupix-vinted-worker.exe`:
108+
// the sidecar (and its Chromium children spawned by nodriver) must be
109+
// killed BEFORE, otherwise the install fails with "Error opening file for
110+
// writing". The Rust side runs a `taskkill /F /T` on the whole tree.
111111
try {
112112
await invoke('stop_local_worker')
113113
} catch (killError) {
114-
console.warn('[Updater] stop_local_worker a échoué, on tente quand même l\'install :', killError)
114+
console.warn('[Updater] stop_local_worker failed, proceeding with install anyway:', killError)
115115
}
116-
// Laisse Windows libérer les handles sur le .exe et ses DLLs avant que
117-
// NSIS ne commence l'extraction.
116+
// Give Windows a moment to release handles on the .exe and its DLLs
117+
// before NSIS starts extracting.
118118
await new Promise((resolve) => setTimeout(resolve, 600))
119119
120120
await pendingUpdate.value.downloadAndInstall(onDownloadEvent)

web/app/components/articles/ArticleList.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ const UAvatar = resolveComponent('UAvatar')
379379

380380
<div
381381
v-if="selectedCount > 0"
382-
class="flex flex-col gap-2 rounded-lg border border-default bg-elevated/60 px-3 py-2.5 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between"
382+
class="sticky top-0 z-30 flex flex-col gap-2 rounded-lg border border-default bg-elevated/95 px-3 py-2.5 shadow-md shadow-black/5 backdrop-blur supports-[backdrop-filter]:bg-elevated/85 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between"
383383
>
384384
<p class="text-sm text-highlighted">
385385
{{ selectedCount }} article{{ selectedCount > 1 ? 's' : '' }} sélectionné{{ selectedCount > 1 ? 's' : '' }}
@@ -445,7 +445,7 @@ const UAvatar = resolveComponent('UAvatar')
445445
<table class="min-w-full text-sm border-separate border-spacing-0">
446446
<thead class="bg-elevated/60 text-left text-muted uppercase text-xs">
447447
<tr>
448-
<th class="w-10 px-3 py-2 font-medium align-middle border-b border-default">
448+
<th class="sticky left-0 z-20 w-10 px-3 py-2 font-medium align-middle border-b border-r border-default bg-elevated">
449449
<UCheckbox
450450
:model-value="allFilteredSelected"
451451
:indeterminate="someFilteredSelected && !allFilteredSelected"
@@ -502,9 +502,9 @@ const UAvatar = resolveComponent('UAvatar')
502502
<tr
503503
v-for="row in filtered"
504504
:key="row.id"
505-
class="border-t border-default hover:bg-elevated/40"
505+
class="group border-t border-default hover:bg-elevated/40"
506506
>
507-
<td class="px-3 py-3 align-middle border-b border-default">
507+
<td class="sticky left-0 z-10 px-3 py-3 align-middle border-b border-r border-default bg-default transition-colors group-hover:bg-elevated/40">
508508
<UCheckbox
509509
:model-value="isSelected(row.id)"
510510
:aria-label="`Sélectionner ${row.pokemon_name || row.title}`"

web/app/composables/useVintedPublishStream.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export function useVintedPublishStream() {
113113
const v = data.vinted
114114
const eb = data.ebay
115115
if (context === 'logs') {
116-
// Journal des publications : on matérialise les échecs dans la
117-
// liste de logs (sinon l'utilisateur ne voit qu'un `done` muet).
116+
// Publish journal: surface failures as log entries (otherwise
117+
// the user only sees a silent `done`).
118118
if (v && v.published === false) {
119119
const detail
120120
= v.detail === 'missing_vinted_credentials'

web/src-tauri/nsis-hooks.nsh

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
; Hooks NSIS personnalisés pour le bundle Tauri Windows.
1+
; Custom NSIS hooks for the Tauri Windows bundle.
22
;
3-
; Contexte : GoupixDex embarque un sidecar PyInstaller `goupix-vinted-worker.exe`
4-
; qui lance lui-même Chromium via nodriver. Pendant une mise à jour, NSIS doit
5-
; réécrire ce .exe ; s'il est encore en mémoire (ou si un des Chromium enfants
6-
; garde un handle), l'install échoue avec :
7-
; « Error opening file for writing: ...\goupix-vinted-worker.exe »
3+
; GoupixDex ships a PyInstaller sidecar `goupix-vinted-worker.exe` that itself
4+
; spawns Chromium through nodriver. During an update NSIS needs to overwrite
5+
; that .exe; if it (or one of its Chromium children) still holds a handle the
6+
; install fails with:
7+
; "Error opening file for writing: ...\goupix-vinted-worker.exe"
88
;
9-
; Côté app Tauri on appelle déjà la commande `stop_local_worker` avant
10-
; `downloadAndInstall`, mais ce hook joue le rôle de filet de sécurité
11-
; (install manuel depuis GitHub Releases, app crashée, anciens orphelins...).
9+
; The Tauri app already calls the `stop_local_worker` command before
10+
; `downloadAndInstall`, but this hook is the safety net (manual install from
11+
; GitHub Releases, crashed app, leftover orphans, ...).
1212

1313
!macro NSIS_HOOK_PREINSTALL
14-
DetailPrint "Arrêt du worker GoupixDex s'il est en cours d'exécution..."
15-
; /F = force, /T = tue aussi l'arborescence (Chromium lancés par nodriver).
16-
; On redirige la sortie vers nul pour rester silencieux si aucun process n'est trouvé.
14+
DetailPrint "Stopping GoupixDex worker if it is still running..."
15+
; /F = force, /T = kill the whole tree (Chromium spawned by nodriver).
16+
; Output redirected to nul so the installer stays silent if no process exists.
1717
nsExec::Exec 'cmd /C taskkill /F /T /IM goupix-vinted-worker.exe >nul 2>&1'
1818
Pop $0
1919
Sleep 500

web/src-tauri/src/lib.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,9 @@ fn check_browser_availability() -> BrowserInfo {
127127
#[cfg(target_os = "windows")]
128128
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
129129

130-
/// Tue le worker local et toute son arborescence de sous-processus (Chromium
131-
/// lancés par nodriver notamment). Indispensable avant un remplacement sur
132-
/// disque de `goupix-vinted-worker.exe` (mise à jour NSIS) pour ne pas se
133-
/// retrouver avec un fichier verrouillé.
130+
/// Kill the local worker and its whole child process tree (Chromium spawned by
131+
/// nodriver in particular). Required before overwriting
132+
/// `goupix-vinted-worker.exe` on disk (NSIS update) to avoid a locked file.
134133
fn kill_worker_tree(child: CommandChild) {
135134
#[cfg(target_os = "windows")]
136135
{
@@ -143,8 +142,8 @@ fn kill_worker_tree(child: CommandChild) {
143142
let _ = child.kill();
144143
}
145144

146-
/// Commande exposée au front-end pour fermer proprement le worker avant une
147-
/// mise à jour Tauri (voir `DesktopUpdaterPanel.vue`).
145+
/// Tauri command called from the front-end to shut the worker down cleanly
146+
/// before a Tauri update (see `DesktopUpdaterPanel.vue`).
148147
#[tauri::command]
149148
fn stop_local_worker(state: tauri::State<'_, VintedLocalChild>) -> Result<(), String> {
150149
let child_opt = match state.0.lock() {

0 commit comments

Comments
 (0)