Changelog

GitHub

Ogni cambiamento rilevante, release dopo release. Lo stesso file è distribuito nel repository ed è tradotto in tutte le 19 lingue.

[1.235.0] — 2026-09-16

Parità con il padre — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 commit). Due correzioni replicate; una delle quali qui perdeva silenziosamente le offerte MokaHR.

Corretto

Una seconda offerta dalla stessa bacheca MokaHR spariva invece di essere aggiunta. url-key.mjs riduce l’URL di un annuncio a una chiave di confronto, e scartava il frammento in modo incondizionato — ragionevole, dato che quasi ogni frammento è solo presentazionale (#apply, #section-2). MokaHR è l’eccezione nel nostro stesso registro: i suoi annunci sono …/social-recruitment/{tenant}/{id}#/job/{n}, dove ogni offerta di un tenant condivide lo stesso percorso e l’id dell’annuncio esiste solo nel frammento. Quindi tutte si normalizzavano sulla stessa chiave. Misurato prima della correzione: incollando due offerte MokaHR diverse nella pipeline si otteneva una sola riga — nessun errore, nessun avviso, nulla da notare; il filtro di freschezza dello scanner (en-scanner.mjs) legge la stessa chiave, quindi una volta che un’offerta di quel tenant era nella cronologia delle scansioni, ogni altra veniva scartata come «già vista». normalizeUrl ora promuove una rotta riconosciuta #/job/{id} o #/jobs/{id} a una chiave di query solo di confronto prima che il frammento venga scartato — MokaHR mantiene il suo nome consolidato mokahr_job_id così le chiavi già scritte restano confrontabili, qualsiasi altro host riceve un _career_ops_fragment_job_id interno. L’id promosso viene aggiunto, non impostato: un URL che porta già quel parametro contiene l’id di un’altra offerta, e sovrascriverlo fonderebbe esattamente le due cose che la chiave esiste per tenere separate. Ogni frammento non riconosciuto continua a collassare, che è la metà che era già corretta. accept-encoding è ora fissato su ogni richiesta verso le sorgenti. Se non impostato, Node negozia qualunque cosa offra la sua build di undici; da Node ≥ 23 questo include zstd, e la risposta zstd di amazon.jobs torna troncata a esattamente 1024 byte con stato 200 — quindi il fallimento si legge come Unterminated string in JSON at position 1024, come un’API malformata piuttosto che un bug di trasporto. Ogni richiesta ora invia gzip, deflate, br. Sui nostri runtime questo è preventivo, non una riparazione: Node 18/20/22 — la matrice CI e il server (22.22.1) — inviano gzip, deflate, verificato direttamente e non presunto, quindi qui nulla era troncato; ciò che cambia oggi è che viene offerto br, che quelle versioni non richiedono di default ma decodificano correttamente. Il default viene applicato senza distinzione tra maiuscole e minuscole, che è la parte che vale la pena rivedere: i nomi degli header non distinguono maiuscole/minuscole e fetch unisce due voci con lo stesso nome in un unico valore separato da virgole, quindi un chiamante che passava Accept-Encoding: identity altrimenti si sarebbe visto inviare gzip, deflate, br, identity.

Note

Gli header restano un oggetto semplice al confine di fetchImpl, dove il padre passa a fetch un’istanza Headers. È una divergenza deliberata, non una svista: gli helper fetch di web-ui accettano un fetchImpl iniettabile, e 45 suite delle sorgenti leggono opts.headers['User-Agent'] da ciò che catturano — un’istanza Headers non ha accesso tramite parentesi quadre, quindi il port fedele è quello del comportamento (override senza distinzione tra maiuscole/minuscole, vince il chiamante) e non dell’implementazione. L’oggetto del chiamante viene copiato, mai mutato, poiché diverse sorgenti condividono un’unica costante di header. La correzione gemella del padre — applicare uno user-agent predefinito senza distinzione tra maiuscole e minuscole — è un non-port: web-ui non inietta alcuno UA predefinito, ogni sorgente imposta quello richiesto dalla propria bacheca, quindi non esiste un default con cui lo User-Agent di un chiamante possa entrare in collisione. Due nuove suite e tre nuovi casi fissano la coppia: tests/http-accept-encoding.test.mjs (il valore fissato esatto, ciascuno dei quattro helper fetch, e un override del chiamante in tre capitalizzazioni — verificando che sopravviva una singola chiave accept-encoding, che è ciò di cui il bug di merge ne produceva due) e tests/parsers-pipeline-hash-route.test.mjs, che lavora sulla giuntura visibile all’utente piuttosto che sulla chiave: due offerte MokaHR devono entrambe raggiungere la pipeline, un reincollaggio della stessa offerta in quattro grafie diverse deve comunque collassare in una riga, e un frammento cosmetico non deve diventare un’identità. Tutti sono stati confermati falliti per primi. Il pin vive nel trasporto, non solo in quegli helper: 25 delle 92 sorgenti chiamano direttamente il fetchImpl iniettato e non ci passano mai, quindi lo applica makeTimeoutFetch — il wrapper che entrambi gli scanner iniettano davvero — ed è l’unico punto di passaggio comune a tutte le sorgenti. (Il progetto padre non ha bisogno di un equivalente: providers/_http.mjs è il suo unico trasporto.) Una guardia strutturale ora fa fallire la CI se una nuova sorgente chiama fetch direttamente invece di usare il trasporto che le è stato consegnato, e un caso comportamentale porta greenhouse — una delle 25 — attraverso il wrapper per dimostrare che l’header arriva davvero sul filo. Non portati, con la motivazione: liveness-browser.mjs (guardia di riutilizzo di Playwright headed — web-ui non guida un browser headed), check-jd-archive.mjs (nessun relay in web-ui), test-all.mjs (l’harness del padre; la divergenza del fork su vpFixtureEnv resta intatta), web/ (il frontend proprio del padre, non rispecchiato), i manifest dei plugin e SIGNATURES.md (solo di repository). batch/batch-runner.sh (le e commerciali vengono sottoposte a escape prima dell’interpolazione sed) e jd-skill-gap.mjs (risolve cv.md rispetto alla radice dei dati) sono inoltrati in sola lettura — web-ui esegue in shell le copie del padre, quindi entrambi arrivano senza alcuna modifica al codice qui, e quello di jd-skill-gap rende più corretto il relay di /api/jds/:id/skill-gap: cv.md ora si risolve dalla radice dei dati invece che dalla directory da cui il processo era stato avviato. Divergenze del fork riverificate dopo il pull: la correzione cirillica \p{L} in providers/telegram-channel.mjs, il providers/telegram.mjs proprio del fork, hermes in web/src/lib/clis.ts. Il conteggio delle sorgenti resta invariato a 92 (87 EN + 5 RU), quindi la guida non è toccata. 3042 → 3066 test.

[1.234.0] — 2026-09-15

Parità con il padre — career-ops main @ 56cce8f (VERSION ancora 1.32.0, 58 commit oltre la 1.32.0). Una correzione replicata su 15 sorgenti, più una guardia perché non possa più tornare.

Corretto

Un singolo id di offerta malformato non svuota più l’intera pagina. encodeURIComponent solleva un URIError su un singolo surrogato UTF-16 isolato, e un payload JSON può portarne uno — JSON.parse('"\\uD800"') lo conserva. Quindici sorgenti costruivano l’URL di ogni annuncio a partire da un id o slug controllato dall’host dentro il ciclo di parsing, quindi un singolo annuncio malformato interrompeva il ciclo e perdeva tutte le offerte di quella pagina; nulla veniva registrato come fallimento della sorgente perché l’eccezione risaliva lo stack prima che il catch per-azienda avesse qualcosa da segnalare. Il padre l’ha corretto in providers/_safe-url.mjs (safeEncodeURIComponent → null su URIError, rilancia qualsiasi altra eccezione) instradandovi l’id di ogni ciclo, scartando solo l’annuncio in questione quando torna null. Quell’helper ora è server/lib/sources/_safe-url.mjs, e la stessa modifica è applicata alle tredici sorgenti replicate — alibaba, arbeitsagentur, bamboohr, feishu-jobs, garena, jibeapply, manfred, meituan, mokahr, phenom, thehub, tkms, vdab — e a due sorgenti solo di web-ui che la nuova guardia ha fatto emergere alla prima esecuzione, jobstreet e trudvsem, che avevano la stessa forma di ciclo e lo stesso throw latente. L’ambito è deliberato: solo gli id di provenienza API passano per l’helper. I valori derivati dalla configurazione mantengono la codifica rigorosa e falliscono rumorosamente — l’office di garena (un segmento di portals.yml) resta sull’urlSegment che solleva eccezioni, mentre il suo id da API riceve un idUrlSegment che restituisce null; il corpName di csod e lo slug di 4dayweek validato da SLUG_RE sono in allowlist per nome, con la relativa motivazione. Il fallback del titolo di rheinmetall poteva sollevare un’eccezione su un href malformato. Quando una scheda non ha un titolo, questo viene ricostruito dallo slug dell’URL tramite decodeURIComponent, che solleva un’eccezione su una sequenza percentuale non valida nell’href estratto — la stessa forma “una riga malformata uccide la pagina”, questa volta sul lato della decodifica. Ora decodifica in modo difensivo e ripiega sullo slug grezzo. Il registro delle sorgenti salta i file con prefisso _. L’auto-discovery importava ogni .mjs in server/lib/sources/ tranne se stesso e avvisava per qualsiasi file privo di export const meta — quindi l’helper produceva un avviso all’avvio. Viene adottata la convenzione del padre: un underscore iniziale significa non è una sorgente; questi file non vengono né importati né segnalati. Un file non dichiarato e privo di meta continua a generare un avviso, e il test di discovery ora verifica entrambe le metà (confermato rosso contro il vecchio filtro). Lo script di build del sito sync-assets.mjs conta i file degli adapter su disco con la stessa regola, quindi l’helper non fa più scattare la sua guardia contro le perdite silenziose.

Note

tests/sources-url-encoding-surrogate.test.mjs rispecchia la suite del padre in tre parti: il contratto dell’helper (un surrogato in coppia — una normale emoji — viene comunque codificato; un’eccezione sollevata dal toString stesso del valore è un bug del chiamante e si propaga), il comportamento per-sorgente per tutte e quindici (un id malformato + un id valido → nessuna eccezione, il valido mantenuto, il malformato scartato, ciascuno guidato attraverso il parser puro esportato dalla sorgente o, per thehub, il suo fetcher con un trasporto fittizio), e una guardia a livello di sorgente: ogni file convertito deve importare l’helper e invocarlo, nessun file può referenziarlo senza importarlo, e nessun file non revisionato può costruire un URL di offerta con un encodeURIComponent nudo — una nuova sorgente con la vecchia forma fallisce la CI indicando il numero di riga incriminato. Tutti e quindici i casi per-sorgente sono stati confermati falliti prima del port. Tutto il resto del delta del padre non è portato, con la motivazione: batch/batch-runner.sh --cli è un flag solo-CLI (web-ui non invoca il batch runner via shell); invite-match.mjs ora risolve set-status.mjs dalla radice del codice — web-ui non lo inoltra; company-funded.mjs ha ricevuto internamente la stessa modifica safe-url e la forma del suo output --json, che /api/company-funded analizza, è invariata; test-all.mjs ha solo spostato un budget di timeout (la divergenza del fork su vpFixtureEnv resta intatta); web/ è il frontend proprio del padre e non è rispecchiato; AGENTS.md/SIGNATURES.md sono solo di repository. Divergenze del fork riverificate dopo il pull: la correzione cirillica \p{L} in providers/telegram-channel.mjs, il providers/telegram.mjs proprio del fork, hermes in web/src/lib/clis.ts. Il conteggio delle sorgenti resta invariato a 92 (87 EN + 5 RU), quindi la guida non è toccata. 3022 → 3042 test.

[1.233.2] — 2026-09-10

Corretto — due avvisi CodeQL js/remote-property-injection ad alta severità.

Corretto

Il ciclo che copia i valori salvati in process.env scorreva Object.entries(safe) — un oggetto costruito dal corpo della richiesta — con una guardia esplicita contro le chiavi di prototipo. Il codice era sicuro, perché safe viene riempito solo da KNOWN_KEYS, ma l’analizzatore non può seguire quel vincolo attraverso due cicli. Ora scorre l’array costante, quindi il nome della proprietà è dimostrabilmente un letterale di modulo. Il comportamento è identico.

Note

Il test che fissava la guardia era un grep del testo sorgente. È stato sostituito da uno strutturale e uno comportamentale che invia __proto__ come JSON grezzo — un letterale di oggetto avrebbe impostato il prototipo invece di creare la proprietà. 3021 → 3022 test.

[1.233.1] — 2026-09-10

Corretto — la guida non descriveva il controllo aggiunto dalla v1.233.0, e la stessa sezione portava ancora istruzioni non più vere da 200 release.

Corretto

DOC-1 — la voce «Usa il valore predefinito (…)» esprime lo stato «la chiave non è impostata»; la differenza dallo scegliere lo stesso valore dall’elenco è invisibile e rilevante. Tre punti nel ### Behavior esistente ×17. DOC-2 — una riga della tabella non nominava alcuna chiave: la cella conteneva (server uses default UA), che descriveva HH_USER_AGENT, rimosso nella v1.19.0, e invitava a registrarsi su dev.hh.ru — inutile dalla v1.65.0. Le tre menzioni obsolete spariscono; la riga di risoluzione problemi è riscritta, non cancellata: da luglio 2026 hh.ru restituisce HTTP 451 agli IP non russi. La tabella delle chiavi ne elenca 6 su 45 mentre il titolo promette un registro. Una frase ×17 la dichiara una selezione e rimanda a #/config.

Note

Il nuovo help-recognized-keys.test.mjs confronta la prima colonna delle tabelle di chiavi con il KNOWN_KEYS vivo. 3018 → 3021 test; la guida resta a 32 H2 / 122 H3.

[1.233.0] — 2026-09-10

Corretto — tre riscontri nel percorso di salvataggio di #/config; CONFIG-3 è stato causato dalla correzione stessa della v1.232.1.

Corretto

CONFIG-3 — una scelta deliberata che coincideva con il valore seminato veniva scartata come «non toccata». initial confonde due origini: seminato da .env e seminato da defaultValue, e nel secondo caso l’uguaglianza è ambigua. Corretto registrando seededFromFile accanto a initial; dove il seme viene dal file nulla cambia, dove viene dal default dirty diventa un secondo segnale valido. Rimuovere una chiave riportava «· 0» perché il server restituiva solo ciò che aveva scritto. Ora risponde anche con removed, e il messaggio conta entrambi. Un menù a tendina poteva essere impostato ma mai svuotato: nessuno dei 18 aveva un’opzione vuota. Ora ognuno inizia con «Usa il valore predefinito (…)». Una nuova chiave i18n ×17.

Note

Base unitaria invariata (3018); browser 111 → 116.

[1.232.1] — 2026-09-10

Corretto — Salva su #/config scriveva campi che l’utente non aveva mai toccato.

Corretto

CONFIG-2. La v1.57.1 semina un controllo non impostato con il suo defaultValue perché il campo mostri ciò che il server usa davvero, mentre save() consultava dirty solo per i segreti e inviava sempre tutti gli altri campi. Il valore messo lì per la VISUALIZZAZIONE partiva quindi come scelta dell’utente: cambiare un menù a tendina inviava 28 chiavi e ne fissava 18 mai aperte. Ciò che cambia è lo stato di ogni impostazione: da «non impostata, segui il default del progetto» a «fissata nel .env».

Note

Confrontare con cfg.values non basta: un select non impostato mostra il suo defaultValue mentre il valore salvato è ''. Ora si confronta con ciò con cui ogni controllo è stato seminato. Il numero di chiavi scritte torna nel messaggio. 105 → 111 test browser.

[1.232.0] — 2026-09-09

Corretto — due difetti da una passata di QA nel browser; nessuno dei due viene dalla v1.231.5.

Corretto

PROFILE-1 — #/profile faceva scorrere lateralmente l’intera pagina. Il valore era un <div> anonimo con soli stili di carattere inline, quindi il suo overflow-wrap restava normal. Un URL LinkedIn non offre al CSS alcun punto di interruzione: la larghezza min-content è l’intera stringa, e min-width: auto sugli elementi di griglia rifiuta di restringersi oltre — la scheda ha fatto scoppiare la traccia, poi la riga e il documento (23 px di scorrimento reale a 1280 px). Corretto con la classe condivisa .card-value e overflow-wrap: anywhere, più .card-row > * { min-width: 0 }. anywhere, non break-word: solo anywhere conta nel calcolo del min-content. CONFIG-1 — POST /api/config accettava qualunque valore per un campo select. {"LLM_PROVIDER":"not-a-provider"} restituiva 200 e finiva nel .env; poi nulla falliva, perché il resolver ripiegava in silenzio. LLM_PROVIDERS era già esportato da env-config.mjs — il validatore non lo consultava.

Note

Solo LLM_PROVIDER è validato: il dominio reale dei modelli appartiene al fornitore e cambia tra le nostre release. Un valore già salvato viene tollerato, perché il form rispedisce ogni campo non segreto a ogni salvataggio. Aggiunto anche il descrittore di OLLAMA_API_KEY. 3013 → 3018 test.

[1.231.5] — 2026-09-09

Sicurezza — sette avvisi chiusi, uno dei quali un’esecuzione di codice remoto critica. Nessun file sorgente è cambiato.

Sicurezza

Unito da Dependabot: multer 2.2.0 → 2.3.0 (alta — denial of service per perdita di descrittori di file all’interruzione) e svgo 4.0.2 → 4.1.0 nel sito (alta e media — removeScripts lasciava passare link eseguibili). Poi altri cinque, emersi dalla riscansione innescata da quelle unioni. Ogni intervallo interessato era già coperto dallo ^ esistente, quindi serviva un aggiornamento, non una modifica al manifesto: astro 7.1.0 → 7.3.2 — critica, esecuzione di codice remoto tramite l’ottimizzazione delle immagini AVIF (corretta in 7.2.8), e un bypass di autorizzazione (7.2.4); sharp 0.35.3 → 0.35.4 — alta, vulnerabilità di libheif; e js-yaml 4.3.1 → 4.3.2 in entrambi i manifesti — alta.

Note

“Solo dipendenze” qui non equivale a “senza rischio”. js-yaml analizza ogni configurazione che l’app legge, multer accetta ogni caricamento, e Astro è avanzato di una versione minore. Verificato: test:ci 3013/3013, suite browser 4/4, e una ricostruzione completa del sito con le stesse 86 pagine e i 17 mirror. npm audit: 0 vulnerabilità.

[1.231.4] — 2026-09-07

Corretto — la v1.231.3 ha sistemato le conseguenze del clic su Doctor, non il momento. Segnalato di nuovo, da mobile e da desktop.

Corretto

withSpinner riscriveva ancora il pulsante mentre la richiesta era in volo. La v1.231.3 gli ha fatto catturare i nodi figli e ripristinarli nel finally, e tornavano davvero — ma il segnale di occupato eseguiva comunque replaceChildren con il testo '⏳ ' + original. Per tutta la durata di doctor.mjs il quadrato da 36 px conteneva quindi ⏳ 🩺Doctor e diventava una pillola larga che rompeva la riga; su desktop appariva come un ⏳🩺Doctor ammassato. Un pulsante con figli elemento non viene più riscritto: il suo segnale è la classe .is-loading, che ora scambia l’icona con una clessidra in CSS, così la geometria non può cambiare: 36 px a riposo, 36 px in volo, 36 px dopo.

Note

La lezione riguarda l’asserzione, non il codice. Il gate della v1.231.3 attendeva che aria-busy sparisse e solo allora misurava: per costruzione vedeva solo lo stato già riparato dal finally. Il nuovo controllo tiene aperta la promessa da sé e campiona a metà volo. 3012 → 3013 test.

[1.231.3] — 2026-09-07

Corretto — cliccare Doctor rompeva la barra superiore su mobile. Segnalato da un telefono il giorno stesso in cui è uscita la v1.231.2.

Corretto

UI.withSpinner appiattiva i figli del pulsante che decorava. Il suo segnale di occupato passava per textContent: salvare button.textContent, scrivere '⏳ ' + original, riscriverlo. Assegnare textContent sostituisce tutti i figli con un unico nodo di testo — e la v1.231.2 aveva appena diviso ogni azione della barra in .btn-ico + .btn-label, con l’etichetta nascosta sotto max-width: 900px. Così il primo tocco su Doctor distruggeva entrambi gli <span> per sempre: le regole mobile non avevano più nulla da agganciare, l’etichetta tornava come testo nudo e il quadrato da 36 px cresceva in una pillola 🩺Doctor che si sovrapponeva all’interruttore del tema. Non guariva da solo — solo un ricaricamento ripristinava il markup. La correzione cattura i nodi figli e li ripristina con replaceChildren; i pulsanti di solo testo, cioè la maggior parte dei 24 punti di chiamata, si comportano esattamente come prima.

  • FIND-3 — il campo di ricerca si era ridotto a 8 px su un telefono da 320 px. Portando la barra su una riga sola, il searchbar è rimasto l’unico elemento flessibile, e il changelog precedente lo diceva: «può ridursi a nulla». E si è ridotto a nulla: 8 px di campo a 320 px, 28 px a 340 px, un carattere di un segnaposto da 21. Cliccabile e digitabile, ma illeggibile come campo. Nessun min-width lo risolve: a 320 px la barra tiene 32 px di padding, un menu da 40 px e 162 px di azioni, senza 120 px da cedere. Perciò sotto i 420 px — la larghezza in cui il campo supera finalmente i 100 px da solo — si nasconde dietro una lente e si espande su tutta la barra al tocco: 182 px a 320 px, 222 px a 360 px, 252 px a 390 px, senza overflow ad alcuna larghezza. Il pulsante conserva un unico nome accessibile e comunica lo stato con aria-expanded, e lo scambio 🔍/✕ è CSS puro. Sopra i 420 px non cambia nulla. Trovato da una passata di regressione in browser sulla build live.

Note

È la stessa classe di difetto della trappola applyI18n() annotata nella v1.231.2 — codice che assegna textContent a un elemento che ora ha figli elemento — raggiunta da un chiamante diverso. La lezione si generalizza: dividere il contenuto di un elemento in <span> figli rende ogni scrittore di textContent già esistente su quell’elemento un bug latente. Bloccato due volte: node:vm sul vero withSpinner e un clic in un browser reale a 320 px. Entrambi confermati fallire sul codice precedente. 3009 → 3012 test.

[1.231.2] — 2026-09-06

Corretto — la barra superiore mobile andava a capo su due righe, il server eseguiva un padre più vecchio e il redirect di cvstart.ru dava 404 su qualsiasi percorso che già nominasse una lingua.

Corretto

  • La barra superiore mobile torna su una sola riga, azioni a sole icone. I pulsanti venivano forzati su una seconda riga a tutta larghezza: un telefono mostrava [☰ · ricerca] sopra [🔔 🌙 Doctor Apri Scan] — due pillole larghe su una riga propria. Ora sono quadrati da 36 px (🩺 / ⚡) accanto alla campanella e all’interruttore del tema, e tutto sta su una riga a 320 px. Misurato, non stimato a occhio: la larghezza del documento eguaglia il viewport a 320, 360, 390 e 430 px e su tutta la fascia 560–760 px, dove prima i pulsanti si sovrapponevano alla barra di ricerca. La lingua non incide più sul layout mobile, perché l’etichetta è nascosta e il pulsante è un quadrato fisso; sopra i 900 px le etichette tornano invariate.
  • Il server eseguiva il padre 1.31.0 benché la versione fosse costruita su 1.32.0. /api/health sul server di produzione riportava parentVersion: 1.31.0. Sincronizzati 95 file di runtime, due rimossi. Rilevato da QA da browser sulla superficie in produzione.
  • cvstart.ru anteponeva /ru/ a qualsiasi percorso, senza guardare cosa ci fosse già. /ru/help diventava /ru/ru/help e /en/help diventava /ru/en/help; entrambi 404. Il primo è ciò che si ottiene copiando un URL dal sito principale; il secondo escludeva del tutto il visitatore anglofono dalla versione inglese. Ora un percorso che nomina già uno dei 16 prefissi passa così com’è, en viene rimosso (l’inglese sta nella radice) e il resto riceve ancora /ru/.

Modificato

  • 98 prompt QA superati spostati in qa/archive/superseded-prompts/. qa/README.md documenta dalla v1.137.0 che al livello superiore resta solo il prompt della versione corrente — la regola non è stata applicata per 95 versioni.
  • Altri cinque prompt hanno lasciato il livello superiore: si dichiaravano attuali senza esserlo. REGRESSION-FINAL.md si definiva «l’unico prompt di regressione autorevole e indipendente dalla versione», ma i suoi §§11–15 sono cinque registri di cicli chiusi (v1.55.x → v1.59.7) e la baseline dell’aiuto dichiarata era 28 H2 / 103 H3 su 16 lingue contro 32 / 122 e 17 oggi: un agente QA che lo seguisse sarebbe caduto al §9 al primo passo. QA-REGRESSION-PROMPT.md aveva un nome senza versione e contenuti di v1.226.0. QA-REGRESSION-PROMPT-v1.76.0-FULL.md era fissato 155 release indietro, REGRESSION-PROMPT-FINAL.md chiude il ciclo v1.58.52 → v1.59.10 e QA-FULL-REGRESSION.md li duplicava tutti su base v1.131.1. Con loro è andato PARENT-SYNC-WORKLIST-v1.26.0.md (il progetto padre è ormai a 1.32.0) e il datato UX-AUDIT-2026-07-06.md è passato in reports/. Restano quattro driver, con le proprie baseline aggiornate ai valori reali di v1.231.2. qa/README.md è riscritto per descrivere la cartella che esiste.

Note

  • Due trappole di specificità ora commentate in app.css: le regole desktop sono dichiarate più in basso del blocco max-width: 900px, quindi a parità di specificità vincono.
  • applyI18n() assegna el.textContent, perciò la chiave i18n è passata a uno <span> interno; sul pulsante avrebbe cancellato l’icona a ogni cambio lingua.
  • Il primo rsync del padre si è interrotto a metà su un file non più presente in locale, lasciando un deploy parziale che riportava successo. I deploy del padre si verificano per checksum, mai per codice di uscita.

[1.231.1] — 2026-09-06

Corretto — la scansione dei contatori della v1.231.0 ha riscritto il nome dell’account in tutti i link.

Corretto

  • Ogni link Fighter90 è diventato Fighter92. La scansione del contatore di sorgenti in v1.231.0 sostituiva 90 con 92 sotto una guardia di confine numerico, che scarta correttamente 290 e 901 ma corrisponde al 90 dentro Fighter90. 562 link nei 17 README, nei 17 pacchetti di aiuto e nei mirror di aiuto del sito, più 138 nel wiki, puntavano a un account inesistente. Segnalato da un utente, per l’immagine mancante.
  • Cinque riferimenti storici a versioni sono slittati. La scansione 85 → 87 ha trasformato v1.85.0 in v1.87.0, inventando una versione mai rilasciata.

Note

  • Un confine numerico non basta: protegge un numero dalle cifre vicine, non dall’essere parte di un identificatore o di una stringa di versione. I contatori sono corretti: 92 sorgenti, 87 adattatori EN, 3009 test.
  • Il repository padre non è stato toccato.

[1.231.0] — 2026-09-05

Aggiunto — due sorgenti di scansione dal career-ops padre v1.32.0 (Collage, Telegram rigoroso), la modalità REST di Gem e un difetto che il port ha portato alla luce nel codice del padre stesso.

Aggiunto

  • Due nuove sorgenti, 90 → 92 (87 EN + 5 RU), ALL_ADAPTERS 85 → 87. Entrambe senza token.
    • Collage (provider: collage) — l’API pubblica dei siti annunci di Collage HR. Il segmento di percorso è l’indirizzo del sito che il tenant ha scelto, non uno slug del nome azienda: viene quindi letto da api: o da un URL secure.collage.co/jobs/<indirizzo> e mai indovinato — un indirizzo indovinato scansionerebbe la bacheca di qualcun altro.
    • Telegram (rigoroso) (provider: telegram-channel) — legge le stesse anteprime t.me/s/<canale> della sorgente telegram esistente, con lo scambio opposto: un post diventa una riga solo se nomina un datore di lavoro E rimanda a una pagina di annuncio. Digest, post con datore nascosto e post di solo contatto vengono scartati. Bassa copertura per scelta — a monte è stato misurato il 32-77% dei post che passano — ma ogni riga porta un datore reale e un link reale. Entrambe vengono distribuite, come nel padre.
  • Gem ha guadagnato una modalità REST opzionale accanto al percorso GraphQL (api: https://api.gem.com/job_board/v0/<board>/job_posts). Non viene mai derivata da un id di bacheca, e il percorso GraphQL resta il predefinito. Portato dal padre #3783.

Corretto

  • Il filtro di luogo di telegram-channel non vedeva il cirillico e nominava città come datori di lavoro. Il LOCATIONISH_RE del padre usava \b, un confine solo ASCII che non scatta mai accanto al cirillico: tutte le sue alternative russe erano irraggiungibili e Senior Engineer | Москва restituiva «Москва» come datore di lavoro. È esattamente la sostituzione di campo che l’intera politica di attribuzione esiste per impedire, e colpiva il pubblico principale del provider. Il port di web-ui usa controlli Unicode, gli stessi che il padre già applica una costante più sotto in ROLE_WORD_RE. Fissato da un test che fallisce sulla forma ASCII. Non ancora corretto a monte.

Note

  • Il recupero per faccette di Workday nel padre (#3851, #3874) non richiedeva port — web-ui non pagina Workday affatto, un solo POST a offset: 0.
  • providers/_types.js è cambiato solo nella documentazione.
  • Test: 2962 → 3009 (+47).

[1.230.0] — 2026-09-05

Modificato — tre correzioni ai provider dal career-ops padre v1.32.0, e un difetto che il port stesso ha portato alla luce.

Aggiunto

  • Welcome to the Jungle accetta un’espressione filters di Algolia applicata lato server (wttj: { filters: "offices.country_code:FR AND contract_type:full_time" }). Una parola chiave da sola non restringe una bacheca globale: a decidere quali max_hits risultati arrivano è il ranking di pertinenza di Algolia, non i filtri dello scanner — i nostri agiscono dopo, su ciò che è già arrivato, quindi tutto oltre il tetto è invisibile per quanto bene corrisponda. Un filtro riduce l’insieme invece di riordinarlo, perciò il tetto per query sale da 200 a 1000. Con un filtro, queries diventa facoltativo: la query vuota significa «tutto ciò che passa il filtro». Portato dal padre #3757.

Corretto

  • Radancy non avvisa più «truncated» quando un tenant serve semplicemente meno annunci di quanti ne dichiari il suo stesso banner. Il padre ha confrontato data-total-results con nove tenant reali: cinque coincidevano, ma quattro — uno piccolo e uno enorme — hanno consegnato dal 10% al 56% di annunci unici in meno del dichiarato, senza una sola riga duplicata. Il banner sovrastima ciò che l’indice di ricerca del tenant serve davvero, quindi confrontarci il conteggio accumulato dava falsi positivi sulla maggioranza delle bacheche. L’avviso ora compare solo quando il percorso è stato fermato dal nostro max_jobs/max_pages, l’unico caso su cui il chiamante possa agire; totalResults resta nel messaggio come contesto e non decide mai. Portato dal padre #3839.
  • Radancy aggira la cache a ogni richiesta di frammento JSON. Uno strato di cache davanti a quella rotta ripete risposte obsolete su alcuni tenant: lo stesso annuncio ricompare pagine dopo mentre un altro non viene mai servito — riprodotto nove volte su nove a monte. Le intestazioni Cache-Control/Pragma non hanno cambiato nulla; un parametro casuale per richiesta sì. Deve essere unico per chiamata, mai derivato dal numero di pagina: un parametro deterministico darebbe comunque alla cache una chiave stabile, e con essa la stessa corrispondenza stabile e sbagliata. Portato dal padre #3839.
  • Gli elenchi di uffici di Greenhouse hanno ordine deterministico. Greenhouse non promette l’ordine di offices[] tra una risposta e l’altra, e l’insieme ricostruito dall’albero /offices segue il suo ordine di visita. Entrambi finiscono nel campo location dell’annuncio, quindi una bacheca che riordinasse i propri uffici riscriveva quella stringa — e un annuncio immutato veniva letto come nuovo. Il padre ha corretto l’insieme (#3839); web-ui aveva una seconda istanza dello stesso difetto, assente nel padre — il ripiego offices[0], usato quando l’annuncio non porta una localizzazione propria — e ora sono ordinati entrambi.

Note

  • La correzione iCIMS del padre (#3728) non richiede port. Legge tutte le voci jobLocation dal JSON-LD della pagina di dettaglio, perché alcuni tenant emettono una voce interamente UNAVAILABLE prima dell’indirizzo reale. L’iCIMS di web-ui prende la localizzazione dall’HTML della pagina di elenco e non apre mai una pagina di dettaglio.
  • La modifica a _http.mjs non richiede port. Estrae isRefusedRedirectError per un secondo consumatore che web-ui non ha; il comportamento di isRetryableError è invariato.
  • La deduplicazione per localizzazione (#3751) non è rispecchiata — vive nella pipeline scan.mjs della CLI del padre.
  • Registro invariato: 90 fonti (85 EN + 5 RU). Test: 2956 → 2962 (+6).

[1.229.0] — 2026-09-03

Aggiunto — quattro fonti di scansione dal career-ops padre v1.31.0: Built In, Feishu Jobs, Garena e MokaHR.

Aggiunto

  • Quattro nuove fonti, 86 → 90 (85 EN + 5 RU), ALL_ADAPTERS 81 → 85, tutte senza token. Built In copre i nove mercati della bacheca tech statunitense e non porta query predefinite: una voce senza queries: né categories: non scansiona nulla e lo dice, perché una fonte condivisa non deve portare i termini di ricerca di qualcuno; un mercato fuori elenco viene rifiutato, mai sostituito in silenzio dalla bacheca nazionale. Feishu Jobs accetta due sole forme di host, che usano percorsi diversi per la pagina dell’offerta. Garena è mono-azienda: office modella il link, non l’elenco. MokaHR restituisce una risposta cifrata (busta AES-128-CBC con la chiave accanto) — offuscamento, non sicurezza, ma senza decifrare non si legge nulla.

Corretto

  • htmlToText poteva lasciar passare nel testo un apri-tag incompleto. safe &lt;img src=x onerror=1 usciva come safe <img src=x onerror=1. Ora ogni decodifica è seguita da una ripulitura e l’apri-tag finale perde la parentesi angolare. Portato dal padre (#3491).

Note

  • Dal fix Workday del padre non è stato portato nulla. Recupera i tenant il cui backend blocca la paginazione all’offset 2000 — web-ui non pagina Workday (un POST a offset: 0, 100 righe).
  • I fix a verify-cv-facts e merge-tracker non hanno richiesto port: web-ui esegue quegli script dal checkout del padre.
  • Test: 2909 → 2956 (+47).

[1.228.5] — 2026-08-30

Corretto — la versione riportata descriveva il file su disco, non il codice in esecuzione.

Corretto

  • /api/health rileggeva package.json a ogni richiesta, quindi un processo obsoleto riportava una versione che non stava eseguendo. Il QA ha trovato un’istanza locale che rispondeva version: 1.228.4 mentre restituiva 404 per /api/ping, una rotta aggiunta in 1.228.3: avviata prima del deploy, serviva il codice vecchio e riportava la versione del file nuovo. È esattamente così che un deploy che copia i file senza mai riavviare sembra riuscito: l’unica stringa che un operatore controlla è quella che non può vedere il problema. Stessa classe del symlink di v1.228.1, dove rsync si interrompeva a metà trasferimento e riportava comunque successo.

    La versione ora viene catturata al caricamento del modulo, quindi descrive il codice in esecuzione. Un processo obsoleto riporta la versione vecchia — la verità, visibile subito. parentVersion resta per richiesta: descrive il checkout del progetto padre, che questo processo non carica e che può legittimamente cambiare sotto un server in esecuzione.

[1.228.4] — 2026-08-30

Corretto — la sonda di liveness non autenticata faceva lavoro su disco a ogni richiesta.

Corretto

  • GET /api/ping leggeva e analizzava package.json a ogni chiamata. È l’unico endpoint raggiungibile senza credenziali, quindi una lettura da disco più un parsing JSON per richiesta è una leva di denial of service consegnata a chiunque — CodeQL l’ha segnalata come mancanza di limitazione di frequenza. La versione ora si legge una sola volta alla registrazione della rotta; non può cambiare senza riavvio, quindi il gestore non fa alcun I/O. Limitare la frequenza avrebbe contenuto il danno; togliere il lavoro toglie la leva.
  • Il mascheramento del nome reale del titolare del profilo fuori dal loopback è ora fissato da un test. /api/health lo sostituiva già con hidden, ma solo perché quella riga condivide la guardia hidden ?? value con la radice del progetto. Nulla lo verificava: una modifica che le desse un valore proprio avrebbe fatto trapelare il nome di una persona reale sulla LAN senza che nulla se ne accorgesse.

[1.228.3] — 2026-08-30

Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.

Fixed

  • The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not. buildAskPrompt did if (ctx.length + sec.body.length > MAX_CONTEXT) break — a break, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 added telegram_channels to it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.

    Two changes: a section past 6 KB is now split at its ### boundaries, so telegram_channels is its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75.

  • A Telegram company label yielded the sentence after it, not the name. Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы) became FinCore Technology (продуктовая разработка, высоконагруженны — capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (so Coca-Cola survives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.

Added

  • GET /api/ping — a liveness probe that leaks nothing. { ok, version } and no more. /api/health reports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.

[1.228.2] — 2026-08-30

Fixed — a two-word search in /api/scan-results matched the phrase, not the words, and under-reported badly.

Fixed

  • ?q= now matches every term independently instead of the literal phrase. The query was a single includes(q) over title + company + location, so продакт менеджер found only rows carrying those two words in that exact adjacency — 32 of the 162 that carry both. Менеджер продукта and Менеджер по развитию продукта were invisible to it: the same words, reordered or with filler between them.

    This is how a narrow match becomes a wrong answer. The Telegram assistant asked for all Продуктовый менеджер reported 9 — faithfully, because 9 was what the endpoint said. The count it quoted was honest; the endpoint’s was not. Word order and the words between are not something a person typing two words intended to pin.

    Matching is AND across terms, so widening precision costs nothing: a row must still carry every term. менеджер разработчик returns zero rather than the union.

[1.228.1] — 2026-08-29

Fixed — a dangling symlink in the repo that silently truncated deploys.

Fixed

  • .claude/skills/refero-design was a tracked symlink to a path that has never existed here. It pointed at ../../.agents/skills/refero-design, which resolves to .agents/ at the repo root; there is no such directory, and the link arrived in the v1.118.0 parity pack from a machine where the surrounding folders nested differently. Anyone cloning the repo has been getting a broken link ever since.

    It was not inert. rsync aborts on it mid-transfer — a deploy today reported success while the server kept running the previous version, and the mismatch only surfaced because the version was checked afterwards. The parent project’s node test-all.mjs also crashes outright when a web-ui checkout sits inside its tree, because its fixture copy walks the working directory and cannot stat the link. Nothing referenced it: the only refero matches in the repo are the Polish word preferowany.

[1.228.0] — 2026-08-29

Aggiunto — i canali Telegram come fonte di scansione e una modalità di query paginata su /api/scan-results.

Aggiunto

  • I canali Telegram sono ora una fonte scansionabile. Un blocco telegram_channels: in portals.yml li elenca; ognuno viene letto dall’anteprima pubblica https://t.me/s/<canale>. Registro: 85 → 86 fonti (81 EN + 5 RU), ALL_ADAPTERS 80 → 81.

    Telegram non pubblica RSS e la Bot API non può leggere un canale che il bot non amministra: nessuna delle due vie ovvie funziona. L’anteprima /s/ è semplice HTML reso dal server: senza autenticazione, senza JS, senza cookie. Verificato su 16 canali: 15 hanno restituito 20 post ciascuno con una GET nuda.

    channel: accetta qualsiasi forma di scrittura. Il parser si ancora alle tre cose che un redesign di Telegram sposterebbe per ultime: data-post, il contenitore del testo e la data ISO <time datetime>.

  • GET /api/scan-results guadagna una modalità di query paginata. ?q=<testo>&limit=50[&region=ru|en][&set=filtered|fresh][&offset=N] restituisce { total, returned, offset, limit, set, rows }, dove total è il conteggio PRIMA della paginazione. Senza parametri la rotta restituisce ancora lo snapshot intero: la tabella #/scan dipende da quella forma.

Corretto

  • L’assistente Telegram rispondeva da una frazione dei dati. Il suo skill puntava già a /api/scan-results, ma la rotta sapeva restituire solo lo snapshot intero — circa 2 MB — che non entra nel contesto di un agente. Alla richiesta «trova tutti i Product Manager» ha riportato 9 corrispondenze dove lo snapshot ne aveva centinaia, dicendo di «lavorare con un indice locale limitato». Era vero: vedeva l’endpoint ma non poteva consumarlo. Con la modalità paginata la stessa domanda costa 2 KB e restituisce un total onesto. Anche lo skill è stato riscritto: chiedeva di riassumere i post nuovi, cioè puntava a fresh invece che a filtered.

Note

  • Un post di canale è prosa, non una scheda di lavoro. Non ci sono campi strutturati: il titolo viene dalla prima riga sostanziale, azienda/luogo/retribuzione da etichette esplicite. Separare le offerte vere da pubblicità e digest resta al title_filter esistente, lo stesso già tarato per le altre fonti. Sono spunti, non un elenco.
  • L’azienda non viene mai indovinata. Senza etichetta la riga è attribuita al canale (@rabotaphp): un datore di lavoro sbagliato entra nel tracker come un fatto.
  • Un parse vuoto è un errore, non «nessuna offerta». t.me risponde con un redirect per un canale privato o inesistente; restituire zero righe si leggerebbe come una giornata tranquilla e nasconderebbe per sempre un refuso nella configurazione. jobGeeks fa esattamente questo (HTTP 302) e deliberatamente non è configurato.
  • Il rilevamento del remoto usa una frontiera di parola Unicode anziché \b, che è solo ASCII — la stessa trappola del filtro sui titoli in v1.227.3, qui colta da un test prima del rilascio.
  • 15 canali configurati e verificati dal vivo: 299 post, 15/15 canali. 30 nuovi test. Test: 2865 → 2895.
  • The README test counts jumped by hundreds, and no, this release did not add them. The localized READMEs carried unit-test totals frozen at 1856 / 1945 / 1955 / 549 / 419 / 284 / 2527 depending on the file — each stale since a different release, and several contradicting the badge at the top of the same page. They now all read 2895, verified by running the suite rather than by copying a badge. Playwright (70 / 90 / 12 → 101), e2e smoke (20 → 21), route modules (12 / 32 → 37), smoke flows (12 → 22) and the test-file count (218 → 329) were stale in the same way. The Total row also repeated the unit count, so it under-summed the three suites below it; it now reads the real sum (3040).

[1.227.5] — 2026-08-29

Corretto — la scansione regionale esauriva la memoria del server e una suite Playwright falliva sul proprio smontaggio.

Corretto

  • La scansione regionale uccideva il server per OOM. ru-scanner accumulava ogni risultato grezzo di ogni query e deduplicava e filtrava solo alla fine. L’elenco di query è volutamente pieno di quasi sinonimi (Golang, Go разработчик, Golang разработчик, Senior Go…), quindi la stessa offerta torna una volta per query e l’array tratteneva un multiplo del conteggio univoco — descrizioni comprese, che sono il grosso dell’oggetto e che i filtri scartano quasi del tutto. Misurato: 742 MB di heap su un’esecuzione reale da 21 query. Il server limita l’heap di Node a 490 MB (V8 lo dimensiona dai 956 MB di RAM della macchina), perciò la scansione abbatteva il servizio con FATAL ERROR: Reached heap limit — quattro volte in un giorno. Ora deduplicazione e filtraggio avvengono per query, portando la stessa esecuzione a 177 MB, il 76% in meno. I conteggi riportati non cambiano: il totale univoco è portato da un Set di stringhe URL.
  • net::ERR_SOCKET_NOT_CONNECTED faceva fallire la CI a caso. Tre suite Playwright chiamano window.stop() per fermare le richieste in volo prima di affermare che non ci sono stati errori di console. Chromium riporta quell’unico atto con tre nomi a seconda dello stato del socket: ERR_ABORTED all’annullamento, ERR_EMPTY_RESPONSE in volo, ERR_SOCKET_NOT_CONNECTED quando il socket era già sparito. Il filtro condiviso ne elencava solo due. È emerso nella scansione dei locale su tr, mentre playwright-forms e playwright-smoke erano a un’esecuzione dallo stesso guasto.

Note

  • Perché solo ora. L’elenco è passato da 14 a 21 query con l’aggiunta delle ricerche product manager. La memoria della vecchia implementazione scalava col totale dei risultati: +50% di query ha superato il tetto della macchina.
  • La modifica alla deduplicazione preserva il comportamento, ed è testato. tests/ru-scanner-dedup-order.test.mjs fissa la direzione scomoda — un duplicato successivo che NON passa i filtri deve sfrattare uno precedente che era passato — ed esegue 200 verifiche randomizzate di equivalenza contro la vecchia implementazione.
  • Il filtro della console resta stretto. Solo la famiglia delle richieste annullate è benigna; connessione rifiutata, errore DNS, reset, 5xx ed eccezioni JS continuano a far fallire l’asserzione.
  • Due ipotesi sono state testate e scartate anziché spedite: un timeout di Caddy (configurazione pulita, flush_interval -1 corretto per SSE) e il requestTimeout predefinito di 5 minuti di Node (una riproduzione in scala ha mostrato che non interrompe una lunga risposta in streaming). La prova vera è stata Reached heap limit nel journal. Test: 2860 → 2865.

[1.227.4] — 2026-08-29

Corretto — word: / stem: erano ancora ignorati da content_filter; la v1.227.3 aveva sistemato solo il filtro sui titoli.

Corretto

  • content_filter non rispettava i prefissi. La v1.227.3 ha insegnato a title_filter i prefissi word: / stem: del progetto padre, ma ha lasciato content_filter al suo lower.includes(k) grezzo: lì una voce con prefisso veniva ancora confrontata con il testo letterale "word:java" — lo stesso no-op silenzioso, un filtro più in là. content_filter legge la DESCRIZIONE dell’offerta e il suo default è sempre stata una semplice sottostringa: per questo un negativo nudo java scarta tutto ciò che si limita a menzionare «JavaScript»; il prefisso è il modo di sottrarre una singola voce a quella regola. Portato il compileContentKeyword del padre, che condivide la macchina dei prefissi ma deliberatamente non ancora le parole corte: il filtro sui titoli ancora le sigle di 2–3 lettere perché «COO» dentro «Coordinator» è sempre sbagliato, mentre una sequenza breve dentro un paragrafo di prosa è spesso voluta (aws, gcp, sql, go).

Note

  • Individuato nella revisione IA della PR v1.227.3, che ha notato come il codice non coprisse ciò che il suo stesso commento affermava: diceva «title_filter / content_filter» mentre era collegato solo il percorso dei titoli. Ora il commento distingue esplicitamente: entrambi i filtri rispettano i prefissi, solo quello sui titoli ancora le sigle.
  • Verificato anch’esso da quella revisione e invariato: passare la regola delle sigle di 2–3 lettere da \b alla frontiera Unicode è identico per l’ASCII e più severo solo dove serve — COO_lead e coo1 restano mantenuti (trattino basso e cifra sono caratteri di parola in entrambi), Coordinator mantenuto, «Директор COO» e «VP «Продукт»» correttamente scartati. Verificati 14 casi limite.
  • Le voci di content_filter senza prefisso mantengono la corrispondenza precedente byte per byte, una descrizione vuota o assente passa ancora, e un word: / stem: vuoto non corrisponde a nulla anziché a tutto. 6 nuovi casi in tests/content-filter.test.mjs. Test: 2852 → 2858.

[1.227.3] — 2026-08-29

Corretto — i prefissi word: / stem: del filtro sui titoli venivano ignorati in silenzio, rendendo quelle righe di filtro inefficaci.

Corretto

  • web-ui non implementava i prefissi word: / stem: del progetto padre. title_filter (e content_filter) confrontano per impostazione predefinita sottostringhe senza distinzione di maiuscole: per questo un negativo nudo intern scarta anche «International Product Manager» e «Internal Tools Engineer». Anziché ribaltare quel default — una rottura per ogni installazione già configurata — il padre ha reso la precisione opzionale per singola voce: word:intern corrisponde solo alla parola intera e stem:agent deve iniziare una parola potendo proseguire (separando «Agentforce» da «Reagents»). web-ui non aveva né l’uno né l’altro, quindi confrontava la voce con il testo letterale "word:intern", che non compare in nessun titolo di offerta. La riga diventava un no-op silenzioso: lo stesso portals.yml filtrava correttamente dalla CLI e per niente qui, così ogni annuncio di stage escluso raggiungeva comunque la tabella #/scan. Portato dal title-keywords.mjs del padre, inclusa la frontiera di parola compatibile con Unicode (\p{L}\p{M}\p{N}_) al posto di \b, che è solo ASCII e corrispondeva a metà parola nei titoli accentati o cirillici.

Note

  • Trovato controllando un portals.yml reale che aveva appena adottato word:intern proprio perché un intern nudo smettesse di scartare «International Product Manager». Il prefisso funzionava nella CLI; web-ui teneva silenziosamente tutti gli stage. Ora il comportamento è identico.
  • I default non cambiano. Una parola chiave senza prefisso mantiene il confronto precedente: sottostringa per le frasi e per tutto ciò che contiene caratteri non alfabetici (.NET, SAP , L&D), ancoraggio ai confini di parola per le sigle di 2–3 lettere. Un word: / stem: vuoto è considerato un refuso e non corrisponde a nulla: come negativo, uno schema vuoto corrisponderebbe a ogni titolo e annullerebbe un’intera scansione per via di due punti sfuggiti.
  • La regola delle sigle di 2–3 lettere usa ora la stessa frontiera Unicode: prima usava \b, quindi coo e word:coo divergevano sui titoli non ASCII — due grafie della stessa regola.
  • 6 nuovi casi in tests/title-filter.test.mjs. Test: 2846 → 2852.

[1.227.2] — 2026-08-29

Corretto — #/config scorreva lateralmente sui telefoni stretti.

Corretto

  • MOBILE-1: #/config andava in overflow orizzontale su viewport ≤ 352 px. Ogni campo di tipo select portava un min-width: 300px inline. Uno stile inline batte il foglio di stile, quindi il controllo non poteva restringersi insieme al contenitore: con un viewport da 320 px il bordo destro del select finiva a 353 px, la pagina otteneva una barra di scorrimento orizzontale e i controlli uscivano a destra. Interessati tutti e 18 i select di provider/modello (cfg-llm-provider e ogni cfg-*-model). Ora min-width: min(300px, 100%) + max-width: 100%, così i comodi 300 px valgono solo quando c’è spazio. Dispositivi alla soglia o sotto: iPhone SE (1ª gen., 320 px), Android datati, schermo esterno del Galaxy Fold.

Note

  • Perché tutte le passate mobili precedenti l’hanno mancato. 360 px e 375 px hanno giusto il margine per nascondere l’overflow, e le suite Playwright girano in una finestra da 1280 px. Nulla in CI aveva mai misurato un viewport più stretto di 375 px. Il nuovo tests/playwright-narrow-viewport.mjs percorre le rotte con form a 320 px e fallisce su qualsiasi overflow orizzontale, nominando nel messaggio l’elemento più largo; un secondo caso fissa che nessun select di #/config supera il viewport. È stato prima verificato che riproducesse il bug (scrollWidth 353 > clientWidth 320, 18 select con bordo destro 353). Playwright: 99 → 101.
  • Noto e volutamente non ancora sotto gate: 280 px (schermo esterno del Galaxy Fold) va ancora in overflow di ~5 px, per una causa diversa — contenuto lungo dentro i blocchi <details> della card di configurazione e la sidebar da 256 px contro un viewport da 280 px, non il min-width inline che questa release corregge. Rifluire a una larghezza che nessun telefono diffuso dichiara è lavoro a parte; la lista WIDTHS della suite è già pronta.
  • Corretto anche il prompt QA della v1.227.1, che chiamava la rotta di follow-up #/modes/followup. Le mode-page registrano lo slug nudo (Router.register(cfg.slug)), quindi la rotta è #/followup e #/modes/followup restituisce correttamente 404. Solo documentazione.
  • Suite unitaria invariata a 2846 (la nuova copertura è a livello browser).

[1.227.1] — 2026-08-29

Corretto — la ripartizione delle fonti nella guida non tornava con sé stessa e il banner GitHub veniva stampato come markup grezzo nell’app.

Corretto

  • §17 dava un totale e una ripartizione che non sommavano. Tutti i bundle della guida dicevano «il registro server/lib/sources/ include 85 adapter — 78 inglesi + 5 russi». 78 + 5 = 83, non 85; in ja/ko la deriva arrivava a 77 (= 82). Ogni nuova fonte alzava il totale lasciando intatto l’addendo, e i due numeri divergevano da diverse release. Ora la ripartizione è 80 EN + 5 RU in tutte e 17 le lingue. Conta più dell’estetica: docs/help/<lang>.md è l’UNICO corpus su cui si fonda «Ask the docs», quindi chi chiedeva quante fonti fossero supportate riceveva due numeri contraddittori dalla stessa frase.
  • La stessa frase fissava un’ancora di versione obsoleta. «A partire da v1.213.0 il registro …» è sopravvissuta in 16 lingue mentre il contatore si muoveva due volte (83 in v1.219.0, 85 in v1.226.0). L’ancora era già stata riparata a mano in v1.210.1 ed è semplicemente derivata di nuovo. Viene rimossa a favore di una formulazione senza versione: il contatore è verificato contro il registro vivo, che non può invecchiare.
  • Il banner dei provider veniva stampato come markup escapato in cima a #/help. v1.225.0/v1.225.1 lo davano per «rimosso nell’app da UI.md() (nessun supporto immagini)». Questo confonde due cose: UI.md() fa escape per primo — esegue l’escape di ogni byte della sorgente prima di qualsiasi trasformazione markdown, che è il confine XSS corretto ma l’opposto della rimozione. Nessuno rimuoveva il banner perché nessuno ci provava, e tutte e 17 le lingue aprivano #/help con 268 caratteri di <p align="center"><img src="https://… letterale. Un nuovo ingresso condiviso (server/lib/help-markdown.mjs::stripGithubOnlyBlocks) scarta in lettura i blocchi HTML immagine isolati, sia per GET /api/help/:lang sia per il corpus del docs-assistant. I file mantengono il banner, quindi GitHub continua a mostrare la vetrina.
  • La colonna «#» della bacheca della cadenza era vuota dalla v1.117.0. Leggeva entry.appNum, mentre followup-cadence.mjs emette num — appNum non è mai esistito nel payload. Ora vengono letti entrambi. La riga «Next up» della v1.227.0 aveva ereditato lo stesso errore ed è corretta insieme.

Note

  • Tutti e quattro sono passati inosservati perché nulla li presidiava. I gate della guida (canonical-docs-coverage, help-ui, help-ru-config-section) contano le INTESTAZIONI — 32 H2 / 121 H3 — e nessun test aveva mai letto un numero dentro un paragrafo né guardato cosa serve davvero l’ingresso della guida. Due nuove suite chiudono la falla: tests/help-source-counts.test.mjs (il totale dichiarato è pari a SOURCES.length; gli addendi corrispondono alla divisione EN/RU viva e sommano al totale; il paragrafo non fissa versioni) e tests/help-banner-strip.test.mjs (il banner resta su disco per GitHub, non sopravvive mai allo strip, la prima riga renderizzata di ogni bundle è la sua intestazione, lo strip resta stretto ed entrambi i consumatori vi passano). Entrambe verificate in rosso contro esattamente questi bug prima del commit. Suite di test: 2837 → 2846.
  • Nessuna modifica a registro, scanner o contratti di route; le fonti restano 85 (80 EN + 5 RU) e ALL_ADAPTERS 80.

[1.227.0] — 2026-08-28

Aggiunto — SEEK Hong Kong (JobsDB), un filtro solo-in-scadenza sui follow-up e i datori di lavoro riservati mostrano l’agenzia che li ha presentati.

Aggiunto

  • JobsDB Hong Kong tramite la sorgente Jobstreet/SEEK esistente. Hong Kong opera con il marchio JobsDB ma poggia sulla stessa piattaforma SEEK dietro lo stesso endpoint v5, quindi non serve una sorgente separata — bastano hk.jobsdb.com nella allowlist degli host e siteKey: HK-Main. Il registro resta invariato: 85 sorgenti (80 EN + 5 RU) e ALL_ADAPTERS 80 — è un nuovo MERCATO su un adapter esistente, non un adapter nuovo.
  • Filtro solo-in-scadenza sulla bacheca della cadenza (#/modes/followup). Una nuova casella restringe la bacheca a ciò su cui agire adesso. La scadenza si legge dall’urgency già calcolata dal followup-cadence.mjs del progetto padre (urgent / overdue), mai dallo status del tracker, che vale applied/responded/interview e non combacerebbe con nulla. Quando non c’è nulla in scadenza, la bacheca indica il follow-up programmato più vicino invece di mostrare un vuoto. La logica vive nel modulo puro public/js/lib/followup-view.js, specchio del followup-view.mjs del padre.
  • I datori di lavoro riservati vengono attribuiti all’agenzia che li ha presentati. Un annuncio con datore di lavoro celato viene registrato come ? nel tracker, con il recruiter nella colonna Via. #/tracker mostrava un ? nudo, così più righe riservate erano indistinguibili e il risolutore dei loghi non aveva nulla su cui agganciarsi. Ora una riga simile si legge «Riservato · tramite <agenzia>», risolve il logo dal nome dell’agenzia ed è rintracciabile con quel nome nella ricerca. Solo presentazione — il campo canonico company non viene mai sovrascritto con il recruiter. Nuovo modulo puro public/js/lib/company-presentation.js, specchio del company-presentation.mjs del padre.

Corretto

  • Jobstreet/SEEK chiamava un endpoint deprecato. La sorgente puntava ancora a chalice-search v4 (/api/chalice-search/v4/search), che SEEK ha ritirato; il padre è migrato a /api/jobsearch/v5/search e web-ui non ha mai seguito. Ora ogni voce Jobstreet/SEEK interroga v5. La forma dell’item v5 è letta nativamente (URL costruita da id, locations[0].label, advertiser.description, salaryLabel), il parametro di proiezione solrFields proprio della v4 sparisce, e una voce il cui portals.yml fissi ancora il percorso v4 morto conserva l’hostname del suo mercato mentre il percorso viene ricostruito su v5 — senza modifiche da parte dell’utente.

Note

  • Parità con career-ops 1.30.0 (HEAD del padre 8d64f65 → 6cc46a4; il suo VERSION segna ancora 1.30.0 — release-please è indietro rispetto a main). Non portato, con motivazioni: riempimento della località da JSON-LD in iCIMS — la sorgente iCIMS di web-ui non ha alcun hook enrichDate() (solo pagine di elenco), quindi non c’è alcun percorso da correggere; stderr CLI non fatale (#1974) — il runner di web-ui decide il fallimento solo dal codice di uscita e non ha mai avuto quell’euristica; tracciamento dei percorsi Turbopack — web-ui non ha bundler; correzioni cv-sync-check — rilanciate in sola lettura e fail-soft; sequenzializzazione di Playwright in scan.mjs, risposte Block H, guardie updater/doctor/update-system, il controllo di sintassi ricorsivo, la dashboard in Go e le auto-verifiche aggiunte a jd-skill-gap/verify-cv-facts — superfici solo-CLI che web-ui non invoca, o auto-test senza cambi di comportamento. Suite di test: 2818 → 2837.

[1.226.0] — 2026-08-27

Aggiunto — due sorgenti di lavoro vietnamite: ITviec e CareerViet.

Aggiunto

  • ITviec + CareerViet (Vietnam). Due board vietnamite con host fissato si uniscono allo scanner tramite voci provider: itviec / provider: careerviet: ITviec (itviec.com, tech) e CareerViet (careerviet.vn, ampia copertura). Entrambi sono scraper HTML senza token, con host-pin anti-SSRF, solo-HTTPS, fail-soft per azienda e limiti di pagina. Il registro ora conta 85 sorgenti (80 EN + 5 RU), ALL_ADAPTERS 80 — entrambi compaiono nel filtro Sorgente di #/scan.

Note

  • Parità con career-ops 1.30.0 (parentVersion 1.29.0 → 1.30.0). Non portato (solo CLI/community o già coperto): la Hired Wall, /calibrate, clean-markers, pacchetti di template, doctor, pdf/LaTeX e le CLI eval/tracker/scan che web-ui non invoca; il fix _html-to-text e roleFuzzyMatch c’erano già; il prefisso title-filter stem: è rimandato. Suite di test: 2818.

[1.225.1] — 2026-08-27

Corretto — il banner dei loghi dei provider ora compare nel README e nella guida.

Corretto

  • Il banner della v1.225.0 non compariva su GitHub: il file images/providers.svg autonomo aveva un font-family non valido (virgolette doppie annidate) che GitHub rifiuta, l’<img> usava un src relativo (l’HTML <img> non risolve percorsi relativi su GitHub) e al blocco <p> mancava una riga vuota finale. Ora punta a un images/providers.png renderizzato tramite URL assoluto, con separazione a righe vuote; anche l’SVG è corretto in XML valido.

Note

  • Solo docs; nessun cambiamento di codice o test. La vetrina di cvstart.org e i riquadri dell’app erano già corretti (SVG inline, non questo file). 2784

[1.225.0] — 2026-08-27

Aggiunto — un banner dei loghi dei provider nel README e nella guida.

Aggiunto

  • Banner dei loghi dei provider. Un unico images/providers.svg autonomo — i 18 provider LLM, con il logo reale del brand (simple-icons, CC0) per gli 11 che ne pubblicano uno e un monogramma con il colore del brand per i 7 restanti — apre ora il README (×17) e la guida in-app. (L’app disegna da sé i riquadri con il logo reale; il banner è reso su GitHub e rimosso nell’app dal renderer sicuro per l’XSS.)

Note

  • Solo docs — nessun cambiamento di codice o test; l’SVG è inline (nessuna risorsa remota). Sorgenti di scansione invariate: 83. Suite di test: 2784.

[1.224.0] — 2026-08-26

Modificato — i riquadri dei provider LLM ora mostrano i loghi reali dei brand.

Modificato

  • Loghi reali dei provider. I riquadri del provider — campi chiave API nelle Impostazioni, le righe di Utilizzo, il chip della dashboard, il riepilogo “Attivo” delle Impostazioni, il risultato della ⚡ eval e il banner di benvenuto — ora mostrano il logo reale del brand per gli 11 provider che pubblicano un’icona open source (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; SVG a percorso singolo da simple-icons, CC0). I 7 senza icona pubblicata (Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark) mantengono il monogramma con il colore del brand. Sempre sicuro per la CSP per costruzione: il percorso del logo è una costante statica incorporata — nessuna risorsa remota, nessun innerHTML.

Note

  • Nessun cambiamento di rotta o comportamento; la stessa API ProviderLogo.el(slug). provider-logo.js è cresciuto (i percorsi dei loghi incorporati). Sorgenti di scansione invariate: 83. Suite di test: 2784.

[1.223.0] — 2026-08-26

Corretto — una valutazione ⚡ live riuscita veniva colorata come errore; inoltre installa il coworker OpenWorker direttamente dal suo URL GitHub o .zip.

Corretto

  • Tinta del badge di risultato della ⚡ eval. Una valutazione live riuscita non è più colorata come fallimento. I provider in-process (OpenRouter, DeepSeek, …) non restituiscono un code di sottoprocesso, quindi lo stretto code === 0 era falso e ogni successo appariva rosso (pur mostrando “exit 0”). La tinta ora segue l’esito — solo un sottoprocesso che esce con codice diverso da 0 lo colora di rosso; un risultato live senza codice è badge-ok, e il suffisso “· exit N” appare solo con un vero codice di uscita.

Modificato

  • Installa il coworker OpenWorker dal suo URL GitHub / .zip. La documentazione del coworker ora descrive l’installer a un comando (curl … | bash, idempotente — riusa un career-ops / web-ui esistente) e i tre percorsi di installazione in OpenWorker (URL GitHub · .zip · import di un singolo file) in docs/integrations/openworker.md, la guida in-app §32 (×17) e il README.

Note

  • Chiusa una vecchia lacuna di copertura: la sorgente Recruitee ora ha un test end-to-end del fix dell’angolo tra virgolette di v1.214.2 (un > dentro un attributo di tag non deve trapelare nella descrizione). Sorgenti di scansione invariate: 83. Suite di test: 2783.

[1.222.0] — 2026-08-26

Modificato — i suggerimenti dei campi dei provider estesi (DeepSeek … Volcengine Ark) sono ora localizzati in tutte le 17 lingue.

Modificato

  • Suggerimenti dei campi dei provider localizzati. Ogni config.<slug>Hint degli 11 provider compatibili con OpenAI aggiunti in v1.216.0–v1.217.0 (DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark) — i suggerimenti di chiave API, modello e URL base in Impostazioni → Chiavi API — è ora tradotto in tutte le 17 lingue anziché ripiegare sull’inglese. URL di registrazione, id dei modelli e il marcatore ⚡ della valutazione live sono conservati alla lettera.

Note

  • Nessun cambiamento di comportamento — il hintFallback inglese in field-specs.js protegge ancora una chiave mancante, e un nuovo test di contratto verifica che ogni hintKey dei descrittori di campo si risolva in tutte le 17 lingue. Fonti di scansione invariate: 83. Suite di test: 2779.

[1.221.0] — 2026-08-26

Sicurezza — difesa in profondità contro il DNS-rebinding sul percorso di fetch dello scanner.

Sicurezza

  • Il core HTTP dello scanner (fetchJson / fetchText in server/lib/http-json.mjs) ora risolve l’host di ogni sorgente sul percorso di rete reale e rifiuta di connettersi se si risolve a un indirizzo privato, loopback, link-local, CGNAT o di metadati cloud — chiudendo un vettore di DNS-rebinding. Gli host dello scanner sono già ancorati a domini pubblici, quindi è difesa in profondità; il percorso degli URL utente aveva già un ancoraggio di connessione più forte (safe-fetch.mjs). La guardia gira solo sul trasporto fetch reale — un fetchImpl di test iniettato non viene mai risolto.

Note

  • Nessun cambio di comportamento per una scansione sana. Fonti di scansione invariate: 83. Test: 2775.

[1.220.0] — 2026-08-26

Aggiunto — scansiona più categorie di Get on Board da una sola voce.

Aggiunto

  • Una voce getonbrd può ora impostare categories: [programming, operations-management, machine-learning-ai] (o un singolo category:) invece del solo feed programming; gli annunci sono deduplicati per URL tra le categorie, con un limite di 12. Le voci esistenti restano identiche (il default resta programming).

Note

  • Solo server; l’host-pin SSRF (www.getonbrd.com) e redirect:'error' sono invariati, e uno slug di categoria non valido viene rifiutato prima di qualsiasi richiesta. Fonti di scansione invariate: 83. Test: 2771.

[1.219.0] — 2026-08-26

Aggiunto — Torre entra nello scanner e ora ogni etichetta di provider è corretta.

Aggiunto

  • Torre (provider: torre) — il marketplace di talenti pan-latinoamericano di torre.ai, una ricerca pubblica senza autenticazione con ruoli remoti che non arrivano a Greenhouse/Lever/Ashby; una richiesta limitata per voce. Il registro conta ora 83 sorgenti (78 inglesi + 5 russe). Inoltre, monogramma del provider attivo nel banner di benvenuto.

Corretto

  • La scansione a16z Speedrun si fermava troppo presto su una board che ruota (una pagina corta a metà passata veniva presa per l’ultima, troncando a 149 di ~300); ora si fida del total_pages del feed. E il banner di benvenuto risolveva il provider da una mappa di 4 voci (mostrando lo slug grezzo per gli altri 14) — ora usa etichetta+monogramma dei 18; la nota provider nelle Impostazioni passa da 7 a 18.

Note

  • La nuova opzione multi-categoria per voce di Get on Board non è ancora collegata qui (richiede un refactor a livello di sorgente). Test: 2768.

[1.218.0] — 2026-08-26

Aggiunto — badge dei provider (e nomi corretti) ovunque venga mostrato un provider.

Aggiunto

  • Monogrammi dei provider sul chip della dashboard, nel riepilogo “Attivo” delle Impostazioni e nell’intestazione del risultato della ⚡ valutazione.

Corretto

  • Etichette obsolete: tre schermate risolvevano il nome da una mappa di 5 voci (il risultato della valutazione etichettava come “Gemini” ogni provider non-Anthropic); ora usano ProviderStatus.label (18). Il conteggio delle chiavi mostrava “/ 7” → 18. L’elenco di slug del suggerimento LLM_PROVIDER ha aggiunto ark / arkcn in tutte le 17 lingue.

Note

  • Solo client, sicuro per la CSP. Nessuna modifica al server. Fonti di scansione: 82. Test: 2758.

[1.217.1] — 2026-08-26

Rafforzamento dei test — copertura degli endpoint per l’intero catalogo di provider.

Aggiunto

  • Un test run<Provider> parametrizzato che verifica endpoint, modello predefinito e autenticazione Bearer per Kimi, MiniMax, Mistral e Fireworks.

[1.217.0] — 2026-08-26

Aggiunto — Ark si aggiunge all’elenco LLM (18 provider). BytePlus Ark e Volcengine Ark — i modelli Doubao di ByteDance — sono ora provider ⚡ dal vivo, con lo stesso core compatibile con OpenAI e un URL di base per regione.

Aggiunto

  • BytePlus Ark (ARK_API_KEY, internazionale) e Volcengine Ark (ARK_CN_API_KEY, Cina) — Chat Completions compatibile con OpenAI via runOpenAICompatible(). Imposta ARK_MODEL / ARK_CN_MODEL (un nome di modello Doubao o il tuo id endpoint ep-…); ARK_BASE_URL / ARK_CN_BASE_URL cambiano regione. Entrambi si uniscono all’ordine auto (per ultimi), con il monogramma nelle Impostazioni e in Utilizzo. L’interfaccia web copre ora 18 provider dal vivo.

Note

  • Stesso percorso di configurazione attendibile + controllo schema http(s) del resto (mai il validatore SSRF degli URL di lavoro). Fonti di scansione invariate: 82. Test: 2755.

[1.216.0] — 2026-08-26

Aggiunto — altri nove provider LLM, ciascuno con la propria targhetta di marca. Le tue valutazioni ⚡ dal vivo possono ora usare DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks o un Ollama completamente locale — a una chiave di distanza, ciascuno segnato da un monogramma accanto al suo campo nelle Impostazioni.

Aggiunto

  • Nove provider compatibili con OpenAI. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI e Ollama (completamente locale, senza chiave). Imposta una chiave — o OLLAMA_BASE_URL per Ollama — nelle Impostazioni e si unisce all’ordine auto dopo Hermes; fissane uno con LLM_PROVIDER. Together ospita anche Inkling di Thinking Machines (thinkingmachines/Inkling). L’interfaccia web copre ora 16 provider dal vivo.
  • Monogrammi dei provider. Una piastrella con l’iniziale nel colore del marchio, sicura per la CSP, segna ogni provider accanto al suo campo chiave nelle Impostazioni e nella sua riga della pagina Utilizzo — SVG inline, nessun logo esterno, nulla lascia il tuo dispositivo.

Note

  • Gli URL di base dei provider sono configurazione attendibile che passa per runOpenAICompatible() con un controllo dello schema http(s) (il loopback di Ollama è consentito), mai il validatore SSRF degli URL di lavoro. Fonti di scansione invariate: 82. Test: 2752.

[1.215.0] — 2026-08-26

Aggiunto — esegui tutta la tua ricerca di lavoro da OpenWorker. Un nuovo coworker guida questa pipeline dall’app di coworker IA open source di Andrew Ng, e la guida in-app ora lo copre così l’assistente Chiedi ai docs può accompagnarti.

Aggiunto

  • Coworker OpenWorker. Un coworker senza codice di OpenWorker — Fighter90/career-ops-coworker — esegue tutta la ricerca di lavoro (scansione, punteggio, adattamento, tracciamento, follow-up) e può avviare questa dashboard, tutto dal tuo desktop. È una persona in un solo file distribuita in 17 lingue, verificata installabile con il loader di OpenWorker stesso.
  • Guida §32 “Run it from OpenWorker” (tutte le 17 lingue) lo documenta, così l’assistente in-app Chiedi ai docs e il pulsante di aiuto fluttuante possono rispondere. Collegato anche dal footer della landing, dal README, dalla wiki e da docs/integrations/openworker.md.

Note

  • Solo docs + sito — nessuna modifica al codice dell’app. Fonti di scansione invariate a 82. Suite di test: 2742.

[1.214.2] — 2026-08-25

Corretto — la pipeline HTML→testo condivisa (descrizioni Greenhouse / Recruitee) non fa più trapelare attributi di tag nella descrizione quando un valore di attributo contiene un carattere >.

Corretto

  • Le descrizioni HTML con > dentro un attributo vengono ripulite correttamente. Il rimuovi-tag dietro le descrizioni Greenhouse e Recruitee usava un match ingenuo che si fermava al primo >, così un tag come <a title="salary > 100k"> lasciava il testo di attributo finale (100k">) nella descrizione. Ora rispetta i valori di attributo tra virgolette (singole o doppie), rimuove il markup prima di decodificare le entità (così una virgoletta codificata dentro un attributo non diventa un falso delimitatore), e preserva ancora un <> letterale in testo semplice.

Note

  • Suite di test: 2742. Fonti di scansione invariate a 82. Nient’altro di nuovo dal padre stavolta: le altre modifiche sono ritrasmesse in sola lettura (i fix follow-up / weekly-digest / rejection-latency / salary-gap non richiedono modifiche a web-ui) o non sono superfici web-ui (un plugin esterno X/Xquik BYO-key, fix CLI/verify). Le voci opzionali — descrizioni di dettaglio SmartRecruiters, salto aggregatori in detect-reposts, prefissi di titolo stem: / word:, irrobustimento anti-DNS-rebinding — restano in coda.

[1.214.1] — 2026-08-25

Corretto — le descrizioni Ashby sono ora limitate alla stessa lunghezza di Greenhouse e Recruitee, così un filtro di contenuto si comporta identico sulle tre board e una board non può gonfiare la cache di scansione.

Corretto

  • Le descrizioni Ashby ora rispettano lo stesso limite di lunghezza delle altre board. La v1.214.0 alimentava il corpo dell’annuncio Ashby al filtro di contenuto, ma — a differenza di Greenhouse e Recruitee, che limitano a 4000 caratteri — quello di Ashby era mappato diretto senza limite (alcuni 4× più lunghi). Così la stessa parola di content_filter poteva corrispondere su Ashby ma non su Greenhouse solo in base a dove cadeva nel testo, e una board poteva aggiungere centinaia di KB alla cache di scansione caricata da #/scan. Ashby è ora limitato agli stessi 4000 caratteri (il suo testo è già in chiaro, quindi viene troncato, mai ripulito dai tag).

Note

  • Suite di test: 2738. Fonti di scansione invariate a 82.

[1.214.0] — 2026-08-25

Corretto — le descrizioni di Greenhouse, Ashby e Recruitee ora alimentano il filtro di contenuto, così i filtri parole / paese / visto corrispondono davvero a quelle board; e “Associate ” (inclusi i ranghi accademici) non è più classificato erroneamente come entry-level.

Corretto

  • I filtri di contenuto ora funzionano su Greenhouse, Ashby e Recruitee. Queste board portano ora il testo completo di ogni annuncio come sua descrizione, così un content_filter (o un filtro parole paese / visto) corrisponde al corpo dell’offerta invece di lasciar passare la board alla cieca. Ashby e Recruitee forniscono il corpo nel payload di lista gratis; Greenhouse lo faceva già. Tutte e tre passano per un’unica pipeline HTML→testo condivisa.
  • “Associate Director” / “Associate Professor” è senior, non entry-level. Il classificatore di seniority (usato da skip_tiers:) trattava la parola “associate” come marcatore entry-level anche quando prefissa un titolo senior. Ora legge “Associate ” — inclusi i ranghi accademici (Associate Professor, Associate Dean) — come senior, mantenendo le vere varianti junior (Associate Attorney, Associate Editor) come entry-level, e classifica ancora un “Intern, Associate Dean” iniziale come tirocinio.

Note

  • Suite di test: 2736. Fonti di scansione invariate a 82. In coda per dopo: arricchimento opzionale delle descrizioni SmartRecruiters (una richiesta di dettaglio per annuncio), il salto delle board aggregator: true di detect-reposts e i prefissi di titolo a parola ancorata stem: / word: — ciascuno una configurazione opzionale, non un cambio di comportamento predefinito. L’irrobustimento anti-DNS-rebinding resta in coda per la sua release di sicurezza dedicata.

[1.213.0] — 2026-08-22

Aggiunto — MyCareersFuture, la banca del lavoro nazionale di Singapore, come fonte di scansione. Corretto — le offerte Greenhouse ora portano il testo completo perché i filtri di contenuto funzionino, e le offerte Ashby da remoto non restano più nascoste dietro una sede solo-città.

Aggiunto

  • MyCareersFuture (Singapore) (mycareersfuture.gov.sg) — una nuova fonte di scansione senza token per la banca del lavoro pubblica nazionale di Singapore, gestita da Workforce Singapore. Selezionala nel filtro Fonte in #/scan, o aggiungi un’azienda con provider: mycareersfuture e una lista keywords facoltativa (altrimenti usa i ruoli target del tuo profilo, come Job Bank). Legge l’API di ricerca pubblica, con host fissato, senza chiave.

Corretto

  • Le offerte Greenhouse ora sono filtrabili per contenuto. Le board Greenhouse vengono recuperate con il corpo completo dell’annuncio, decodificato in testo semplice come descrizione — così un content_filter (o un filtro per parole paese/visto) che legge la descrizione ora corrisponde davvero alle offerte Greenhouse invece di lasciarle passare alla cieca.
  • Le offerte Ashby da remoto non vengono più scartate da un filtro città. Ashby tiene il modello di lavoro (Remote/Hybrid/Onsite) separato dalla città dell’ufficio, quindi un ruolo totalmente da remoto si leggeva ancora come es. “San Francisco” — e un filtro sede che blocca quella città nascondeva un ruolo accettabile. Ora “Remote” viene aggiunto alla sede quando il ruolo è da remoto, e workplaceType vince su un isRemote obsoleto per non etichettare male un ruolo ibrido ancorato all’ufficio.

Note

  • Fonti di scansione: 82 (77 inglesi + 5 russe). Suite di test: 2724. Un irrobustimento anti-DNS-rebinding (validare l’indirizzo risolto di un host prima di connettersi) è in coda per una release dedicata — richiede un design proprio di web-ui, non un port diretto.

[1.212.1] — 2026-08-21

Corretto — la landing di cvstart.org contava per difetto le fonti di lavoro dello scanner (mostrava 80 e ometteva Job Bank (Canada)); ora torna a combaciare con le 81 dell’app, e la build del sito fallisce rumorosamente se le due divergono.

Corretto

  • Il conteggio “Fonti di lavoro” della landing è di nuovo sincronizzato con l’app. Dopo la v1.212.0, cvstart.org mostrava 80 board e mancava il nuovo chip Job Bank (Canada), mentre app, menu a tendina di scansione e guida elencavano tutti 81. La landing costruisce l’elenco caricando il registro live dello scanner, e una fonte non si è caricata in quella build per come importava una dipendenza YAML — quindi è stata scartata in silenzio. Ora Job Bank carica quella dipendenza in modo pigro, come fa il resto dell’app al momento della scansione, così compare sempre.
  • La build del sito ora rifiuta di pubblicare un conteggio di fonti incoerente. Se il registro enumera meno fonti di quante esistano su disco (la firma di una fonte non caricata), la build fallisce con un messaggio chiaro invece di pubblicare in silenzio il numero sbagliato.

Note

  • Il comportamento dell’app è invariato — lo scanner ha sempre avuto tutte le 81 fonti; solo la landing era interessata. Fonti di scansione: 81 (76 inglesi + 5 russe) — invariato. Suite di test: 2687.

[1.212.0] — 2026-08-21

Aggiunto — Job Bank (Canada), la job board nazionale federale. Rimosso — EchoJobs (il suo feed è ora bloccato da anti-bot). Corretto — le board Consider restituiscono di nuovo risultati e le offerte Lever multi-sede non nascondono più metà delle loro sedi.

Aggiunto

  • Job Bank (Canada) (jobbank.gc.ca) — una nuova fonte di scansione senza token per il servizio nazionale federale per l’impiego del Canada, una board ad alto volume che nessun aggregatore copre bene. Selezionala nel filtro Fonte in #/scan, o aggiungi un’azienda con provider: jobbankca e una lista keywords facoltativa (altrimenti usa i ruoli target del tuo profilo). Legge il feed ATOM pubblico, con host fissato, senza chiave.

Rimosso

  • EchoJobs — ritirata. Il suo feed pubblico è ora dietro protezione anti-bot e non restituisce nulla, quindi tenerla sprecava solo uno slot di scansione.

Corretto

  • Le board Consider restituiscono di nuovo risultati. Consider ora richiede un handshake anonimo (una GET che semina un cookie di sessione + token CSRF) prima di accettare la ricerca; senza, la richiesta veniva rifiutata in silenzio e la board sembrava vuota.
  • Le offerte Lever multi-sede non nascondono più metà delle loro sedi. Lever mette una città primaria in location e il resto in allLocations; leggere solo la primaria faceva sembrare un’offerta aperta a Barcellona E Montevideo come solo-Barcellona (e la faceva scartare per errore da un filtro di sede). Ora entrambe vengono unite.

Note

  • Ritmo tra pagine più delicato (250 ms, da 150) sulle board paginate, per cortesia verso i siti carriera single-host. Fonti di scansione: 81 (76 inglesi + 5 russe) — invariato (Job Bank entra, EchoJobs esce). Suite di test: 2685.

[1.211.0] — 2026-08-19

Aggiunto — Yourator, job board tech taiwanese. Corretto — le entità accentate di titolo/azienda ora si decodificano ovunque, e un’azienda con un accento nel nome non viene più segnalata per errore.

Aggiunto

  • Yourator (yourator.co) — una nuova fonte di scansione senza token per il mercato del lavoro tech e digitale di Taiwan. Selezionala nel filtro Fonte in #/scan, o aggiungi un’azienda con provider: yourator. Legge l’API JSON pubblica (nessuna chiave, nessun browser), percorre ogni pagina della board ed emette il vero link del datore di lavoro di ogni annuncio (il suo ATS) con i parametri di tracciamento rimossi.

Corretto

  • Le entità con nome accentato ora si decodificano ovunque. Il decoder HTML condiviso ha acquisito le lettere Latin-1 (&eacute; → é, &ccedil; → ç, …), quindi una board europea che scrive D&eacute;veloppeur o Fran&ccedil;ais non lascia più quel letterale in un titolo, nel tracker o in un documento generato. (Le maiuscole restano maiuscole — &Eacute; è É, non é — e una ricerca come &constructor; ora si risolve in se stessa.)
  • Un’azienda con un accento nel nome non viene più segnalata per errore per essere sul proprio dominio. “Işık” ora si ripiega in “isik” e corrisponde a isik.com.tr; “Société Générale” corrisponde a societegenerale.com. Il vecchio controllo cancellava le lettere accentate invece di ripiegarle sulla loro base ASCII.

Note

  • Fonti di scansione: 81 (76 inglesi + 5 russe). Suite di test: 2667.

[1.210.1] — 2026-08-19

Corretto — i titoli degli annunci e i nomi azienda di Habr Career con “&” o virgolette non arrivano più corrotti.

Corretto

  • La fonte Habr Career ora decodifica le entità HTML nel titolo e nel nome azienda prima che proseguano. Le card renderizzate lato server arrivano con entità (“Changellenge >>”, “Demand Forecasting & Inventory Optimization”, “ООО "М-ТЕХ"”), quindi una “&” non decodificata falliva in silenzio il tuo stesso filtro del titolo con “&” — lo stesso sintomo che la release precedente ha chiuso su altre cinque board — e i nomi azienda arrivavano corrotti al tracker e ai report. La decodifica delle entità è ora completa su tutte e sei le fonti interessate.

Note

  • Suite di test: 2644.

[1.210.0] — 2026-08-19

Aggiunto — Senjob, la prima job board africana dello scanner (Senegal); corrispondenza dei titoli più precisa su altre cinque board.

Aggiunto

  • Senjob (senjob.com) — una nuova fonte di scansione senza token per il Senegal, la prima board africana dello scanner. Selezionala nel filtro Fonte in #/scan, o aggiungi un’azienda con provider: senjob. Legge l’elenco pubblico via HTTP semplice (nessuna chiave, nessun browser), fissa ogni richiesta a senjob.com e — analizzando HTML — tratta un elenco che di colpo non restituisce nulla come una board rotta (un errore visibile) anziché un paese senza offerte.

Corretto

  • I titoli con “&” non fanno più cadere offerte su cinque board — su beesite, Cornerstone (csod), Hacker News “Who is hiring”, Phenom e TKMS i titoli arrivano con entità HTML, quindi una “&” con escape in un ruolo come “R&D Engineer” falliva la tua stessa parola chiave “r&d” e l’offerta spariva in silenzio (nemmeno un veto “sales & marketing” scattava). Ora i titoli — e le località di Phenom — vengono decodificati prima del filtro.

Note

  • Fonti di scansione: 80 (75 inglesi + 5 russe). Suite di test: 2643.

[1.209.0] — 2026-08-17

Aggiunto — la guida nell’app ora copre come registrare l’esito di una candidatura, e «Chiedi alla guida» può portarti lì.

Aggiunto

  • La guida del Tracker (§11) ha guadagnato una sezione «Registrare un esito» in tutte le 17 lingue, che illustra il pulsante Esito: scegli cos’è successo (rifiutato / offerta / assunto / rifiutata / ghostato / passato al colloquio), visualizza l’anteprima di cosa farà, poi registra — il che annota il risultato, archivia il CV e la lettera che hai inviato e sincronizza lo Stato della riga per te. L’assistente fluttuante «Chiedi alla guida» legge la guida, quindi ora ti indirizza a quel pulsante invece di suggerirti solo di modificare lo Stato a mano.

Note

  • Ogni bundle della guida ora è 31 H2 / 119 H3 (era 118); le guardie di parità sono state alzate. Solo documentazione — nessuna modifica al codice o al comportamento. Suite: 2625.

[1.208.2] — 2026-08-16

Corretto — su un telefono i pulsanti di notifiche e tema non stanno più sopra il campo di ricerca.

Corretto

  • La v1.208.1 ha impedito ai pulsanti della barra superiore di sovrapporsi al titolo della pagina, ma su un telefono stretto — pur non essendo il più stretto — e soprattutto nelle lingue con etichette più lunghe, l’intera barra si stipava ancora in una sola riga, così i pulsanti 🔔 e 🌙 potevano finire sopra il campo di ricerca. Ora i pulsanti d’azione (notifiche, tema, Diagnostica, Apri Scan) scendono sempre su una loro seconda riga a tutta larghezza sul telefono, così il campo di ricerca resta pienamente leggibile e niente si sovrappone.

Note

  • Sul telefono i pulsanti d’azione della barra passano a una seconda riga a tutta larghezza, eliminando la fragile fascia di “riga quasi piena” in cui il layout distribuiva lo spazio negativo residuo come sovrapposizione. Un guard di Playwright riproduce ora il trigger esatto — una lingua con etichette lunghe nella fascia 565–640px — e verifica che i controlli della barra non condividano mai pixel. Suite: 2621.

[1.208.1] — 2026-08-16

Corretto — su un telefono i pulsanti della barra superiore non si sovrappongono più alla pagina.

Corretto

  • La v1.208.0 mandava i pulsanti della barra superiore (Diagnostica, Apri Scan, notifiche, tema) su una seconda riga sugli schermi stretti, ma la barra manteneva un’altezza fissa, così la riga a capo debordava e si posava sul titolo della pagina. Ora la barra cresce per accogliere le sue righe e il contenuto scorre sotto.

Note

  • L’height fissa della barra è diventata una min-height, così cresce con il contenuto a qualsiasi larghezza (il desktop è invariato). Un guard di Playwright verifica ora anche che la barra non debordi sulla pagina. Suite: 2621.

[1.208.0] — 2026-08-16

Corretto — l’app ora sta nello schermo di un telefono: niente più scorrimento laterale.

Corretto

  • Su uno schermo stretto l’intera app scivolava di lato — la barra superiore, le tabelle, gli articoli di aiuto e le schede delle impostazioni sforavano il bordo destro. Ora ogni pagina sta in qualsiasi larghezza: i pulsanti della barra superiore vanno a capo su una seconda riga, tabelle e blocchi di codice larghi scorrono nel proprio riquadro, l’aiuto impila l’indice sopra l’articolo, le righe di pulsanti/schede vanno a capo, e percorsi o URL lunghi si spezzano invece di allungare la pagina.

Note

  • La causa era la classica trappola min-width: auto di flex/grid più un paio di elementi larghi non incapsulati; risolto con min-width: 0 sugli elementi della griglia, overflow-wrap su markdown/titoli, una tabella markdown scorrevole e l’impilamento della griglia di aiuto al breakpoint mobile. Un guard Playwright verifica 0 overflow orizzontale a 375 px sulle rotte principali. tests/playwright-smoke.mjs. Suite: 2621.

[1.207.2] — 2026-08-16

Corretto — i piani IA e i profili di orientamento non vengono più mostrati come un dump di codice grezzo.

Corretto

  • Alcuni modelli avvolgono l’intera risposta in un recinto di codice markdown … . Quando accadeva, il piano di sviluppo e il profilo di orientamento apparivano come un blocco di codice a spaziatura fissa invece di un documento con titoli ed elenchi. Ora il recinto avvolgente viene rimosso — solo quando avvolge l’intera risposta e il linguaggio è esplicitamente markdown/md, così una vera risposta in python/js/``` senza linguaggio resta intatta.

Note

  • Gestito una volta sola nel passo condiviso di pulizia LLM (cleanLlmMarkdown), così ne beneficiano tutte le route IA e i blocchi di codice interni alla risposta avvolta sopravvivono. tests/llm-output.test.mjs (+3). Suite: 2621.

[1.207.1] — 2026-08-16

Corretto — la landing page non deborda più di lato sui telefoni piccoli.

Corretto

  • Su un telefono stretto l’hero — il titolo, la riga introduttiva e il terminale di installazione — poteva essere tagliato sul bordo destro perché un comando di installazione lungo e le colonne del layout non si restringevano allo schermo. Ora stanno in qualsiasi larghezza; il comando di installazione scorre dentro il proprio terminale.

Note

  • È stato inoltre irrobustito un controllo E2E instabile che poteva fallire per un 404 transitorio di una risorsa: ora ignora il rumore di rete benigno (favicon / connessione / risorsa fallita) come i controlli vicini, pur continuando a rilevare veri errori di script. Nessun cambiamento nel comportamento dell’app. Suite: 2618.

[1.207.0] — 2026-08-15

Aggiunto — registra l’esito di una candidatura direttamente dal tracker.

Aggiunto

  • Ogni riga del tracker ottiene un’azione Esito: scegli cosa è successo (rifiutato, offerta ricevuta, assunto, offerta rifiutata, nessuna risposta, passato al colloquio), aggiungi una nota facoltativa, visualizza in anteprima lo stato risultante e registralo. La registrazione archivia gli artefatti del CV e della lettera inviati e sincronizza il tracker allo stato canonico — un’azione deterministica invece della modifica manuale.

Note

  • Nuova POST /api/outcome che inoltra la CLI degli esiti: dryRun:true è un’anteprima in sola lettura (individua la riga, riporta lo stato risultante, non scrive nulla); una chiamata reale lo registra. Sicurezza in scrittura: il tipo di esito è limitato all’insieme noto e ogni campo di testo viene rifiutato se contiene caratteri di controllo prima della chiamata (argomenti come array, spawn — nessuna shell). tests/outcome-route.test.mjs. Suite: 2618.

[1.206.0] — 2026-08-15

Documentazione — la guida in-app ora copre le cinque funzionalità più recenti, in tutte e 17 le lingue.

Aggiunto

  • La guida in-app — e l’assistente «Chiedi alla guida» che risponde basandosi su di essa — ora documenta cinque funzionalità aggiunte di recente: Doctor di configurazione (Impostazioni — controlla CV e profilo per lacune e dati di esempio rimasti), Scopri board ATS (Portali — trova automaticamente il portale carriere di un’azienda), il controllo «ancora aperta?» (Tracciamento — se un annuncio è ancora aperto), il suggerimento «riusare un CV precedente?» (CV Studio — segnala quando un CV già adattato va bene per un nuovo annuncio) e il Registro competenze (Analisi — registrare punteggi di autovalutazione). Cinque nuove sottosezioni, tradotte in tutte e 17 le lingue.

Note

  • La struttura della guida cresce a 31 H2 / 118 H3, con parità garantita in ogni lingua. Documentazione di riferimento aggiornata: docs/architecture/API.md documenta le cinque route di queste funzionalità, e i contatori di route/versione in CLAUDE.md e docs/sdd/CONVENTIONS.md sono aggiornati (36 moduli di route). Suite: 2610.

[1.205.0] — 2026-08-15

Aggiunto — un Registro competenze per annotare i risultati di test/valutazioni.

Aggiunto

  • Un nuovo Registro competenze (Analisi → Registro competenze) permette di annotare un’autovalutazione — azienda, piattaforma, competenza, punteggio % e una nota facoltativa — aggiunta a data/assessments.tsv, con un elenco delle voci precedenti (più recenti prima). Zero token, deterministico; il formato del file è gestito dalla CLI del progetto padre.

Note

  • Nuova GET /api/assessments (inoltra la lista JSON predefinita di assessment-log.mjs; fallimento morbido {available:false}) + POST /api/assessments (scrittura esplicita: campi passati come argomenti array a assessment-log.mjs add). Sicurezza in scrittura: ogni campo testo con un carattere di controllo viene rifiutato (un TAB romperebbe una colonna, un a-capo inietterebbe una riga) → 400 prima di scrivere; punteggio/soglia limitati a 0–100, lunghezze limitate. tests/assessments-route.test.mjs. Suite: 2610.

[1.204.0] — 2026-08-15

Aggiunto — un pannello “Dottore di configurazione” nelle Impostazioni che segnala un CV/profilo incompleto o con dati di esempio.

Aggiunto

  • Impostazioni → Dottore di configurazione ora esegue un controllo a zero token del tuo cv.md e config/profile.yml ed elenca i problemi bloccanti (file/campi mancanti) e gli avvisi (dati di esempio residui, metriche fisse) — così cogli una configurazione incompleta prima che indebolisca scansioni e adattamenti. Sola lettura; ri-esegui con un clic.

Note

  • Nuova rotta di sola lettura GET /api/cv-sync-check che inoltra cv-sync-check.mjs del progetto padre, che stampa testo + un codice di uscita (nessun --json); la rotta effettua un parsing leggero delle sue righe stabili ERROR: / WARN: in {ok, errors[], warnings[]} — a decidere il successo è il banner, non il codice di uscita. Fallimento morbido {available:false} sulle installazioni autonome. tests/cv-sync-check-route.test.mjs. Suite: 2602.

[1.203.0] — 2026-08-15

Aggiunto — un suggerimento “riutilizzare un CV precedente?” in CV Studio.

Aggiunto

  • Quando apri un annuncio salvato in CV Studio, l’app ora lo confronta con i tuoi altri annunci salvati (sovrapposizione di parole deterministica, zero token) e ti dice se il più simile basta per riutilizzare quel CV su misura, riutilizzarlo con modifiche o prepararne uno nuovo — così non riparti da zero per un ruolo già mirato.

Note

  • Nuova rotta di sola lettura GET /api/jds/:name/reuse che inoltra jd-similarity.mjs del progetto padre (sovrapposizione Jaccard + guardia di seniority; JSON {decision, score, reason}) una volta per annuncio precedente (fan-out limitato a 25, vince il migliore); fallimento morbido {available:false} se mancano lo script o gli annunci precedenti. tests/jd-similarity-reuse-route.test.mjs. Suite: 2594.

[1.202.0] — 2026-08-15

Aggiunto — scopri la bacheca ATS di un’azienda da #/portals e inizia a seguirla.

Aggiunto

  • Su #/portals, digita il nome di un’azienda e l’app sonda Greenhouse, Ashby e Lever per la sua bacheca pubblica — zero LLM, nessun browser — e mostra le bacheche che esistono e listano ≥1 offerta. Un clic aggiunge la bacheca scelta alle aziende monitorate dallo scanner. Il sondaggio è di sola lettura; la scrittura su portals.yml avviene solo al clic su Aggiungi.

Note

  • Nuovo server/lib/discover-ats.mjs (sonda di slug con host fisso e charset validato tramite safeGet con DNS fissato, ≤12 sonde/richiesta) + POST /api/portals/discover (sola lettura) e POST /api/portals/track (scrittura esplicita: withFileLock + innesto testuale + guardia di ri-parsing + rinomina atomica; solo host ATS noti, idempotente). Riusa il registro di adattatori dello scanner. i18n ×17. tests/discover-ats-resolver.test.mjs + tests/discover-ats-route.test.mjs. Suite: 2588.

[1.201.0] — 2026-08-15

Corretto — un tracker con intestazioni di colonna localizzate o varianti non appare più vuoto.

Corretto

  • Se il tuo data/applications.md usa intestazioni non inglesi o varianti — spagnolo empresa / puesto / estado / fecha / enlace, o position / stage / link — il tracker le leggeva con le chiavi sbagliate e mostrava colonne Azienda / Ruolo / Stato / Data / Link vuote. Ora queste intestazioni vengono ripiegate sui nomi di campo canonici e il tracker si visualizza correttamente. Un tracker tutto in inglese viene elaborato come prima.

Note

  • Nuova mappa HEADER_ALIASES + una piegatura di normalizzazione in parseApplications (server/lib/parsers.mjs); intestazioni sconosciute o già canoniche passano invariate. tests/tracker-header-aliases.test.mjs. Suite: 2563.

career-ops-ui v1.267.1 · page 3/10 · mirror of the project changelog for cvstart.org