I built a tool for myself. Not a product, not a pitch — a local dashboard that pulls together a handful of AI tools I use to think through career decisions: resume tightening, interview prep, negotiation framing, that kind of thing. The interesting part isn't any single tool. It's the plumbing that lets them share one panel without me duct-taping five different integrations together.
J'ai construit un outil pour moi-même. Pas un produit, pas un pitch — un tableau de bord local qui rassemble une poignée d'outils IA que j'utilise pour réfléchir aux décisions de carrière : resserrer un CV, préparer un entretien, cadrer une négociation. Ce qui est intéressant, ce n'est pas un outil en particulier. C'est la plomberie qui leur permet de partager un seul panneau sans que j'aie à bricoler cinq intégrations différentes.
Not linked anywhere — yet
Non lié nulle part — pour l'instant
Full disclosure: this thing is not linked anywhere on the site. No nav item, no footer link, no announcement. It's running locally on my machine, shared quietly with a small group for testing and feedback. This article documents the architecture, not a launch — treat it as a build log, not a product page.
Transparence totale : cet outil n'est lié nulle part sur le site. Pas d'entrée de menu, pas de lien en pied de page, pas d'annonce. Il tourne localement sur ma machine, partagé discrètement avec un petit groupe pour tests et retours. Cet article documente l'architecture, pas un lancement — considérez-le comme un journal de construction, pas une page produit.
The problem with wiring tools one by one Le problème quand on branche les outils un par un
The naive approach to "combine a few AI tools into one panel" is to give each tool its own integration: its own API client, its own auth handling, its own error format, its own frontend fetch call. That works fine for one tool. At three tools it's tedious. At five it's a maintenance tax — every tool you add duplicates the same boilerplate, and every frontend component needs to know the specific shape of whatever backend it's talking to.
L'approche naïve pour « combiner quelques outils IA dans un panneau » consiste à donner à chaque outil sa propre intégration : son propre client API, sa propre gestion d'authentification, son propre format d'erreur, son propre appel fetch côté frontend. Ça fonctionne bien pour un outil. À trois outils, c'est fastidieux. À cinq, c'est une taxe de maintenance — chaque outil ajouté duplique le même code répétitif, et chaque composant frontend doit connaître la forme exacte de ce à quoi il parle.
The fix I landed on is boring in the best way: one local Python server, one endpoint, and a routing layer behind it that decides which tool actually handles the request. The frontend never needs to know which AI tool answered — it just asks a question and gets an answer.
La solution où j'ai atterri est ennuyeuse dans le bon sens : un seul serveur Python local, un seul endpoint, et une couche de routage derrière qui décide quel outil traite réellement la requête. Le frontend n'a jamais besoin de savoir quel outil IA a répondu — il pose une question et reçoit une réponse.
Career Intelligence Panel ArchitectureArchitecture du Panneau d'Intelligence de Carrière
Click to zoomCliquer pour zoomer
The single passthrough endpoint L'endpoint passthrough unique
Everything routes through one endpoint: /career/ask. The panel sends a JSON payload with a tool identifier and a prompt. The server looks at the identifier, picks the right handler, calls whatever AI backend that handler wraps, and returns a normalized response shape regardless of which tool produced it.
Tout passe par un seul endpoint : /career/ask. Le panneau envoie un payload JSON avec un identifiant d'outil et un prompt. Le serveur regarde l'identifiant, choisit le bon gestionnaire, appelle le backend IA que ce gestionnaire encapsule, et renvoie une réponse de forme normalisée peu importe l'outil qui l'a produite.
This matters more than it sounds. A single endpoint means one place to handle auth, one place to log requests, one place to add rate limiting or caching later, and one contract the frontend has to honor. Adding a sixth tool means adding a handler function, not a whole new integration surface.
Ça compte plus qu'il n'y paraît. Un seul endpoint signifie un seul endroit pour gérer l'authentification, un seul endroit pour journaliser les requêtes, un seul endroit où ajouter plus tard du rate limiting ou du cache, et un seul contrat que le frontend doit respecter. Ajouter un sixième outil signifie ajouter une fonction de gestion, pas toute une nouvelle surface d'intégration.
One Endpoint vs. Many IntegrationsUn Seul Point d'Accès vs. Multiples Intégrations
Click to zoomCliquer pour zoomer
No cloud database — localStorage does the job Pas de base de données cloud — localStorage fait le travail
This is a single-user local tool, so I skipped the database entirely. State — past queries, saved answers, panel preferences — lives in the browser's localStorage. The Python server is stateless between requests: it receives a prompt, routes it, returns an answer, and forgets about it. Persistence is the frontend's job.
C'est un outil local mono-utilisateur, donc j'ai complètement sauté la base de données. L'état — requêtes passées, réponses sauvegardées, préférences du panneau — vit dans le localStorage du navigateur. Le serveur Python est sans état entre les requêtes : il reçoit un prompt, le route, renvoie une réponse, et l'oublie. La persistance est le travail du frontend.
That's a deliberate constraint, not laziness. No database means no schema migrations, no backup strategy, no data-retention question to answer, and nothing sensitive sitting on a server anywhere. If I ever open this up beyond a small testing group, the storage layer is the first thing that changes — but for a tool that only I run, on my own machine, it's the right amount of infrastructure.
C'est une contrainte délibérée, pas de la paresse. Pas de base de données signifie pas de migrations de schéma, pas de stratégie de sauvegarde, pas de question de rétention de données à répondre, et rien de sensible qui traîne sur un serveur quelque part. Si j'ouvre un jour cet outil au-delà d'un petit groupe de test, la couche de stockage sera la première chose à changer — mais pour un outil que je suis seul à faire tourner, sur ma propre machine, c'est le bon niveau d'infrastructure.
Local-Only Data FlowFlux de Données Local Uniquement
Click to zoomCliquer pour zoomer
Build it yourself: step by step Construisez-le vous-même : étape par étape
None of this requires exotic tooling. Here's the order I'd actually follow if I were rebuilding it from scratch.
Rien de tout ça ne demande d'outillage exotique. Voici l'ordre que je suivrais si je devais tout reconstruire depuis zéro.
- Stand up a minimal local Python server (Flask or FastAPI both work fine) bound to a fixed local port — I used 5555, purely so I'd remember it. No auth needed yet; you're the only client.
- Design a single passthrough endpoint before writing any tool-specific code. Decide the request shape (tool identifier + prompt + optional params) and the response shape (status, tool name, answer, timestamp) up front — this contract is what keeps everything else simple.
- Wire each AI tool call behind that one endpoint as a separate handler function keyed by tool identifier. The endpoint's job is just to look up the right handler and call it — routing logic, not business logic.
- Build a plain frontend panel — a single HTML page with fetch calls is enough. It only ever talks to
/career/ask, passing the tool identifier as a parameter and rendering whatever comes back using the same response shape regardless of source. - Persist state in localStorage: past prompts, saved answers, whatever the panel needs to remember between sessions. Read on load, write on change. No server round-trip needed for state that only matters to you, on your machine.
- Keep it unlisted while you test. No nav link, no sitemap entry, no public route advertised anywhere. Share the URL directly with a small group if you want feedback, and treat every response as something to sanity-check before you'd trust it for a real decision.
- Montez un serveur Python local minimal (Flask ou FastAPI conviennent tous les deux) lié à un port local fixe — j'ai utilisé 5555, uniquement pour m'en souvenir. Pas besoin d'authentification pour l'instant ; vous êtes le seul client.
- Concevez un endpoint passthrough unique avant d'écrire le moindre code spécifique à un outil. Définissez à l'avance la forme de la requête (identifiant d'outil + prompt + paramètres optionnels) et la forme de la réponse (statut, nom de l'outil, réponse, horodatage) — ce contrat est ce qui garde tout le reste simple.
- Branchez chaque appel d'outil IA derrière cet endpoint unique sous forme de fonction de gestion séparée, indexée par identifiant d'outil. Le travail de l'endpoint est juste de trouver le bon gestionnaire et de l'appeler — de la logique de routage, pas de la logique métier.
- Construisez un panneau frontend simple — une seule page HTML avec des appels fetch suffit. Il ne parle jamais qu'à
/career/ask, en passant l'identifiant de l'outil comme paramètre et en affichant ce qui revient selon la même forme de réponse, peu importe la source. - Persistez l'état dans localStorage : prompts passés, réponses sauvegardées, tout ce dont le panneau doit se souvenir entre les sessions. Lisez au chargement, écrivez au changement. Pas d'aller-retour serveur nécessaire pour un état qui n'importe qu'à vous, sur votre machine.
- Gardez-le non répertorié pendant les tests. Pas de lien de navigation, pas d'entrée dans le sitemap, pas de route publique annoncée nulle part. Partagez l'URL directement avec un petit groupe si vous voulez des retours, et traitez chaque réponse comme quelque chose à vérifier avant de lui faire confiance pour une vraie décision.
What's still rough Ce qui est encore brut
This isn't a polished product and I'm not pretending it is. There's no retry logic on tool failures yet, no request queueing if two calls land at once, and the error responses are inconsistent between handlers because I added them at different times with different levels of care. The routing pattern is solid; the edges around it are exactly as rough as you'd expect from something built for one person, on one machine, in spare time.
Ce n'est pas un produit poli et je ne prétends pas le contraire. Il n'y a pas encore de logique de nouvelle tentative en cas d'échec d'un outil, pas de mise en file d'attente si deux appels arrivent en même temps, et les réponses d'erreur sont incohérentes entre les gestionnaires parce que je les ai ajoutées à des moments différents avec des niveaux de soin différents. Le pattern de routage est solide ; les bords autour sont exactement aussi bruts qu'on peut s'y attendre pour quelque chose construit pour une personne, sur une machine, sur du temps libre.
If you build the equivalent, the passthrough pattern and the localStorage-only persistence are the two decisions worth keeping. Everything else — which tools you plug in, how you style the panel, whether you ever make it public — is yours to figure out.
Si vous construisez l'équivalent, le pattern passthrough et la persistance uniquement via localStorage sont les deux décisions à garder. Tout le reste — quels outils vous branchez, comment vous stylez le panneau, si vous le rendez public un jour — c'est à vous de le déterminer.