Skip to content
NexusCorpSCPublic

About

Desktop app for Nexus Tools for Star Citizen.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

nexus-app

Desktop app for Nexus Tools for Star Citizen.

Client desktop (Tauri + React) pour Nexus Tools, la boîte à outils communautaire Star Citizen. L'application expose les fonctionnalités de Nexus Tools directement depuis le bureau : blueprints, objets du jeu, missions, réputations, inventaire et organisations.

Architecture

L'interface est une vraie application React — pas une webview du site. Elle consomme l'API REST de Nexus Tools.

Les appels réseau passent par @tauri-apps/plugin-http, qui exécute les requêtes côté Rust. Deux conséquences :

  • pas de CORS : l'API n'a besoin d'aucune configuration particulière pour accepter l'application desktop ;
  • cookie de session maîtrisé : la session better-auth est rejouée depuis le store persistant, sans dépendre du cookie jar de la webview.
src/
  lib/
    api-client.ts     couche HTTP (plugin-http, cookie de session, erreurs)
    api/              un module par domaine (blueprints, missions, reps, …)
    settings.ts       store persistant : URL de l'API + cookie de session
  auth/               contexte de session (connexion via Nexus Tools)
  components/         layout + primitives d'UI
  pages/              un écran par fonctionnalité
  types/nexus.ts      types miroir des réponses de l'API
src-tauri/            binaire Tauri, plugins et permissions

Fonctionnalités

La fenêtre s'ouvre sur l'accueil : les superpositions à lancer avant le jeu (recherche, escouade, carte, NPS, plan de vol, bloc-notes, cargo, Nexus Chat), chacune avec son raccourci, l'état de l'escouade, le début du bloc-notes et l'accès à la base de données. Le menu range les écrans en deux groupes — Base de données et Mon espace — et garde en bas deux boutons qui affichent les superpositions Escouade et Carte. Ces boutons, comme les tuiles de l'accueil, passent par la commande show_overlay : ils affichent la fenêtre, jamais ne la masquent, contrairement au raccourci qui bascule.

Écran Endpoints Session requise
Recherche /api/search non⁴
Blueprints /api/blueprints, /api/blueprints/:slug, …/categories non¹
↳ mes blueprints /api/blueprints/:id/ownership (POST, DELETE) oui
↳ reçu en jeu /api/blueprints/lookup?name= (voir « Game.log ») oui
↳ dans mon org /api/blueprints/:id/org-owners oui
Objets /api/items, /api/items/:slug, …/facets non
Missions /api/missions, /api/missions/:id, …/factions non
Factions /api/factions non
Réputations /api/reps, /api/reps/factions oui
Inventaire /api/inventory/items, /api/inventory/items/bulk, /api/inventory/locations oui
↳ colis /api/inventory/parcels, …/parcels/:code, …/parcels/:code/accept oui
Organisations /api/orgs, /api/orgs/:id/inventory partiellement²
↳ en jeu /api/orgs/:id/presence, /api/me/presence (GET, PUT, DELETE) oui
Feuille de cargo /api/cargo-ships (une fois, mise en cache) non⁵
Bloc-notes /api/notes non³
NPS /api/lieux/nps, /api/lieux/:slug/position (POST) pour enregistrer
Nexus Chat /api/chat (POST, en flux), /api/chat/status, /api/chat/access (POST), /api/chat/conversations, …/conversations/:id (GET, PATCH, DELETE) oui, et un accès ouvert

¹ le filtre « possédés » n'apparaît qu'une fois connecté, il est résolu côté serveur. ² la liste publique est accessible sans session ; l'inventaire partagé non. ³ connecté, ce sont les notes en ligne du compte ; sinon, des notes locales. ⁴ l'inventaire personnel n'entre dans les résultats qu'une fois connecté. Les objets du jeu y figurent avec leur type (arme, véhicule, ressource…) et s'ouvrent dans l'écran « Objets ». ⁵ la feuille elle-même ne quitte jamais la machine ; voir « Feuille de cargo ».

Le site va plus loin que cette application : boutiques, marché, commandes, industrie (fret, raffinage) et fiche d'organisation n'ont aucune route API et ne sont rendus que côté serveur. Les écrans correspondants demanderaient donc d'abord du travail dans nexus-tools ; d'ici là, la palette de recherche ouvre ces résultats-là dans le navigateur.

Les endpoints /api/me, /api/reps, /api/reps/factions, /api/orgs, /api/blueprints/:slug, /api/blueprints/:id/ownership et /api/blueprints/lookup ont été ajoutés à nexus-tools pour cette application : ces données n'étaient jusqu'ici disponibles qu'en rendu serveur, ou par une server action que seul le site sait appeler.

« Mes blueprints », depuis l'application

Trois endroits donnent la possibilité d'ajouter, parce que c'est trois moments différents : la grille, où l'on parcourt ; la fiche, où l'on décide ; et la liste de la palette de recherche, où l'on ne faisait que passer.

Retirer n'est offert que là où la possession est connue, c'est-à-dire dans la grille et sur la fiche. Un résultat de recherche n'en dit rien : proposer un retrait à l'aveugle reviendrait à parier. La palette garde donc son seul +, que la route idempotente rend sans conséquence — added: false veut dire « il y était déjà », ce qui suffit à répondre juste sans poser une deuxième question.

Dans la grille, un blueprint possédé porte un contrôle plutôt qu'un badge : une coche qui devient un moins au survol. Une carte a la place d'un seul élément dans ce coin, et un badge à côté d'un bouton dirait deux fois la même chose.

Un blueprint par défaut est possédé par tout le monde. C'est le seul cas où la carte garde son badge « Possédé » sans bouton : il n'y a rien à ajouter, et rien à reprendre. Le site le sait par isDefault, que la liste et la fiche renvoient à côté de owned — et pour la même raison, seulement à un appelant connecté.

Le changement est fait dans la fenêtre où l'on a cliqué, et la palette est une fenêtre à part. Comme le client de requêtes ne rafraîchit pas au retour du focus, un ajout depuis la palette laisserait la fenêtre principale afficher « non possédé » indéfiniment : il est donc annoncé aux autres fenêtres, comme l'est une modification de la feuille de cargo. L'annonce part quoi qu'ait répondu la route, « c'était déjà le cas » compris — elle se lit « quelqu'un vient d'affirmer une possession, relisez », et une fenêtre qui affiche le contraire est précisément celle qu'il faut prévenir.

Game.log

L'application lit le Game.log de Star Citizen pendant que l'on joue (src-tauri/src/game_log.rs). Pour l'instant, une seule ligne l'intéresse : celle que le jeu écrit quand le HUD annonce « Received Blueprint ».

<2026-09-28T12:00:00.000Z> [Notice] <SHUDEvent_OnNotification> Added notification "Received Blueprint: FS-9 LMG: " …

Le nom est retrouvé sur Nexus par /api/blueprints/lookup?name=, une correspondance exacte (casse et espaces mis à part) : une recherche approchée pourrait ajouter le mauvais blueprint à un compte. Ensuite :

  • déjà possédé → rien ;
  • inconnu de Nexus → une notification le signale ;
  • sinon → une notification propose Ajouter ; la fermer vaut « non ». Rien n'est ajouté sans ce clic.

Le fichier est suivi, pas relu : ce qu'il contient au lancement de l'application est ignoré, pour ne pas rejouer toute la soirée. Un fichier qui apparaît ou qui est recréé ensuite — le jeu en commence un nouveau à chaque lancement et range l'ancien dans logbackups/ — est lu depuis sa première ligne. Les anciens journaux ne sont pas parcourus.

Le dossier du jeu se règle dans Paramètres › Jeu, et l'analyse peut y être coupée. Par défaut : C:\Program Files\Roberts Space Industries\StarCitizen\LIVE. Rust lit ces réglages dans le même settings.json que le frontend (gameLogDir, gameLogEnabled) ; la fenêtre principale lui demande de les suivre au démarrage et à chaque enregistrement (game_log_sync).

La lecture se fait par sondage, une fois par seconde, plutôt que par notification du système : le jeu garde le fichier ouvert en écriture toute la session, et Windows ne signale pas de manière fiable ce qui change dans un fichier tenu ainsi.

NPS (Nexus Positioning System)

La superposition NPS (Ctrl+Shift+K) dit à quelle distance est un lieu et dans quelle direction aller. Le jeu ne donne qu'une position, jamais d'orientation : la commande /showlocation, tapée dans le chat, copie Coordinates: x:… y:… z:… (mètres, repère du système) dans le presse-papiers.

  • Le bouton Actualiser ma position met /showlocation dans le presse-papiers ; il reste à le coller dans le chat. La réponse du jeu est repérée toute seule (src-tauri/src/nps.rs), horodatée au dixième de seconde. Le presse-papiers n'est lu que fenêtre NPS affichée, et seulement quand son contenu change (GetClipboardSequenceNumber).
  • Les planètes tournent : un lieu posé sur un corps est rangé dans le repère de ce corps, rotation annulée, et la géométrie (src/lib/nps.ts) le replace dans le repère du système à l'instant du relevé. Le modèle de rotation vient de l'outil communautaire de Valalol (MIT) ; les paramètres des corps (Stanton, Pyro, Nyx) de starmap.space, posés côté site par npm run import:celestial-bodies.
  • Chaque système a son repère, centré sur son étoile : la fenêtre retient le dernier système où un corps a été reconnu, ne guide que vers les lieux de ce système et refuse d'enregistrer une position pour un lieu d'un autre.
  • Sur le même corps, la fenêtre donne la distance au sol, le cap à suivre (0° = nord) et, dès deux relevés distants d'au moins 20 m, de combien tourner par rapport à la trajectoire. Dans l'espace, l'écart avec la trajectoire, sans côté.
  • Les lieux n'ont pas de coordonnées au départ : un joueur sur place enregistre sa position pour un lieu, envoyée comme contribution (placeEdit, publiée d'emblée au niveau 3, relue en dessous).

La fiche d'un lieu relevé propose Y aller (NPS), qui en fait la destination et affiche la superposition.

Nexus Chat

L'assistant du site (Claude), dans l'écran Nexus Chat et en superposition (Ctrl+Maj+H). Il cherche dans Nexus Tools et agit sur le compte du joueur avec les outils du serveur MCP du site ; toute écriture s'affiche d'abord en carte Confirmer / Refuser. Tout se passe côté site : l'accès (ouvert par un administrateur), le budget mensuel, le modèle et l'historique des conversations. L'application n'envoie que le nouveau message, ou la réponse à une confirmation, et lit la réponse en flux par le plugin HTTP (chatFetch, src/lib/api/chat.ts), avec la session persistée.

  • La superposition reprend la dernière conversation, d'ici, de la fenêtre principale ou du site, chaque fois qu'elle s'affiche ; elle prend le focus, le curseur dans la saisie, et Échap la range pour rendre le clavier au jeu. Elle ne parle au site qu'affichée.
  • Une conversation continuée ailleurs est relue quand la fenêtre revient au premier plan, jamais pendant une réponse en cours (src/hooks/use-active-chat.ts).
  • Le bouton Ouvrir dans la fenêtre principale y ouvre la même conversation (/chat?c=).

Icône de notification

Tant que l'application tourne, elle tient une icône dans la zone de notification. Clic gauche : la fenêtre principale revient, qu'elle ait été minimisée ou rangée. Clic droit : recherche rapide, capture de zone, bloc-notes, et Quitter.

Fermer la fenêtre principale la range au lieu de la détruire — sans quoi l'icône n'aurait plus rien à rouvrir, et l'application continue de toute façon de tourner pour ses raccourcis. Quitter, dans le menu de l'icône, est donc le seul chemin qui termine réellement le processus.

Les trois commandes du menu passent par le même point d'entrée que les raccourcis : elles sont journalisées de la même manière ((tray) au lieu de (raw input)), et le bloc-notes bascule au lieu de seulement s'ouvrir.

Une icône que le système refuserait n'empêche pas le démarrage : c'est journalisé, et l'application se lance.

L'icône est le logo Nexus Corp, celui du site (nexus-tools/public/nexus_logo_square.png), décliné par tauri icon dans src-tauri/icons/. Le lanceur, la fenêtre et les installeurs le prennent de là. La zone de notification, elle, reçoit le PNG 128 px plutôt que l'icône de fenêtre en 32 px : elle réclame entre 16 et 32 pixels selon l'écran, et une source large se réduit mieux qu'une petite ne s'agrandit. C'est ce qui vaut au crate la fonctionnalité image-png.

Superposition et capture d'écran

Des raccourcis globaux, enregistrés côté Rust pour rester actifs quand l'application est minimisée ou n'a pas le focus :

Raccourci par défaut Effet
Ctrl+Maj+B ouvre la palette de recherche en superposition
Ctrl+Maj+S ouvre la capture de zone, dont le texte alimente la palette
Ctrl+Maj+N affiche ou masque le bloc-notes en superposition
Ctrl+Maj+G affiche ou masque la feuille de cargo en superposition
Ctrl+Maj+E affiche ou masque l'escouade en superposition
Ctrl+Maj+H affiche ou masque Nexus Chat en superposition
Ctrl+Maj+O efface les trois superpositions, ou leur rend leur panneau
Ctrl+Maj+L verrouille les superpositions affichées, ou les déverrouille
Alt+V (maintenu) affiche le menu radial rapide tant qu'il est maintenu

Ils se redéfinissent dans Paramètres, en appuyant sur la combinaison voulue. Au moins un modificateur est exigé : un raccourci global sans modificateur confisquerait la touche à toutes les applications.

Les combinaisons sont stockées au format du plugin (Ctrl+Shift+KeyB), dont les noms de touches correspondent à KeyboardEvent.code — ce que le navigateur enregistre se transmet donc tel quel.

Deux chemins guettent les combinaisons, et il en suffit d'un.

RegisterHotKey, via le plugin global-shortcut, est le seul qui puisse confisquer la combinaison à l'application en dessous. Mais il n'est pas délivré tant qu'un jeu tient le clavier : Star Citizen le prend pour lui et la combinaison n'arrive jamais jusqu'ici — c'est-à-dire précisément quand ces raccourcis servent.

Un écouteur Raw Input (src-tauri/src/hotkeys.rs) comble ce trou. La couche périphérique rapporte chaque frappe à qui l'a demandée, focus ou pas, et sans aucun enregistrement préalable : une combinaison déjà prise par une autre application nous parvient donc quand même. C'est l'approche des overlays de la communauté autour du jeu — l'écouteur lit, il n'accroche aucun hook et n'injecte rien dans un autre processus, il n'y a donc rien qui puisse inquiéter un anti-triche. En contrepartie il ne peut pas avaler la frappe : le jeu reçoit aussi la combinaison. Cela ne coûte rien en pratique, puisque ce chemin ne sert que là où RegisterHotKey n'a pas fonctionné, donc là où le jeu recevait déjà les touches.

Seule une combinaison illisible, ou demandée deux fois, est donc rapportée comme refusée dans Paramètres. Le choix reste enregistré même refusé, il suffit d'en saisir un autre.

Rien de tout cela ne peut empêcher le démarrage : ni l'initialisation du plugin, ni l'enregistrement d'une combinaison. Un plugin qui n'a pas démarré coûte l'exclusivité de la combinaison, pas le raccourci lui-même — Raw Input continue de le rapporter.

Chaque déclenchement est écrit dans le journal avec sa provenance (shortcut notes fired (raw input)), ce qui répond du premier coup d'œil à la seule question qui compte quand un raccourci ne réagit pas : est-il arrivé jusqu'à l'application ?

Les diagnostics partent dans <données de l'app>/logs/nexus-app.log, car les builds de release sont liés avec windows_subsystem = "windows" et n'ont donc aucune console où écrire. Les panics y sont écrits également, avec le thread, le fichier:ligne:colonne et une backtrace : un panic levé sur la boucle de messages ne peut pas dérouler et abrège le processus sur-le-champ, sans rien laisser d'autre qu'un minidump. Avant que l'application ne soit construite, le fichier est celui de %LOCALAPPDATA%\services.nexus.app\logs, ce qui couvre aussi les échecs de démarrage.

La palette est une fenêtre transparente et sans décoration, toujours au-dessus. Elle interroge /api/search, la recherche générale du site : blueprints, missions, factions, articles en vente, boutiques, organisations, vaisseaux de fret, et l'inventaire de l'utilisateur quand il est connecté. Une seule liste, classée par pertinence, chaque résultat portant son type et l'adresse de la page qui le montre. En dessous de deux caractères l'API répond 400 : la palette attend donc, plutôt que d'afficher une erreur sur un mot à moitié tapé.

Cette adresse est celle du site, et cette application n'en couvre qu'une partie. src/lib/search.ts tient la table des écrans qu'elle a — un blueprint, une mission, une faction, l'inventaire — et ouvre tout le reste dans le navigateur, à l'adresse de l'instance configurée. Sans cette table, un résultat sans écran ici atterrirait sur une route inexistante, que le routeur transforme en silence en liste de blueprints. Une petite flèche sur la ligne annonce lesquels sortent de l'application.

Le piège de la table : /missions/factions/<id> est une faction, pas une mission. Son motif passe donc avant celui des missions, qui n'accepte qu'un seul segment.

La capture suit cet enchaînement :

  1. le raccourci fige l'écran sous le curseur (xcap) avant d'afficher quoi que ce soit — la surface de sélection ne peut donc pas se retrouver dans sa propre capture, et le tracé se fait sur une image stable ;
  2. la fenêtre de sélection couvre exactement ce moniteur ;
  3. au relâchement, la zone est découpée, agrandie puis lue par Windows.Media.Ocr, le moteur fourni avec le système — rien à embarquer, contrairement à Tesseract qui demanderait un binaire et ses données d'entraînement ;
  4. le texte reconnu remplit la barre de recherche.

Deux détails pèsent lourd sur ce que le moteur rend. La zone est agrandie avant d'être lue — jusqu'à 3×, tant que le résultat reste raisonnable — parce qu'il est entraîné sur du texte de document et qu'à sa taille native un ATH de jeu lui fait confondre les glyphes, voire avaler les mots courts. Et le résultat est repris ligne par ligne plutôt que par OcrResult::Text(), qui recolle tout avec une simple espace : un journal de mission lu ainsi revient en une seule phrase interminable, où plus rien ne sépare les objectifs.

Le frontend envoie la sélection en fractions de la fenêtre (0..1) plutôt qu'en pixels : la mise à l'échelle DPI disparaît du protocole et les valeurs se projettent directement sur l'image capturée, quelle que soit sa définition.

Limites connues : la capture porte sur le moniteur sous le curseur, et la qualité de l'OCR dépend du module linguistique installé dans Windows.

Vérifier le code Windows sans machine Windows

xcap ne compile pas sur Linux et la chaîne Tauri ne se contrôle pas en compilation croisée (il lui manque lib.exe). En revanche src-tauri/src/capture.rs ne dépend que de serde, xcap et windows : recopié dans une crate jetable ciblant x86_64-pc-windows-msvc, il se type-checke en local, appels WinRT compris. C'est nettement plus rapide que d'attendre la CI.

Factions et possession dans l'organisation

Deux écrans qui répondent à des questions que les listes ne savaient pas poser :

  • Factions (/api/factions) : quelle faction récompense quel blueprint. La route rend tout d'un coup, sans filtre — la recherche de l'écran filtre donc ce qui est déjà là, sur les noms de faction et de blueprint, puisque « où est-ce que j'obtiens ça » est la question posée. La palette de recherche y renvoie avec un identifiant, et la faction visée est alors mise en évidence et amenée à l'écran.
  • Dans mon organisation, sur la fiche d'un blueprint (/api/blueprints/:id/org-owners) : qui, parmi les membres, le possède déjà — autrement dit à qui le demander plutôt que de le refarmer. La route refuse (403) toute organisation dont l'appelant n'est pas membre, et la liste proposée est justement celle de ses organisations.

Ce qui n'a pas pu suivre : un filtre par statistique sur les blueprints. /api/blueprints/stat-names n'est qu'une autocomplétion pour le formulaire de création côté site — /api/blueprints n'accepte aucun paramètre de statistique, il n'y a donc rien à filtrer tant que la route n'en prend pas.

Feuille de cargo

Le portage de /industry/cargo : on y répartit un volume en conteneurs SCU (32, 24, 16, 8, 4, 2, 1) et on suit le remplissage du vaisseau. Ctrl+Maj+G l'affiche en superposition, comme le bloc-notes.

Hors ligne, et pas seulement « ça marche hors ligne » : la feuille vit dans le store local, personne d'autre ne la lit, rien ne la synchronise. Le seul appel réseau de la fonctionnalité est la liste des vaisseaux (GET /api/cargo-ships, ajoutée à nexus-tools pour l'occasion), lue une fois et gardée en cache — l'écran dit d'où elle vient quand ce n'est pas du réseau.

Le vaisseau choisi est recopié dans la feuille, nom et capacité compris. Deux conséquences voulues : la superposition n'a besoin de rien d'autre que la feuille pour tracer sa jauge — ni liste, ni réseau — et une capacité modifiée sur le site en cours de route ne déplace pas la cible sous les pieds du pilote.

Modifier la feuille sans quitter le jeu

La superposition s'ouvre en lecture, parce que c'est ce qu'on lui demande en vol : la liste et la capacité restante, et rien à cliquer par erreur au-dessus d'un jeu. Les contrôles sont derrière un « Modifier » explicite, et ce sont les mêmes que ceux de l'écran principal — ajouter une ligne, la réécrire, la déplacer d'une mission à l'autre, renommer ou supprimer un bloc mission entier, ouvrir la mission suivante, changer de plus gros conteneur, clôturer.

Renommer un bloc a deux conséquences assumées. Renommer vers un nom déjà pris fusionne les deux blocs : la feuille regroupe par nom, pas par identité, c'est donc déjà ce qu'elle affichait. Et renommer le bloc en cours de remplissage fait avancer le compteur — le numéro qu'il portait est libre, et une ligne ajoutée ensuite doit ouvrir un nouveau bloc plutôt que ressusciter le nom dont on vient de le sortir.

Une exception : le vaisseau ne se change pas depuis la superposition. C'est la seule chose qui demanderait la liste, donc le réseau, alors que la feuille est justement faite pour s'en passer.

Deux détails du même souci :

  • la clôture demande deux fois plutôt qu'une, sans boîte de dialogue native — celle-ci passerait devant un jeu en plein écran, ce qui coûte plus cher que ce qu'elle protège ;
  • Échap quitte d'abord les contrôles, et ne ferme la fenêtre qu'ensuite : la même touche ne doit pas jeter ce qu'on était en train de taper.

Les deux écrans dessinent le même composant (src/components/cargo/sheet-view.tsx), chaque contrôle n'étant tracé que si son gestionnaire est fourni. La version compact est la superposition : mêmes champs, étiquettes descendues dans les libellés indicatifs, faute de largeur.

Une capture d'écran qui devient du cargo

Le journal de mission du jeu a une forme fixe, répétée par livraison :

Deliver 0/32 SCU of Titanium to Port Olisar above Crusader.
   Collect Titanium from Area18.

Quand le texte reconnu par la capture de zone correspond à ce modèle, il ne part pas dans la recherche : la palette devient un import de cargo.

  • Une feuille existe : les lignes y sont ajoutées, sans rien demander. La ligne rejoint la mission en cours, ce qui fait qu'une deuxième capture du même contrat atterrit dans le même bloc.
  • Aucune feuille : la seule chose que l'application ne peut pas deviner est le vaisseau. Elle le demande, crée la feuille et y met la capture.

Le parseur (src/lib/mission-objectives.ts) est recopié du site et reste tolérant, parce que l'OCR l'est peu :

  • le 0/ revient en O/, et les chiffres passent pour les lettres qui leur ressemblent — O/l6 SCU se lit bien 16 ;
  • la puce en losange devient ©, * ou une simple lettre O, qu'aucun nettoyage de ponctuation n'attrape ;
  • le l minuscule remonte en I majuscule (Audio-VisuaI Equipment) ;
  • la station est « above » sa planète, ou « on » quand la livraison se pose au sol — Area18 on ArcCorp ;
  • le verbe Deliver disparaît parfois : la forme 0/3 SCU of … to … porte alors l'objectif à elle seule ;
  • une ligne longue est coupée en deux par le jeu, elle est donc recollée avant d'être lue ; à l'inverse, une capture qui revient d'un bloc est redécoupée sur les mots qui ouvrent un objectif.

La ligne Collect nomme la ressource une seconde fois : elle s'en sert pour retrouver sa livraison, même quand celle du dessus a été manquée ou que le journal a été lu dans le désordre.

Le même texte se colle à la main dans l'écran, qui accepte aussi le format tabulé Destination;Contenu;Volume;Emplacement.

Une capture d'écran qui remplit l'inventaire

Le panneau d'un ordre de travail terminé de la raffinerie liste un lot par ligne — matériau, qualité sur 1000, rendement en cSCU — sous l'en-tête MATERIALS YIELDED (CSCU) · QUALITY · YIELD, et finit par le total YIELD.

Quand la capture de zone lit ce panneau, elle ne part pas non plus dans la recherche : la palette se range et la fenêtre principale s'ouvre sur l'ajout en masse de l'inventaire, les lots déjà dans le tableau en cSCU. Il reste à choisir le lieu et à relire les chiffres face au jeu — un bandeau rappelle la somme des lignes et le total affiché par le jeu, qui doivent coïncider. Un rendement que l'OCR n'a pas pu lire laisse sa ligne en erreur plutôt que de compter 1.

La palette ne voyant que du texte, les lots voyagent dans la route (/inventory/quick-add?workOrder=…, voir openMainRoute) ; la page les lit une fois, puis les retire de l'adresse.

Le parseur (src/lib/refinery-work-order.ts) est recopié du site, qui lit le même panneau depuis une capture collée. Il ne retient une capture comme ordre de travail qu'avec un matériau connu et l'en-tête ou le total : « Iron 325 » reste une recherche. Il rapproche les noms mal lus des matériaux connus (RON → Iron), relit les chiffres sosies (lSO → 150), et, propre à Windows OCR, rattache à leurs matériaux les nombres lus sur des lignes à part, qu'ils reviennent ligne par ligne ou colonne par colonne.

Colis entre joueurs

Le même échange que sur le site. Dans l'inventaire, l'icône de colis au pied d'une carte met une quantité du lot choisi dans le colis (panneau à droite ; pas plus que ce qui n'est pas déjà promis ailleurs). Envoyer le colis le scelle : l'API rend un code de 8 caractères A-Z0-9, affiché K7QM-2X9A, à donner au destinataire.

Rien ne bouge encore : les quantités sont réservées (icône d'avion en papier sur le lot) jusqu'à ce que le destinataire accepte, que l'expéditeur annule, ou 24 h. Recevoir un colis montre, dès que le code est complet, l'expéditeur et le contenu ; on choisit où ranger et on accepte — l'API retire les lots d'un inventaire et les ajoute à l'autre en une transaction, un lot identique au même lieu étant complété. #/inventory/parcels suit les envois et les réceptions.

Les refus arrivent en codes stables (not_found, expired, unavailable…) que src/lib/api/parcels.ts met en phrases.

Bloc-notes

Le même bloc-notes que le site, dans une fenêtre indépendante toujours au-dessus pour rester lisible en jeu. Ctrl+Maj+N l'affiche et le masque ; contrairement à la palette de recherche, il ne se referme pas quand il perd le focus.

La fenêtre n'ayant pas de décorations, son en-tête tient lieu de barre de titre : il porte data-tauri-drag-region, ce qui déplace la fenêtre. La permission qui l'autorise, core:window:allow-start-dragging, ne fait pas partie de core:default — d'où la capacité dédiée src-tauri/capabilities/notes-overlay.json, limitée à cette fenêtre. Sans elle le glissement est refusé sans un mot.

Connecté, l'éditeur lit et écrit /api/notes, donc les notes sont les mêmes que sur le site. Déconnecté, il écrit dans le store local. Les deux ne fusionnent pas : la session décide simplement lequel s'applique, et le cache React Query est indexé là-dessus pour qu'une connexion échange les notes affichées.

Chaque fenêtre a sa propre copie du contexte d'authentification, et les superpositions sont créées au démarrage : celle du bloc-notes garderait donc indéfiniment la session vue à ce moment-là — aucune — et continuerait d'afficher le bloc-notes local à quelqu'un qui vient de se connecter. Une connexion ou une déconnexion est donc diffusée à toutes les fenêtres (auth://session-changed), qui relisent la session du store ; la fenêtre à l'origine du changement ignore sa propre diffusion, sinon elle repasserait par son écran de chargement pour rien.

Une note écrite ailleurs — la fenêtre principale, le site, un autre appareil — arrive par le flux d'événements (voir plus bas) : Rust la remet à toutes les fenêtres (note://changed), et l'éditeur l'adopte, sauf s'il tient un brouillon non enregistré ou une sauvegarde en cours — ceux-là ne disparaissent pas sous les doigts de l'utilisateur. Sa propre sauvegarde lui revient par le même chemin, identique à ce qu'il vient de poser, et ne change rien. Tant que le flux n'est pas connecté, la superposition relit la note quand elle reprend le focus, comme elle l'a toujours fait.

L'enregistrement est automatique, 1,2 s après la dernière frappe. Les écritures peuvent se chevaucher (minuterie, bouton, fermeture de la fenêtre) et les réponses revenir dans le désordre : seule la requête la plus récente met l'écran à jour.

En jeu

Chacun déclare lui-même qu'il joue, et s'il le souhaite ce qu'il fait — rien ne le devine. Depuis le pied du menu : le statut sous le pseudo (« En jeu · Minage », ou « Hors jeu ») ouvre la déclaration, avec des suggestions d'activité et un champ libre. La page d'une organisation montre ses membres en jeu, leur activité et depuis combien de temps, relue toutes les 30 secondes.

C'est la même déclaration que sur le site : faite ici, elle s'y voit, et inversement. Elle s'éteint d'elle-même au bout de quatre heures, pour que quelqu'un qui ferme le jeu sans le dire ne reste pas « en jeu » ; la fenêtre principale la renouvelle toutes les 20 minutes tant qu'elle tourne. Seuls les membres d'une organisation voient qui y joue, même quand elle est publique.

Escouade

Ctrl+Maj+E affiche ou masque l'escouade en superposition : qui est prêt, qui est à terre, qui tient quel rôle, et l'annonce du chef — lisible au travers du cockpit, puisque cette fenêtre-là s'ouvre sans panneau. Tout ce qui touche à l'escouade se fait depuis cette fenêtre : la créer, la rejoindre par code, la quitter, ses rôles, et le raid qui en regroupe plusieurs sous une même annonce.

Dès qu'il y a une escouade, l'en-tête porte aussi un bouton qui affiche le plan de vol de l'escouade.

Dans un raid, l'en-tête porte un commutateur à deux positions, Escouade et Raid, à la place de l'icône : la position allumée est la vue affichée, ce qu'un seul bouton qui basculait ne disait pas. Le bouton Gérer le raid ouvre la feuille qui le compose : lier une escouade par son code, en retirer une, et en ouvrir une nouvelle.

Ouvrir une escouade depuis le raid la crée avec son organisateur pour seul membre et pour chef — une escouade vide n'existe pas, le dernier à en sortir la supprime — sans le faire sortir de la sienne. C'est la seule façon d'être dans plusieurs escouades à la fois, et elle sert à préparer un raid : Bravo et Charlie existent avant qu'on y soit, leurs codes partent, et le commandement passe à qui de droit une fois arrivé. La superposition regarde alors une de ces escouades — un sélecteur à côté du titre change laquelle — et sur la vue de raid, toutes celles dont on est membre se manipulent, pas seulement celle qui est affichée. Côté API, chaque route accepte ?squad=<id> pour dire laquelle elle vise ; sans lui, c'est la plus ancienne des appartenances, donc la seule pour tout le monde sauf l'organisateur.

La vue elle-même n'est plus demandée toutes les deux secondes : elle arrive par le flux d'événements (voir plus bas). Un point dans l'en-tête dit où en est ce flux — vert, la vue est en direct ; ambre, la connexion se rétablit.

Ready check. Le chef d'une escouade ou un de ses lieutenants demande à l'escouade de se déclarer prête ; celui qui commande l'escouade meneuse le demande à tout le raid. Tout le monde repasse « non prêt » — le demandeur compris — et reçoit une notification avec un bouton Prêt, en grand, en haut de l'écran au milieu (voir « Notifications »). Le bouton est pressé dans la fenêtre des bannières, qui n'a ni session ni réseau : il diffuse squad://ready, et la superposition d'escouade, qui a les deux, écrit la ligne du lecteur. Le marqueur (readyCheck, un identifiant frais) arrive par le flux d'événements comme le reste de la vue ; un identifiant inconnu, c'est une notification. Son propre ready check n'en produit pas : la réponse de l'écriture est posée dans le cache avant le push qui la répète.

Une annonce envoyée part en notification, en grand, en haut de l'écran au milieu. Celle de l'escouade comme celle du raid : ce sont les deux textes que le chef écrit pour tout le monde, et les deux que personne ne voit tant que la superposition est cachée derrière le jeu — ce qui est justement le moment où la fenêtre des bannières, elle, reste visible. La comparaison se fait avec ce que l'écran montrait : le premier push reçu pour une escouade — au lancement, ou après être passé sur elle — est un instantané, pas un changement, et n'annonce rien, même si le cache gardait cette escouade telle qu'elle était une heure plus tôt ; une annonce effacée n'annonce rien non plus, et ses propres modifications ne reviennent pas en toast — la supposition optimiste et la réponse de l'écriture sont posées dans le cache avant le push qui les répète.

Une annonce part quand on l'envoie, pas pendant qu'on l'écrit : Entrée ou Envoyer, Maj+Entrée pour aller à la ligne, Échap ou Annuler pour la laisser telle qu'elle était. Enregistrée à chaque pause de la frappe, elle partait en autant de notifications — « RDV », « RDV Grim », « RDV Grim Hex ». Une fois envoyée, la cloche à côté la renvoie telle quelle : le serveur horodate chaque envoi (announcedAt), et c'est ce tampon, pas le texte, que la superposition compare — un même texte envoyé deux fois sonne deux fois. Face à un serveur qui ne l'horodate pas encore, seul le texte compte, et renvoyer ne sonne pas.

Sur la vue de raid, l'annonce de chaque escouade s'affiche sous son nom, et celles qu'on commande s'y écrivent sans passer par la vue d'escouade. Le chef d'une escouade passe la main depuis le ⋯ d'une ligne, Passer chef.

Les menus d'une ligne (rôle, ⋯) s'ouvrent par-dessus la fenêtre entière, pas dans la liste qui les porte : sur la vue de raid, chaque escouade est sa propre liste, et le menu du dernier membre s'ouvrait sous son bord, coupé.

Glisser une escouade d'une place à l'autre sur la vue de raid demande "dragDropEnabled": false sur la fenêtre squad (tauri.conf.json) : sous Windows, le glisser-déposer de fichiers que Tauri intercepte par défaut confisque les événements HTML5 de la webview, et rien ne se déposait jamais.

Menu radial rapide

Maintenir Alt+V affiche un menu radial au centre de l'écran où se trouve le curseur ; le relâcher lance l'action du secteur visé :

Secteur Position Proposé
Nexus (fenêtre principale) haut toujours — ramène la fenêtre principale
Éliminé / Actif droite dans une escouade
Verrouiller / Déverrouiller bas droite toujours — comme Ctrl+Maj+L
Capture de zone bas gauche toujours — comme Ctrl+Maj+S
READY / NOT READY gauche dans une escouade, grisé si éliminé

Fenêtre principale, verrou et capture gardent leur place hors escouade : le geste est le même dans les deux cas. Le menu est une fenêtre à part, jamais verrouillée : il reste utilisable par-dessus des superpositions verrouillées, y compris pour les déverrouiller.

Relâcher au centre, sur un secteur grisé, ou appuyer sur Échap annule.

Viser sans curseur. En jeu le curseur est verrouillé et caché : le menu se vise au mouvement de la souris, pas à sa position. Pendant qu'il est affiché, l'écouteur Raw Input (src-tauri/src/hotkeys.rs) lit aussi la souris, focus ou pas, et chaque déplacement pousse un pointeur depuis le centre ; il bute sur le bord du cercle, si bien que changer d'avis coûte toujours le même petit coup de poignet. La souris n'est écoutée que menu ouvert — son flux, jusqu'à plusieurs milliers de rapports par seconde, réveillerait sinon le thread pour rien. Hors Windows, le trajet du curseur depuis l'appui en tient lieu.

Ni focus, ni clic. La fenêtre (radial) ne peut pas prendre le focus et laisse passer la souris : le jeu garde le clavier et la souris pendant tout le geste. En contrepartie, comme pour les raccourcis, le jeu reçoit aussi les frappes — Échap compris.

Relâcher, de façon certaine. Le relâchement arrive par Raw Input et par le plugin, mais une touche relâchée pendant un changement d'application peut échapper aux deux ; le menu interroge donc aussi l'état du clavier à chaque image et se ferme dès que la combinaison n'est plus tenue. Un menu resté ouvert sans personne pour le tenir resterait sinon au-dessus du jeu.

Qui fait quoi (src-tauri/src/radial.rs, src/pages/radial-overlay-page.tsx) : Rust ouvre, déplace le pointeur et ferme ; la page dessine et décide du secteur. Elle n'a ni session ni réseau : ce qu'elle sait de l'escouade, c'est la fenêtre Escouade qui le lui dit (useRadialBridge), et c'est à elle qu'elle rend les deux actions d'escouade — écrites par la même mutation que les boutons de la ligne de membre, donc affichées aussitôt et confirmées par une notification. La capture passe par la commande du raccourci de capture, la fenêtre principale par radial_open_main, qui la réaffiche et la remet au premier plan même réduite derrière le jeu.

Flux d'événements

Nexus Tools pousse ce qui change — l'escouade, le bloc-notes, demain les notifications — sur une connexion Server-Sent Events par client (GET /api/events, protocole décrit dans le README de nexus-tools). Elle est tenue par Rust (src-tauri/src/event_feed.rs), pas par une webview : une fenêtre ne sait rien des autres, et sept fenêtres tenant chacune un flux feraient sept sessions vérifiées pour un seul utilisateur. Une seule connexion, donc, ouverte dès qu'une session existe, quelle que soit la fenêtre visible — une notification doit arriver pendant que l'app dort dans la zone de notification —, et chaque événement est remis à la fenêtre qui le dessine :

Événement du serveur Événement Tauri Destinataire
squad.view squad://view la superposition d'escouade
note note://changed toutes les fenêtres
— feed://status toutes les fenêtres (état du flux)

Ce que Rust ne connaît pas, il l'ignore : un sujet peut exister côté serveur avant qu'une fenêtre ne l'écoute.

La session vient du store. Rust lit le même settings.json que le frontend (apiBaseUrl, sessionCookie), sans jamais l'écrire — et il ne sait pas quand ce store a été ouvert ou modifié. Chaque fenêtre le lui dit donc (feed_sync) après chaque vérification de session, et auth://session-changed lui parvient aussi ; l'appel est idempotent. L'URL est revalidée contre la même liste d'hôtes que la capacité http impose aux webviews : une requête faite d'ici ne répond à aucune capacité, la règle est tenue à la main.

Ce que la superposition d'escouade en fait. Elle dit à Rust quelle escouade elle regarde (feed_set_squad) — une autre escouade, c'est un autre flux — et met chaque squad://view dans son cache React Query, sous la clé de l'escouade pour laquelle il a été demandé. Au montage, elle prend d'abord le dernier instantané reçu (feed_snapshot), le flux étant en général ouvert bien avant elle ; sinon une seule requête. Deux règles tiennent l'ordre entre le flux et ses propres écritures : un push reçu pendant qu'une mutation est en vol est retenu (le dernier gagne) et appliqué une fois la réponse posée ; et un push dont squad.version est strictement inférieur à celui affiché pour la même escouade est ignoré — il a été lu avant ce que l'écran montre déjà.

Reconnexion. Perdu, le flux revient avec un délai de 1 s doublé jusqu'à 30 s (±20 %), remis à 1 s après trente secondes de connexion stable ; le bye que le serveur envoie avant sa propre limite de temps redémarre sans attendre. Un 401 arrête tout jusqu'au prochain changement de session — la superposition d'escouade revérifie alors la session, ce qui efface le cookie mort. Un 404 (serveur d'avant le flux) rabat sur GET /api/squads toutes les 10 s, seulement tant que la superposition d'escouade est visible, et ressaie le flux toutes les cinq minutes. Le journal (nexus-app.log) note chaque transition, jamais le contenu.

Les trois superpositions ont le même en-tête

Bloc-notes, feuille de cargo et escouade partagent deux boutons, en plus de la croix.

Le fond, en trois états que le bouton fait défiler : transparent — rien derrière le texte, qu'une ombre sous chaque lettre garde lisible ; ombré — la même ombre, plus un léger voile sombre sur toute la fenêtre, pour un cockpit trop clair pour du texte nu mais qu'on veut encore voir ; opaque — le panneau. Chaque fenêtre garde son réglage d'un lancement à l'autre. Ctrl+Maj+O passe les trois d'un coup au transparent si l'une d'elles dessine encore quelque chose, et leur rend leur panneau sinon.

Le verrou. Verrouillée, la fenêtre laisse passer les clics au jeu : elle reste lue, mais plus rien dessus ne réagit — sauf le verrou lui-même, seul chemin de retour. Une fenêtre qui ignore la souris ne la voit plus, et ne peut donc pas savoir que le curseur est sur son bouton : c'est Rust qui surveille la position du curseur tant qu'une superposition est verrouillée, et rend la souris à la fenêtre pendant que le pointeur est sur la zone que le bouton a déclarée — set_ignore_cursor_events, basculé dans un sens puis dans l'autre. Le bouton redéclare sa zone quand la fenêtre est redimensionnée, puisque l'en-tête se réorganise sous lui.

Notifications

Les notifications s'affichent dans des fenêtres à elles, deux :

  • Le coin. Accrochée à un coin de l'écran — en bas à droite par défaut, comme celles de Windows. Le coin se choisit dans Paramètres, où un exemple part à chaque changement pour montrer où il tombe. C'est là que va tout ce qui vaut d'être remarqué sans gêner : un échec de capture, une mise à jour.
  • Le haut de l'écran, au milieu, en grand. Pour ce qui est demandé à toute l'escouade — une annonce, un ready check — et que personne ne lirait dans un coin pendant que le jeu tient les yeux. Plus large (560 px), plus gros caractères, un bouton plus gros ; trois au plus, la plus récente en haut. Centré est centré : le réglage du coin ne la concerne pas.

Des fenêtres séparées plutôt qu'un coin de la fenêtre principale : celle-ci est presque toujours rangée ou minimisée derrière le jeu, et une notification que personne ne voit n'en est pas une.

Le partage des rôles :

  • Rust possède la géométrie (src-tauri/src/notifications.rs). Il choisit le moniteur, lit sa zone de travail — pas l'écran entier, pour que les toasts se posent au-dessus de la barre des tâches et non dessous — et place chaque fenêtre à sa place. Le moniteur est celui sous le curseur, la règle que suit déjà la capture de zone ; il est figé le temps d'une pile, pour qu'elle ne saute pas d'un écran à l'autre en cours de route.
  • La superposition (src/pages/notifications-overlay-page.tsx, la même page dans les deux fenêtres) dessine la pile et renvoie sa hauteur : la fenêtre est redimensionnée à chaque changement pour ne jamais couvrir plus que les toasts eux-mêmes. Rust reconnaît la fenêtre qui l'appelle, la page n'a qu'à savoir à quoi ressembler. Chaque fenêtre n'écoute que ce qui lui est adressé : l'écoute par défaut de Tauri vise « n'importe quelle cible » et reçoit donc tout, y compris ce que Rust envoie à l'autre fenêtre — une annonce s'afficherait alors en haut et dans le coin.

Émettre une notification depuis n'importe quelle fenêtre :

import { notify } from "@/lib/notifications";

await notify({ kind: "error", title: "Capture impossible", body: raison });
await notify({ title: "Annonce — Alpha", body: texte, placement: "top" });

placement vaut corner par défaut.

L'appel passe par Rust plutôt que par un état React : la fenêtre qui émet n'est pratiquement jamais celle qui affiche. Rust en émet aussi directement — un échec de capture ou d'OCR n'avait jusqu'ici nulle part où s'afficher, puisque la fenêtre de sélection est refermée avant l'erreur.

Une notification levée pendant que la superposition charge encore n'est pas perdue : elle est mise de côté (huit au plus) et remise quand elle signale qu'elle écoute.

Une notification peut porter un bouton (action : un libellé et un événement, avec sa charge utile). Le presser diffuse l'événement à toutes les fenêtres et ferme le toast : c'est la fenêtre qui a levé la notification qui agit, celle des toasts n'ayant ni session ni réseau. Le ready check d'escouade est le premier à s'en servir.

Quatre niveaux — info, success, warning, error — qui décident de l'icône, de la couleur et de la durée (5 à 10 s). Quatre toasts au maximum dans le coin, le plus récent contre le coin. Survoler un toast le retient ; le quitter relance son compte à rebours depuis le début. Un curseur oublié dans ce coin — ce qu'un jeu qui tient la souris rend très possible — ne l'épingle pas pour autant : passé 30 s, il s'en va quoi qu'il arrive.

Limites connues : les fenêtres ne prennent jamais le focus (focusable: false), mais elles restent des fenêtres — un clic dans la zone qu'elles occupent leur revient et n'atteint pas ce qu'il y a dessous. D'où le redimensionnement au plus juste. Et comme toute fenêtre en surimpression, elles sont invisibles d'un jeu en plein écran exclusif ; en plein écran fenêtré, elles s'affichent.

Mises à jour

L'application interroge la dernière release GitHub au démarrage puis toutes les six heures. Si elle annonce une version supérieure à celle du binaire en cours, une notification le dit ; un clic dessus ouvre Paramètres → Mises à jour, où l'on voit la version, les notes de release, et un bouton qui télécharge et installe. Rien ne s'installe sans ce clic.

Ce qui rend la notification nécessaire : la fenêtre principale est presque toujours rangée. Elle porte donc une route (/settings), et le clic ramène la fenêtre dessus — c'est le seul cas où un toast fait autre chose que disparaître.

Sous Windows, l'installateur NSIS est lancé en mode passive (/P /R) : le greffon termine le processus lui-même juste après l'avoir lancé, et l'installateur relance l'application. Rien de ce qui suit l'appel ne s'exécute, ce qui est pourquoi l'écran reste sur « Téléchargement… ».

Signature

Le greffon refuse toute mise à jour qu'il ne peut pas vérifier, d'où une paire de clés minisign. La publique est versionnée dans plugins.updater.pubkey (src-tauri/tauri.conf.json), identifiant C301B23CC68E65F2 — c'est son rôle d'être connue de tous.

La privée n'existe que chez qui publie, et dans les secrets du dépôt :

Secret Contenu
TAURI_SIGNING_PRIVATE_KEY le contenu du fichier .key
TAURI_SIGNING_PRIVATE_KEY_PASSWORD le mot de passe choisi à sa création

Sans eux, l'étape de build de release.yml échoue franchement plutôt que de publier des installateurs que personne ne pourra installer.

Pour en refaire une — la privée perdue, la paire est à remplacer, et les versions déjà installées ne pourront plus se mettre à jour d'elles-mêmes :

npm run tauri signer generate -- -w "$HOME/.tauri/nexus-app.key"

La sortie donne la clé publique à recopier dans la configuration, et le fichier .key à déposer dans les secrets.

Ce que publie la CI

release.yml construit avec --config src-tauri/updater.conf.json, qui ajoute createUpdaterArtifacts : le bundler signe alors chaque installateur et écrit un .sig à côté. Ce réglage est tenu hors de tauri.conf.json pour que la CI des pull requests, qui n'a pas de clé, continue de compiler.

Le workflow attache ensuite un latest.json à la release :

{
  "version": "0.6.0",
  "notes": "le corps de la release, tel qu'écrit à la main",
  "pub_date": "2026-08-01T12:00:00.0000000Z",
  "platforms": {
    "windows-x86_64": { "signature": "…", "url": "https://github.com/…" }
  }
}

L'URL y est relue depuis les assets de la release plutôt que construite : en publiant, GitHub remplace les espaces des noms de fichiers par des points (Nexus App_0.6.0_x64-setup.exe devient Nexus.App_0.6.0_x64-setup.exe).

L'application lit ce fichier via https://github.com/NexusCorpSC/nexus-app/releases/latest/download/latest.json, qui ne résout que vers la dernière release publiée : tant qu'elle reste en brouillon, personne ne se voit proposer la mise à jour.

Authentification

La connexion se fait sur Nexus Tools, dans le navigateur (redirection loopback, RFC 8252) :

  1. « Se connecter » ouvre /desktop/connect sur le site, avec un state et l'empreinte d'un secret que l'application garde (PKCE, RFC 7636) ; src-tauri/src/browser_auth.rs y ajoute le port de la boucle locale sur lequel il écoute.
  2. Déjà connecté sur le site, le navigateur revient aussitôt ; sinon le site demande la connexion — e-mail, Discord ou passkey — puis revient.
  3. Le site renvoie le navigateur vers http://127.0.0.1:<port>/callback avec un code à usage unique (deux minutes).
  4. L'application l'échange, avec le secret, contre une session à elle (POST /api/auth/desktop/exchange) : se déconnecter de l'une ne déconnecte pas l'autre.

Le cookie Set-Cookie renvoyé par l'échange est persisté tel quel, ce qui évite de deviner le nom du cookie (better-auth le préfixe __Secure- en HTTPS).

Développement

Prérequis

  • Node ^20.19.0 || >=22.12.0 (contrainte de Vite 7)
  • Rust stable
  • Linux : libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev

Lancer

npm install
npm run tauri:dev

npm run tauri:dev démarre Vite et le binaire. Lancer le binaire de debug seul n'affiche rien : en mode debug, Tauri charge build.devUrl (http://localhost:1420), donc le serveur Vite doit tourner.

Vérifications

npm run typecheck   # tsc --noEmit
npm run build       # tsc + vite build
cargo fmt --manifest-path src-tauri/Cargo.toml -- --check
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings

Intégration continue

.github/workflows/ci.yml rejoue ces vérifications sur windows-latest, la seule plateforme cible, puis produit les installeurs via tauri build.

Les binaires (.msi et .exe NSIS) sont publiés en artefact nexus-app-windows sur chaque exécution, ce qui permet de tester une PR sans build local.

Le build du frontend précède obligatoirement toute commande qui compile la crate (cargo clippy, tauri build) : build.rs appelle tauri_build::build(), qui résout frontendDist (../dist) et échoue si le dossier n'existe pas. cargo fmt ne compile rien et n'est pas concerné.

Publier une release

.github/workflows/release.yml se déclenche sur les tags v*. Il n'écrit pas la release : il y attache les deux installeurs. Les notes restent donc écrites à la main, et la release doit exister — le workflow le vérifie avant de compiler quoi que ce soit et s'arrête aussitôt s'il n'en trouve pas.

# aligner les trois fichiers de version, puis
git tag v0.2.0 && git push origin v0.2.0
# créer la release du tag (un brouillon suffit), puis relancer le run

Créer le brouillon avant de pousser le tag marche tout aussi bien et évite la relance : un brouillon ne crée pas le tag côté GitHub, le git push déclenche donc bien le workflow. À ne pas faire avec une release publiée, qui crée le tag elle-même : le git push origin v0.2.0 n'aurait alors plus rien à pousser et le workflow ne partirait jamais.

Le workflow refuse aussi de builder si le tag ne correspond pas aux versions déclarées dans src-tauri/tauri.conf.json, package.json et src-tauri/Cargo.toml — les noms des installeurs venant de tauri.conf.json, un tag v0.2.0 publierait sinon un Nexus App_0.3.0_x64-setup.exe.

Relancer le workflow après un build raté remplace les fichiers déjà envoyés au lieu d'échouer dessus.

Il refuse enfin de builder si plugins.updater.pubkey est vide, et exige les secrets de signature : sans eux les installateurs partiraient sans .sig, donc sans mise à jour possible depuis l'application (voir « Mises à jour »). Une fois les installeurs attachés, le workflow publie le latest.json que liront les versions déjà installées — tant que la release reste un brouillon, personne ne la voit passer.

Microsoft Store

Le Store se soumet un installeur différent de celui des releases GitHub, d'où un mode de build à part :

npm run tauri:build:store

Il compile le binaire sans l'empaqueter (tauri build --no-bundle), puis l'empaquette avec src-tauri/tauri.microsoftstore.conf.json par-dessus la configuration normale (tauri bundle --config …). Le fichier à téléverser est src-tauri/target/release/bundle/nsis/Nexus App_<version>_x64-setup.exe.

Ce que change l'overlay, et pourquoi :

Réglage Raison
windows.webviewInstallMode.type: "offlineInstaller" exigence du Store : l'installeur embarque WebView2 au lieu de le télécharger
targets: ["nsis"] une soumission ne porte qu'un installeur ; NSIS est celui que l'application sait déjà relancer

L'installeur passe donc d'environ 5 Mo à ~140 Mo : c'est le runtime WebView2 embarqué, et le workflow refuse un bundle trop léger — un repli silencieux sur le bootstrapper produirait un installeur parfaitement fonctionnel, que Partner Center rejetterait ensuite.

Le MSI reste possible (msiexec /quiet au lieu de /S), mais rien ne l'exige ici et le choisir demanderait de redéclarer le paramètre silencieux.

Ce que demande Partner Center

  • un compte développeur Microsoft, à titre individuel ou au nom d'une société ;
  • un produit créé depuis Apps and Games en « EXE or MSI app », avec un nom réservé ;
  • le paramètre d'installation silencieuse, à déclarer dans la soumission : /S — en majuscule, c'est celui de NSIS. Sans lui la soumission est refusée avec « Win32 products must install silently » ;
  • un installeur signé (Authenticode). C'est le seul prérequis que le dépôt ne couvre pas encore : il demande un certificat, donc bundle.windows.certificateThumbprint (ou signCommand) et un secret de plus. À garder dans un overlay privé plutôt que dans tauri.conf.json, pour la même raison que updater.conf.json : la CI des pull requests n'a pas de certificat.

Les logos attendus par le Store (Square*Logo.png, StoreLogo.png) sont déjà dans src-tauri/icons, écrits par npm run tauri icon.

Éditeur

bundle.publisher vaut "Nexus Corp", et pas seulement pour l'affichage : le Store refuse un éditeur identique au nom du produit. Sans ce réglage, Tauri déduit l'éditeur du deuxième segment de l'identifiant (services.nexus.app, donc « nexus »), ce qui passerait la règle mais n'est le nom de personne.

Mises à jour

Elles restent celles de l'application (voir « Mises à jour ») : pour un produit EXE/MSI, le Store distribue l'installeur, il ne met rien à jour de lui-même. Une copie installée depuis le Store se mettra donc à jour avec l'installeur en ligne attaché à la release GitHub — sans conséquence, WebView2 étant déjà là.

Ce que fait la CI

.github/workflows/microsoft-store.yml rejoue ce build sur les tags v* et à la demande, et attache l'installeur à la release du tag, à côté de celui du build normal — mais pas sous le même nom :

Asset de la release Contenu
Nexus.App_<version>_x64-setup.exe l'installeur public, qui télécharge WebView2
Nexus.App_<version>_x64-microsoft-store.exe celui à soumettre à Partner Center, WebView2 embarqué

Le renommage n'est pas cosmétique. Le bundler donne le même nom aux deux, et release.yml annonce aux copies déjà installées le premier asset qui se termine par -setup.exe : sous son nom d'origine, celui du Store remplacerait l'autre purement et simplement, et à défaut enverrait 140 Mo de WebView2 embarqué à des gens qui l'ont déjà.

Comme release.yml, le workflow exige que la release existe et le vérifie avant de compiler. L'installeur reste par ailleurs publié en artefact nexus-app-microsoft-store (90 jours) : c'est le seul moyen de le récupérer depuis une exécution qui n'a pas de release — pull request ou lancement manuel.

Il tourne enfin sur les pull requests qui touchent le workflow ou l'overlay : rien d'autre ne construit cette variante, une erreur dedans ne se découvrirait donc qu'au moment du tag. Les autres pull requests gardent le seul build de ci.yml.

Instance ciblée

L'URL de l'API se règle dans Paramètres. Les hôtes autorisés sont déclarés dans src-tauri/capabilities/default.json — une URL absente de cette liste est rejetée à l'exécution par les permissions Tauri :

  • https://tools.services.nexus (défaut, seul domaine de production)
  • http://localhost:3000 / http://127.0.0.1:3000

Linux sans GPU

En conteneur ou VM sans accélération, WebKitGTK peut afficher une fenêtre vide :

WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri:dev

Suites possibles

  • Écrans marketplace (/shopping) et industrie (cargo, raffinage).
  • Cache hors-ligne persistant : le cache React Query est aujourd'hui en mémoire.
  • Mises à jour automatiques (tauri-plugin-updater).

About

Desktop app for Nexus Tools for Star Citizen.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages