<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Oleksii Mazurenko — Blog</title>
    <link>https://oleksiimazurenko.dev/ro/blog</link>
    <description>Technical articles about web development, performance optimization, and developer tools.</description>
    <language>ro</language>
    <lastBuildDate>Tue, 30 Jun 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://oleksiimazurenko.dev/ro/blog/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Îmi scot board-urile din Miro și le aduc în Obsidian</title>
      <link>https://oleksiimazurenko.dev/ro/blog/migrate-miro-board-to-obsidian</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/migrate-miro-board-to-obsidian</guid>
      <description>Butonul Export din Miro îți dă un PNG sau un PDF — o fotografie a datelor, nu datele. Dacă board-urile tale sunt load-bearing maps of thinking adunate de-a lungul anilor, locul lor e în Obsidian: editable, searchable, într-un vault local. Cum am scos board-urile din Miro prin REST API și le-am convertit în Excalidraw / Obsidian Canvas — păstrând poziții, culori, fonturi, curved connectors, mindmap nodes — iar lecția merge mai departe decât Miro: orice SaaS merită un exit plan ÎNAINTE să-l deschizi vreodată.</description>
      <content:encoded><![CDATA[<p>Orice instrument care trăiește în browser te ține ostatic la UI-ul lui. Mai ales după ce ai investit în el timp, carduri, boards — și îți dai seama că map of thinking-ul tău stă pe serverul altcuiva, într-un format pe care nu-l poți deschide fără pagina lor încărcată.</p>
<p>Am câteva boards în Miro care demult au devenit load-bearing — reflecții, planuri, hărți mentale adunate de-a lungul anilor. Butonul Export îți dă un PNG sau un PDF. Astea nu sunt date, e o fotografie a datelor.</p>
<h2>De ce să le scoți afară</h2>
<p>Vendor lock-in nu e o problemă teoretică. E concretă: ca să-ți descarci propriile date prin API, trebuie să faci upgrade la Expert tier. Adică plătești pentru permisiunea de a-ți lua înapoi ceea ce oricum e al tău.</p>
<p>Trăiesc de mult după principiul Obsidian-first: tot ce e important stă într-un vault local, în plain files pe care le pot pune în git, le pot da grep și le pot citi peste zece ani. Un instrument care nu exportă în formatul ăsta e un instrument din care trebuie să pleci înainte ca dependența să devină imposibil de rupt.</p>
<h2>De ce Export nu te salvează</h2>
<p>Butonul Export din Miro îți dă un PNG sau un PDF. Ăsta e raster — structura s-a dus. Nu poți schimba textul dintr-un box. Nu poți muta un shape. Nu poți da grep pe o etichetă. E anunțul de deces al board-ului, nu board-ul în sine.</p>
<p>Obsidian are două alternative care trăiesc în interiorul vault-ului: plugin-ul Excalidraw și Canvas-ul nativ. Ambele sunt JSON. Ambele sunt editable. Ambele sunt searchable. Ambele stau în vault-ul tău lângă notițe, și poți da grep pe text direct din Obsidian.</p>
<h2>Cum le scoți când nu există un buton &apos;Export to Obsidian&apos;</h2>
<p>Când Export-ul din UI nu-ți dă ce ai nevoie, te bagi în API. Miro are un REST API: îți returnează o descriere JSON a fiecărui shape de pe un board. Excalidraw și Obsidian Canvas sunt amândouă JSON open-format. Între ele stă un mapping.</p>
<p>Am scris două scripturi Python. <code>miro_to_excalidraw.py</code> și <code>miro_to_canvas.py</code>. Standard library only — zero deps. Le rulezi cu un token și un board ID, primești un fișier, îl arunci în vault.</p>
<h2>Ce a supraviețuit conversiei</h2>
<p>Sarcina nu era doar să mut shapes dintr-o parte în alta — era să păstrez vizualul astfel încât board-ul să se citească la fel ca în Miro.</p>
<ul><li><strong>Poziții și dimensiuni</strong> — coordonatele fiecărui shape 1:1</li><li><strong>Culori</strong> cu fill opacity (un galben la 40% rămâne la 40%)</li><li><strong>Borders</strong> — culoare, grosime, stil (solid / dashed)</li><li><strong>Fonturi și alignment</strong> — monospace rămâne monospace, center rămâne center</li><li><strong>Connectors</strong> — săgețile se prind în aceleași puncte în care le-a prins Miro</li><li><strong>Curved connectors</strong> — arce bow în loc de linii drepte</li><li><strong>Images</strong> încorporate în fișier (nu URL-uri externe)</li><li><strong>Mindmap nodes</strong> — textul extras prin experimental Mindmap API</li><li><strong>Z-ordering</strong> — overlap-ul nu se încurcă</li></ul>
<h2>Unde a fost incomod</h2>
<p>API-ul Miro are câteva găuri pe care a trebuit să le ocolesc prin reverse engineering.</p>
<ul><li><strong>Curved connectors</strong> — Miro nu expune control points prin API. Așa că în loc de curba originală exactă sintetizez un bow — un arc cu rază fixă între cele două endpoints. Vizual apropiat, mathematically nu identic.</li><li><strong>Z-index</strong> — API-ul Miro nu returnează deloc un câmp z. Care shape stă deasupra, care dedesubt? Euristică: suprafață mai mare = mai în spate. Funcționează pentru că în board-urile reale containerele mari stau de obicei sub elementele mai mici.</li><li><strong>Mindmap</strong> — textul din mindmap nodes nu e accesibil prin items API-ul obișnuit. Ai nevoie de un call separat către experimental Mindmap endpoint. Filled nodes devin boxes, unfilled devin plain text.</li></ul>
<h2>Excalidraw vs Canvas — cum alegi</h2>
<p>Dacă gândești în stil drawings — sketchy lines, hand-drawn feel, compoziții freeform — alege <strong>Excalidraw</strong>. Plugin-ul Excalidraw randează fișierul în aceeași estetică pe care ai văzut-o pe whiteboards.</p>
<p>Dacă board-urile tale sunt mai ales <em>dreptunghiuri cu text legate prin săgeți</em> și trăiești în Obsidian ca second brain — alege <strong>Canvas</strong>. Canvas e nativ, se deschide dintr-un click și e cusut în backlinks și tags-urile vault-ului.</p>
<p>Unele board-uri le convertesc în Excalidraw (unde contează visual style), altele în Canvas (unde contează viteza și feel-ul nativ). Repo-ul suportă ambele formate — alegi la rulare.</p>
<h2>Rezumat</h2>
<p>Python 3, standard library only. Fără pip install, fără virtualenv, fără node_modules. <code>python3 miro_to_excalidraw.py 'BOARD_ID' 'output.excalidraw'</code> — și fișierul e în mâinile tale.</p>
<p>Repo: <a href="https://github.com/oleksiimazurenko/miro-to-obsidian">github.com/oleksiimazurenko/miro-to-obsidian</a>. Licență MIT. PR-uri binevenite — mai ales pentru edge case-uri pe care încă nu le-am întâlnit.</p>
<h2>Lecția e mai mare decât Miro</h2>
<p>SaaS lock-in-ul e un drog. Primele luni sunt gratis. La șase luni, e deja obicei. La un an, datele tale trăiesc într-un format pe care doar UI-ul lor îl poate citi. La doi ani, plătești Expert tier ca să scoți ce oricum era al tău.</p>
<p>Concluzia nu e „nu folosi SaaS”. Concluzia e că orice instrument pe care îl lași să devină load-bearing are nevoie de un <strong>exit plan ÎNAINTE să-l deschizi vreodată</strong>. Întrebarea „cum ies de aici când o să am nevoie” cântărește la fel de mult ca „cât de plăcut e UI-ul”. Un instrument fără export în plain-text sau fără un API deschis e un instrument cu un preț ascuns pe care îl plătești peste doi ani.</p>
<p>Obsidian-first nu pentru că Obsidian ar fi perfect. Ci pentru că plain markdown files sunt un format care va supraviețui însuși Obsidian-ului. La fel cum un fișier JSON Excalidraw simplu va supraviețui plugin-ului. La fel cum scriptul ăsta Python va supraviețui Miro-ului.</p>]]></content:encoded>
      <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
      <category>obsidian</category>
      <category>miro</category>
      <category>automation</category>
      <category>tools</category>
      <category>open-source</category>
    </item>
    <item>
      <title>CRAFT: o metodă de a folosi AI fără să-ți pierzi măiestria</title>
      <link>https://oleksiimazurenko.dev/ro/blog/craft-method-ai-without-deskilling</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/craft-method-ai-without-deskilling</guid>
      <description>Un diagnostic fără leac e doar anxietate. După postarea despre pierderea abilităților din cauza AI, iată rețeta: CRAFT — cinci principii (Challenge First, Review Adversarially, Ask Why Not What, Friction on Purpose, Test Your Understanding) sub o singură axiomă: AI face ce ai stăpânit deja, tu faci ce încă înveți. O metodă practică și ușor de reținut pentru a folosi AI zi de zi fără să-ți pierzi pe tăcute priceperea.</description>
      <content:encoded><![CDATA[<p>Postarea mea anterioară a diagnosticat boala: AI a oprit în tăcere bucla de feedback forțat pe care realitatea o rula cândva pentru tine — eroarea de compilare, testul care pică, privirea goală a unui reviewer. Presiunea care te <em>obliga</em> să înveți a dispărut, iar calea ușoară poartă acum masca succesului. Postarea aceasta este rețeta.</p>
<p>Nu o listă vagă de sfaturi — acelea se evaporă în clipa în care ești obosit. Un canon cu nume, pe care îl poți invoca sub presiune, așa cum SOLID funcționează pentru proiectarea orientată pe obiecte. Îl numesc <strong>CRAFT</strong>: cinci principii, câte o literă fiecare, așezate sub o singură axiomă. Numele e chiar esența — programarea rămâne un meșteșug, iar AI e doar o unealtă în mâinile cuiva care încă poate face treaba fără ea.</p>
<h2>Axioma: linia care se mișcă</h2>
<p>Înaintea celor cinci litere vine o regulă care le guvernează pe toate — așa cum „responsabilitatea unică” stă în inima SOLID:</p>
<blockquote>AI face ceea ce ai stăpânit deja. Tu faci ceea ce încă înveți. Linia se deplasează pe măsură ce crești.</blockquote>
<p>Predă partea stăpânită către AI și mergi mai repede — e câștig curat. Predă către AI partea pe care încă ți-o formezi și pur și simplu nu ți-o mai formezi niciodată; plătești în pricepere pentru o viteză de care nici măcar nu aveai nevoie. Fiecare principiu de mai jos e doar o cale de a rămâne sincer cu tine însuți despre unde se află acea linie pentru tine <em>astăzi</em>.</p>
<h2>CRAFT — cinci principii, un acronim</h2>
<p>Citite de sus în jos, literele sunt un flux de lucru: provoacă-te mai întâi pe tine, analizează cu ostilitate ce îți dă AI, întreabă <em>de ce</em>, nu <em>ce</em>, creează fricțiune în mod deliberat și verifică dacă înțelegi cu adevărat.</p>
<h3>C — Challenge First</h3>
<p><strong>Principiu.</strong> Înainte să deschizi AI, încearcă singur problema timp de 5–15 minute. Prima soluție de pe pagină trebuie să fie <em>a ta</em> — chiar și una stângace. AI intervine ca să te <strong>deblocheze</strong>, niciodată ca să <strong>înceapă</strong> în locul tău.</p>
<p><strong>De ce.</strong> Zbaterea e cea care construiește modelul mental — neuronii se activează cât timp ești blocat, nu cât timp citești un răspuns gata făcut. Este exact fricțiunea pe care realitatea ți-o impunea gratis; acum trebuie să ți-o programezi singur.</p>
<h3>R — Review Adversarially</h3>
<p><strong>Principiu.</strong> Nu lași AI să-ți facă review la cod — <em>tu</em> faci review la codul AI. Citește fiecare linie generată vânând bug-ul, cazul-limită, gaura de securitate.</p>
<p><strong>De ce.</strong> Judecata este cea mai durabilă abilitate din inginerie și singura care devine mai valoroasă pe măsură ce modelele se întăresc. Să faci review la AI o ascute și prinde greșelile pline de siguranță pe care modelele adoră să le facă. Să accepți un diff necitit erodează ambele deodată.</p>
<h3>A — Ask Why, Not What</h3>
<p><strong>Principiu.</strong> Folosește AI ca tutore, nu ca autor-fantomă. Cere raționamentul, alternativele, compromisurile — nu doar fragmentul gata scris.</p>
<p><strong>De ce.</strong> „Ce” îți dă un pește; „de ce” îți dă undița. „Dă-mi codul” nu lasă nimic în urmă. „De ce abordarea asta și nu cealaltă, și ce se strică la input gol?” te lasă cu un nivel mai deștept decât erai.</p>
<h3>F — Friction on Purpose</h3>
<p><strong>Principiu.</strong> Realitatea nu te mai obligă să te încordezi, așa că adaugă tu însuți rezistența la loc. Fă regulat <strong>sesiuni fără AI</strong> — construiește ceva de la zero, fără asistent. Rescrie codul în loc să-l lipești. Închide AI și reprodu ce tocmai te-a învățat.</p>
<p><strong>De ce.</strong> Abilitatea se fixează doar prin dificultate dezirabilă. Un copy-paste nu lasă nicio urmă în memorie; rescrierea lentă a acelorași linii lasă. Fricțiunea pare o taxă pe viteză — de fapt e prețul de a-ți păstra abilitatea, pur și simplu.</p>
<h3>T — Test Your Understanding</h3>
<p><strong>Principiu.</strong> Poarta supremă e o singură întrebare: <em>aș putea explica asta, linie cu linie, altei persoane?</em> Dacă nu, nu stăpânești codul și nu-l dai în producție. Prezice output-ul înainte să-l rulezi; strică-l intenționat și repară-l la loc.</p>
<p><strong>De ce.</strong> „Merge” nu înseamnă „înțeleg” — prăpastia aceea e iluzia competenței, și exact acolo se ascunde pierderea abilităților. Să-ți testezi propria înțelegere este cea mai ieftină asigurare împotriva zilei în care te trezești incapabil să lucrezi fără unealtă.</p>
<h2>CRAFT pe scurt</h2>
<table><thead><tr><th>Principiu</th><th>Capcana</th><th>Obiceiul</th></tr></thead><tbody><tr><td>&lt;strong&gt;C&lt;/strong&gt; · Challenge First</td><td>Prompt pe o pagină goală</td><td>Încearcă singur 5–15 min mai întâi</td></tr><tr><td>&lt;strong&gt;R&lt;/strong&gt; · Review Adversarially</td><td>Accepți diff-ul necitit</td><td>Vânează bug-ul în output-ul AI</td></tr><tr><td>&lt;strong&gt;A&lt;/strong&gt; · Ask Why, Not What</td><td>„Dă-mi pur și simplu codul”</td><td>„Explică abordarea și compromisurile”</td></tr><tr><td>&lt;strong&gt;F&lt;/strong&gt; · Friction on Purpose</td><td>Copy-paste la tot</td><td>Sesiuni fără AI; rescrie, nu lipi</td></tr><tr><td>&lt;strong&gt;T&lt;/strong&gt; · Test Your Understanding</td><td>Dai cod pe care nu-l poți explica</td><td>„Aș putea explica asta linie cu linie?”</td></tr></tbody></table>
<h2>Autoverificare: crești sau te degradezi?</h2>
<p>Nu-ți trebuie un studiu ca să știi în ce direcție mergi. <strong>Te degradezi dacă:</strong></p>
<ul><li>Nu poți scrie o versiune de bază a lucrului fără AI.</li><li>Lipești cod pe care nu l-ai putea explica dacă te-ar întreba cineva.</li><li>Simți anxietate la gândul de a lucra cu asistentul închis.</li><li>Ai încetat să mai citești output-ul AI și pur și simplu îl accepți.</li><li>Nu poți prezice ce face un fragment înainte să-l rulezi.</li></ul>
<p><strong>Crești dacă:</strong></p>
<ul><li>AI te face mai rapid la lucruri pe care le înțelegi cu adevărat.</li><li>Prinzi în mod obișnuit greșelile modelului.</li><li>Încă poți lucra fără el — mai lent, dar poți.</li><li>Îl folosești ca să înveți un concept nou, apoi reproduci acel concept fără ajutor.</li><li>Decizi conștient ce predai și ce păstrezi.</li></ul>
<h2>Dacă reții o singură literă</h2>
<p>Reține <strong>T</strong>. Poarta explicabilității — <em>aș putea explica asta, linie cu linie?</em> — este cel mai simplu obicei din toată metoda și, de una singură, prinde aproximativ 80% din degradare. Tot restul din CRAFT nu face decât să-ți fie mai ușor să treci sincer de acea poartă.</p>
<h2>Concluzia</h2>
<p>Nimic fundamental nu s-a schimbat în privința învățării. Înțelegerea conștientă a contat dintotdeauna; tocitul a fost mereu un substitut prost. Ce s-a schimbat e că mediul a încetat să mai impună acea diferență în locul tău. CRAFT e pur și simplu o cale de a ți-o impune singur.</p>
<blockquote>Regulile jocului sunt aceleași. Doar că jocul a devenit mai perfid — calea ușoară poartă acum masca succesului.</blockquote>
<p>Dacă n-ai citit diagnosticul din spatele a toate acestea, începe cu postarea anterioară despre AI și bucla de feedback pierdută. Aceea era boala; aceasta e leacul.</p>]]></content:encoded>
      <pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate>
      <category>ai</category>
      <category>learning</category>
      <category>career</category>
      <category>methodology</category>
    </item>
    <item>
      <title>Liniștea pe care a oprit-o AI</title>
      <link>https://oleksiimazurenko.dev/ro/blog/ai-deskilling-and-feedback-loop</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/ai-deskilling-and-feedback-loop</guid>
      <description>Mai merită să înveți programare dacă există AI? Răspunsul zgomotos greșește din ambele părți. Cu baza de dovezi 2025–2026 (Nature, Anthropic, AI &amp; Society) întrebarea nu e &quot;AI sau nu&quot;, ci ce competențe îi predai AI-ului și pe care le păstrezi pentru tine — și un lucru chiar s-a schimbat: înainte realitatea te forța să înveți prin fricțiune, AI a oprit în tăcere această presiune.</description>
      <content:encoded><![CDATA[<p>Discuția "mai merită să înveți programare dacă există AI" o ia de la zero la fiecare două săptămâni. Mă frământ de luni de zile — la început ca întrebare teoretică, apoi ca una de care depinde ce îi spui unui junior care întreabă "de unde încep". A apărut o bază de dovezi, așa că nu mai e părere contra părere. Și, ca s-o spun blând, ambele extreme zgomotoase greșesc.</p>
<p>Nu e "înveți sau nu înveți". E despre <strong>ce anume</strong> îi predai AI-ului și ce păstrezi pentru tine. Și despre un lucru pe care AI chiar l-a schimbat — nu în sensul "s-a răsturnat totul", ci ceva mult mai subtil: <strong>a dispărut feedbackul forțat</strong> pe care realitatea însăși ți-l dădea înainte.</p>
<h2>Două extreme — ambele unilaterale</h2>
<p>Întâi să curățăm masa de cele două tabere zgomotoase care apar în fiecare discuție:</p>
<ul><li><strong>Tabăra nr. 1:</strong> "Nu mai trebuie să înveți limbaje — AI le scrie". Cei mai gălăgioși sunt cei care vând bootcamp-uri "devii programator în 30 de zile cu AI".</li><li><strong>Tabăra nr. 2:</strong> "Toată lumea se va tâmpi, în câțiva ani nu vor mai fi specialiști". Cei mai gălăgioși sunt cei experimentați, puțin speriați de propria neputință în fața vitezei cu care juniorii par productivi.</li></ul>
<p>O falsă dihotomie. În esență, nimic fundamental nu s-a schimbat: ca întotdeauna, se prețuiește <strong>înțelegerea conștientă</strong>, nu tocitul sec. AI doar adâncește prăpastia dintre cei care chiar se pricep și restul — și o face mai repede decât înainte.</p>
<h2>Ce spune baza de dovezi</h2>
<p>Nu mai e "mi se pare". Pe parcursul 2025–2026 au apărut măsurători concrete:</p>
<ul><li><strong>Nature (iunie 2026), "Is AI ruining our skills?"</strong> — sprijinul excesiv pe AI degradează competențele. Nu doar la programatori — și la medici.</li><li><strong>Anthropic RCT</strong> pe 52 de ingineri: un asistent AI produce o <strong>formare a măiestriei cu ~17% mai mică</strong>. Nuanța fină — nu suferă viteza de execuție a sarcinii, ci <em>formarea</em> competenței. Adică pe termen scurt ești mai rapid, pe termen lung — mai slab.</li><li><strong>AI &amp; Society</strong>: deskilling-ul e o problemă <strong>structurală</strong>, fără soluție de-a gata. Nu "AI e rău", ci "unealta a schimbat mediul de învățare".</li><li><strong>TechCrunch</strong>: inginerii refuză să lucreze fără AI pentru că nu mai pot fără el. Titlul: "asta li se poate întoarce împotrivă".</li></ul>
<p>Concluzia expertului K. Crowston (citat de Nature) — nu e "AI sau nu", ci: <strong>decide conștient ce competențe păstrezi pentru tine și pe care le predai</strong>. Exact pe această frază se sprijină restul articolului.</p>
<h2>Ce predai și ce păstrezi</h2>
<p>Concret — aici trece linia:</p>
<table><thead><tr><th>Poți preda către AI</th><th>Trebuie să păstrezi pentru tine</th></tr></thead><tbody><tr><td>Să-ți amintești sintaxa sau semnătura unui API</td><td>&lt;strong&gt;Să citești și să înțelegi&lt;/strong&gt; codul</td></tr><tr><td>Să scrii boilerplate de la zero</td><td>&lt;strong&gt;Să judeci&lt;/strong&gt;: e corect? e sigur? are vreun bug?</td></tr><tr><td>Să găsești rapid un exemplu</td><td>Fundamentul: cum funcționează sistemele, datele, rețeaua</td></tr></tbody></table>
<p><strong>Nu poți face review la ceva ce nu știi să citești.</strong> Așa că "să înțelegi fără să știi limbajul" e o iluzie; e încredere oarbă în AI. Și exact aici e punctul de trecere între "un specialist care folosește AI" și "un operator de AI". Primul conduce AI-ul; pe al doilea îl conduce el.</p>
<p>Încă o distincție pe care lumea o confundă mereu: <strong>pierderea competenței</strong> ≠ <strong>competența neformată</strong>. Un om cu experiență care deodată se sprijină pe AI rămâne puternic — are deja un model al lumii. Un începător care din prima zi stă 100% pe AI nu va construi niciodată acel model. Sunt probleme diferite, cu consecințe diferite.</p>
<h2>Cum să înveți corect — și de ce, în esență, nimic nu s-a schimbat</h2>
<p>Trebuie să înveți în continuare — doar că <strong>corect</strong>. Nu să toci sec termeni sau comenzi, ci să înțelegi logica și să ai o intuiție a felului în care funcționează în ansamblu. Valoarea nu stă în "a reține", ci în "a înțelege → a ști să aplici → a judeca".</p>
<p>Și cel mai important: <strong>e exact același lucru care a fost dintotdeauna, cu mult înainte de AI.</strong> Mereu au fost prețuiți cei care chiar se pricep ca specialiști, nu cei care au tocit sec. AI n-a răsturnat nimic — doar face calea leneșă mai ușoară (acum poți "să faci" fără să înțelegi), așa că prăpastia dintre aceste două tipuri doar se adâncește.</p>
<blockquote>Cine a învățat conștient — așa va rămâne. Cine a fost leneș — leneș va rămâne. AI n-a creat această diviziune — doar a scos-o la lumină.</blockquote>
<h3>Verifică pe exemple simple</h3>
<ul><li><strong>Matematică:</strong> să știi tabla înmulțirii pe de rost nu înseamnă încă "să știi matematică". Calculatorul a omorât înmulțirea în coloană, dar nu și nevoia de a înțelege ce e înmulțirea și când s-o aplici.</li><li><strong>Telefoane:</strong> nu mai ții numerele în cap — le ține telefonul minte. Dar trebuie să știi pe cine suni și de ce. Memoria ai predat-o — judecata ai păstrat-o.</li><li><strong>GPS:</strong> nu ții rutele în cap. Dar trebuie să înțelegi unde mergi — și să observi când GPS-ul te duce pe un ocol aiurea.</li></ul>
<p>În toate trei, mașina a luat <strong>memoria mecanică</strong>, nu <strong>înțelegerea și judecata</strong> — și așa a fost întotdeauna. AI e următoarea iterație a aceluiași lucru, doar cu un spectru mai larg de ce poți delega acum.</p>
<h3>Sintaxa e un produs secundar, nu scopul</h3>
<p>Asta nu înseamnă "nu trebuie să știi sintaxa". Sintaxa oricum o vei prinde — dar ca <strong>produs secundar al înțelegerii și practicii, nu ca scop</strong>. Doar n-ai învățat <code>%d</code> cu flashcards — ai înțeles logica format-string-urilor și ți-a rămas de la sine. Așa funcționează învățarea sănătoasă: înțelegi → aplici → sintaxa se așază singură. AI doar accelerează această buclă, dacă o folosești cum trebuie.</p>
<h2>Dar un lucru chiar s-a schimbat: a dispărut feedbackul forțat</h2>
<p>"Nu s-a schimbat nimic" — e adevărat cam în proporție de 80%. Un lucru important chiar s-a schimbat, și nu e o nimica toată.</p>
<p>Înainte, mediul te <strong>forța</strong> să înveți. Nu puteai să pui pe picioare cod funcțional fără să-l înțelegi măcar atât cât să meargă. Compilatorul, bug-ul, crash-ul — erau <strong>profesori</strong> care trăgeau cu forța pe toată lumea până la un nivel de bază. Lupta era obligatorie — și construia înțelegere chiar și la cei nu prea silitori.</p>
<p>Acum profesorul acesta e <strong>oprit</strong>: poți obține un rezultat funcțional <strong>fără să fi înțeles nici măcar o dată</strong> cum funcționează. Pedeapsa imediată pentru neînțelegere a dispărut. De aici o consecință pe care teza "nu s-a schimbat nimic" o ratează:</p>
<blockquote>Prăpastia nu se adâncește doar între tipuri fixe — noul mediu îi trage acolo și pe cei care înainte ar fi învățat, fiindcă acum calea leneșă livrează un rezultat funcțional fără pedeapsă imediată.</blockquote>
<p>De aceea și "leneșul rămâne leneș" e prea îngust. E mai mult despre <strong>mediu și obiceiuri</strong> decât despre fire: majoritatea reacționează la fricțiune și feedback, nu sunt leneși "din fire". Scoate fricțiunea — și chiar și cei care nu sunt leneși alunecă. Riscul nu e doar la leneși.</p>
<p>Și cel mai subtil — <strong>iluzia competenței</strong>. AI astupă golurile atât de lin încât nu te izbești de zidul propriei neștiințe și nici nu-l observi. Ți se pare că înțelegi, fiindcă "totul merge" (efectul <em>metacognitive laziness</em>). De aceea <strong>a rămâne silitor a devenit mai greu</strong>: acum îți cere să-ți creezi conștient fricțiune — uneori să oprești AI-ul, să încerci să explici sau să scrii fără el, să verifici "chiar înțeleg, sau doar cred că înțeleg". Înainte, asta o făcea realitatea în locul tău — acum e responsabilitatea ta.</p>
<blockquote>Regulile jocului sunt aceleași, dar jocul a devenit mai perfid — calea ușoară se deghizează acum în succes.</blockquote>
<h2>Pentru cei care deja se pricep — jocul e altul</h2>
<p>Tot ce e mai sus e despre <strong>formarea</strong> unei competențe de la zero. Dacă ești deja format — pentru tine tabloul e răsturnat, și merită spus răspicat, altfel articolul se citește ca "AI e rău", ceea ce nu e teza.</p>
<p>Cel cu experiență își cunoaște limitele. Vede ce i-a scos AI-ul → într-o jumătate de secundă înțelege unde e "pe lângă", unde e "da, dar nu pentru cazul ăsta", unde e "da și e ok aici și acum". Are un detector încorporat de "asta nu miroase a bine", asamblat din ani de incidente reale. AI nu oprește acel detector — dimpotrivă, îl hrănește mai repede: mai multe exemple, mai multe cazuri, mai multe patterns de filtrat rapid.</p>
<p>De aceea același AI care riscă să transforme un începător în operator transformă un senior într-un turbo-senior. Boilerplate în 30 de secunde în loc de 3 minute. Explorarea unei biblioteci noi — într-o seară, nu într-o săptămână. Portarea unui modul din Python în TypeScript — chiar fezabilă, nu "cândva, mai târziu". E nitro într-un motor deja asamblat.</p>
<p>Și încă un lucru subtil: <strong>AI nu-i poate minți pe cei cu experiență pe nesimțite</strong>. Îi minte la fel de des ca pe un începător — doar că el prinde asta înainte să termine de citit fraza. Ce pentru un începător e o capcană a iluziei de competență, pentru un senior e doar încă un bug report în fluxul zilnic.</p>
<blockquote>Pentru cei care au investit deja timp, AI e turbo. Pentru cei care încă n-au investit — e încă un motiv să n-o facă. Între aceste două stări nu există cale de mijloc — și asta face din &quot;când anume ai învățat&quot; unul dintre cei mai importanți factori dintr-o carieră.</blockquote>
<h2>Risc sistemic: &quot;scara ruptă&quot;</h2>
<p>Seniorii ies din juniori. Dacă AI mănâncă munca de junior și nimeni nu construiește fundamentul → peste 5–10 ani <strong>nu va mai fi din cine să crești un senior</strong>. Riscul unei "generații pierdute" de specialiști e real — și nu e alarmism, e o simplă problemă de piping: țeava de intrare a sistemului e blocată, cea de ieșire nu.</p>
<p>De ce nu va ajunge la zero (dar va durea): echilibrul "toți s-au oprit din învățat" e instabil. Penuria îi face pe cei care au învățat supraevaluați → stimulentul de a învăța se întoarce. Companiile care cresc conștient specialiști obțin un moat. Dar tranziția e dureroasă: operatorii se prăbușesc la primul incident serios, când AI minte clar și convingător și nu e nimeni în jur care să deosebească adevărul.</p>
<h2>Poziția mea</h2>
<ul><li>Limbajul trebuie învățat <strong>obligatoriu</strong> — cu țintă pe <em>citire și judecată</em>, nu pe tocit. Nu poți scurta drumul spre fluența în citire; AI nu-l scurtează, doar îl face pe nesimțite opțional.</li><li>Cât timp majoritatea "predă înțelegerea" către AI — <strong>a învăța acum e o fereastră de oportunitate</strong>: devii un specialist rar, care conduce AI-ul, nu care concurează cu el. E o fereastră pe termen scurt — piața se va adapta mai devreme sau mai târziu. Cât timp e deschisă — ar fi păcat să n-o folosești.</li><li>Regimul sănătos: să folosești AI ca să mergi mai repede <strong>și să înveți mai adânc</strong>, nu ca să eviți învățatul. Antipatternul — "fă-mi tu tot exercițiul". Patternul — scrii tu, AI îți explică "de ce", iar tu îi deslușești varianta înainte s-o accepți.</li><li>Un paradox pentru drum: ca să folosești AI eficient, trebuie să știi destul <strong>cât să nu ai nevoie de el la lucrurile simple</strong>. Cine depinde de AI la cele simple — la cele grele va fi păcălit de AI fără nicio șansă să observe.</li></ul>
<h2>Surse</h2>
<ul><li>Nature — <em>Is AI ruining our skills? Early results are in</em> (iunie 2026): <a href="https://www.nature.com/articles/d41586-026-01947-1">nature.com/articles/d41586-026-01947-1</a></li><li>InfoQ — Anthropic study, ~17% skill mastery drop: <a href="https://www.infoq.com/news/2026/02/ai-coding-skill-formation/">infoq.com/news/2026/02/ai-coding-skill-formation</a></li><li>TechCrunch — Coders are refusing to work without AI: <a href="https://techcrunch.com/2026/05/29/coders-are-refusing-to-work-without-ai-and-that-could-come-back-to-bite-them/">techcrunch.com/2026/05/29/coders-refusing-ai</a></li><li>AI &amp; Society — <em>AI deskilling is a structural problem</em>: <a href="https://link.springer.com/article/10.1007/s00146-025-02686-z">link.springer.com/article/10.1007/s00146-025-02686-z</a></li></ul>]]></content:encoded>
      <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
      <category>ai</category>
      <category>learning</category>
      <category>career</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Claude Code Multi-Profile, v2: o arhitectură XDG curată</title>
      <link>https://oleksiimazurenko.dev/ro/blog/claude-profiles-clean-architecture</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/claude-profiles-clean-architecture</guid>
      <description>După o lună pe vechea abordare bazată pe alias, mi-am reconstruit setup-ul multi-profile Claude Code într-o structură conformă XDG sub ~/.config/claude-profiles/. Motivul real nu a fost ordinea — a fost descoperirea că ~/.claude.json este un fișier separat în rădăcina home, unde trăiau discret serverele MCP adăugate cu --scope user.</description>
      <content:encoded><![CDATA[<p>Acum o lună am publicat <a href="/ro/blog/multiple-claude-accounts-one-device">un articol despre rularea a două conturi Claude Code în paralel pe aceeași mașină</a> — personal și de muncă, prin <code>CLAUDE_CONFIG_DIR</code> și un alias de shell. Abordarea a funcționat. Totul a făcut ce trebuia să facă.</p>
<p>Dar după o lună de uz real m-am lovit de câteva probleme serioase pe care articolul acela nu le acoperea. Cea mai mare — o ciudățenie ascunsă a Claude Code peste care am dat din întâmplare, când am adăugat MCP-ul Gmail și acesta a "dispărut" din profilul meu cinci minute mai târziu. Azi am reconstruit tot pe o arhitectură nouă — conformă XDG, cu un symlink și un selector interactiv de profil prin <code>gum</code>. Asta e v2 — o evoluție ieșită din experiență reală, nu o îmbunătățire teoretică.</p>
<h2>Ce a început să mă roadă</h2>
<p>Patru lucruri. Primele trei țin de ordine, vizibilitate și scalare. Al patrulea e adevărata capcană de arhitectură pe care n-am văzut-o imediat. Le iau pe rând, fiindcă tocmai al patrulea a forțat până la urmă reconstrucția.</p>
<h3>1. Două foldere separate în $HOME — e o dezordine</h3>
<p><code>~/.claude</code> și <code>~/.claude-promova</code> — două dotfoldere așezate unul lângă altul în rădăcina lui $HOME. XDG Base Directory Specification spune că fișierele de configurare stau în <code>~/.config/</code>. Folderele dotfile împrăștiate direct în home sunt un antipattern care, cu timpul, transformă $HOME într-un sertar de vechituri. Cosmetic, de acord, dar mă enerva de fiecare dată când vedeam <code>ls -la ~</code>.</p>
<h3>2. Nicio confirmare vizuală a profilului activ</h3>
<p>Pornesc <code>claude</code> și n-am idee care profil e activ — până nu rulez <code>claude config list</code> în interiorul sesiunii. Dacă am uitat care terminal l-am pornit unde, trebuia să verific. Un fleac, dar cu personal + muncă rulând în paralel în două taburi se adună.</p>
<h3>3. Aliasurile nu scalează</h3>
<p>Două profiluri — <code>claude</code> și <code>claude-promova</code> — e în regulă. Adaugi al treilea (un client de freelance) — îți trebuie un al treilea alias. Al patrulea — un al patrulea. După șase luni n-aș mai ține minte ce aliasuri chiar creasem.</p>
<h3>4. Capcana ascunsă din ~/.claude.json</h3>
<p>Și ăsta e adevăratul motiv al reconstrucției. Claude Code are <strong>două locuri diferite</strong> pentru configurare, iar documentația nu strigă asta: <code>~/.claude/</code> — directorul care ține <code>projects/</code>, <code>sessions/</code>, <code>hooks/</code>, <code>skills/</code>. Și separat — <code>~/.claude.json</code>, un fișier chiar în rădăcina lui $HOME, unde stau <code>oauthAccount</code>, <code>mcpServers</code>, istoricul <code>projects</code>, <code>skillUsage</code> și cam alte 40 de câmpuri de stare live reală.</p>
<p>Comanda <code>claude mcp add --scope user</code> scrie exact în acel <code>~/.claude.json</code> din rădăcina home, <strong>nu</strong> în <code>~/.claude/.claude.json</code> sau în directorul profilului. Nu știam asta. Până când, într-o zi, m-am lovit de ea.</p>
<h2>Descoperire: de ce a &quot;dispărut&quot; MCP-ul Gmail</h2>
<p>Azi-dimineață configuram MCP-ul Gmail în Claude Code. Setup standard: proiect Google Cloud, credențiale OAuth, <code>claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp</code>. Totul în regulă. Am repornit sesiunea — merge, citesc mailuri, răspund la mesaje. O oră mai târziu am început să refactorizăm aliasurile într-o funcție cu un picker de profil pe <code>gum</code>, apoi am mutat totul pe XDG. Am făcut <code>mv ~/.claude → ~/.config/claude-profiles/personal</code>, am repornit CC, am ales personal în meniu. Și în noua sesiune am deschis <code>/mcp</code>:</p>
<pre><code>figma            (failed)
playwright-test
claude.ai Notion</code></pre>
<p>Niciun gmail. Niciun vaultforge. Doar trei servere, dintre care unul chiar eșuase. Și tocmai adăugasem Gmail. Între timp, o sesiune într-alt terminal (profilul de muncă) afișa Gmail și Vaultforge fără nicio problemă.</p>
<p>Am început să sap și am găsit că aveam <strong>trei</strong> fișiere diferite pe mașină numite <code>.claude.json</code>:</p>
<table><thead><tr><th>Path</th><th>Size</th><th>mcpServers</th></tr></thead><tbody><tr><td>&lt;code&gt;~/.claude.json&lt;/code&gt; (home root)</td><td>113 KB</td><td>figma, gmail, vaultforge</td></tr><tr><td>&lt;code&gt;~/.config/claude-profiles/personal/.claude.json&lt;/code&gt;</td><td>29 KB</td><td>figma, playwright-test</td></tr><tr><td>&lt;code&gt;~/.config/claude-profiles/promova/.claude.json&lt;/code&gt;</td><td>40 KB</td><td>figma, playwright-test, vaultforge</td></tr></tbody></table>
<p>Iată. Asta e capcana de arhitectură:</p>
<ol><li><code>claude mcp add --scope user</code> scrie mereu în <code>~/.claude.json</code> din rădăcina home, indiferent de <code>CLAUDE_CONFIG_DIR</code></li><li>Când <code>CLAUDE_CONFIG_DIR</code> e setat, Claude Code citește <code>$CLAUDE_CONFIG_DIR/.claude.json</code> — fișierul din interiorul profilului</li><li>Intrările "MCP-uri user-scope" și intrările "MCP-uri de profil" stau în <strong>fișiere diferite</strong> cu același nume — și sunt ușor de confundat</li></ol>
<p>În cazul meu, <code>~/.claude.json</code> (rădăcina home, 113 KB) era starea vie, curentă — cu gmail, vaultforge, sesiunea OAuth, tot. Iar <code>~/.config/claude-profiles/personal/.claude.json</code> (29 KB) s-a dovedit a fi un snapshot vechi care stătea de mai demult în vechiul <code>~/.claude/</code> — poate o versiune mai veche de CC a scris acolo, poate un plugin. <code>jq -r 'keys[]'</code> pe ambele fișiere a arătat că versiunea din rădăcina home avea 41 de chei unice care lipseau din snapshot.</p>
<p>Iar cele 41 de chei nu sunt gunoi. E starea reală a Claude Code:</p>
<ul><li><code>skillUsage</code> — statistici de utilizare a skill-urilor</li><li><code>githubRepoPaths</code> — cache de repo-uri pentru navigarea prin proiecte</li><li><code>cachedGrowthBookFeatures</code> + <code>cachedStatsigGates</code> — feature flags (fără ele CC le aduce proaspete la fiecare pornire)</li><li><code>hasShownOpus45Notice</code>, <code>hasShownOpus46Notice</code>, <code>hasShownS1MWelcomeV2</code> — flaguri UI (fără ele, modalele reapar la următoarea pornire)</li><li><code>lastPlanModeUse</code>, <code>feedbackSurveyState</code>, <code>installMethod</code> — stare de onboarding și UX</li></ul>
<p>Dacă faci pur și simplu <code>mv ~/.claude ~/.config/claude-profiles/personal</code> fără merge, pierzi tot. Modalele de welcome revin, căutarea githubRepoPaths rulează din nou, fiecare cerere de sondaj se întoarce. Ceea ce era cât pe ce să fac.</p>
<h2>Noua arhitectură</h2>
<p>Totul trăiește sub un singur director-părinte în <code>~/.config/</code>, așa cum vrea XDG. Fiecare profil e autonom — își are starea completă, inclusiv propriul <code>.claude.json</code>. <code>~/.claude</code> rămâne ca symlink către profilul personal, pentru compatibilitate retroactivă.</p>
<pre><code>~/.config/claude-profiles/
├── personal/
│   ├── .claude.json          ← full live state + MCP list (113 KB)
│   ├── projects/, sessions/, hooks/, skills/, ...
│   └── CLAUDE.md
└── promova/
    ├── .claude.json          ← work state + work MCPs (41 KB)
    └── projects/, sessions/, agents/, skills/, ...

~/.claude  →  symlink to ~/.config/claude-profiles/personal/</code></pre>
<p>Niciun <code>.claude.json</code> în rădăcina lui $HOME. Fiecare profil e un director izolat separat, care ține totul: folderele <code>projects</code>/<code>sessions</code> și același fișier cu serverele MCP și token-urile OAuth. O singură sursă de adevăr per profil.</p>
<h3>De ce symlink-ul indică spre personal</h3>
<p>Tot ce codează hardcodat calea <code>~/.claude/</code> — scripturi vechi, plugin-uri, extensiile IDE ale Claude Code, configurări de statusline precum <code>claude-powerline.json</code> — funcționează în continuare fără modificări. Symlink-ul se rezolvă spre profilul personal. Dacă rulezi din greșeală <code>command claude</code> (ocolind funcția wrapper), tot în personal ajungi, prin căutarea pe calea implicită. Personal devine "default-ul tăcut" cum era înainte, dar acum trăiește fizic în locația XDG.</p>
<h3>Selector interactiv la pornire — funcție + gum</h3>
<p>În loc de aliasuri — o funcție <code>claude()</code> în <code>~/.zshrc</code> care afișează un meniu cu săgeți prin <code>gum</code> (helper-ul TUI de la Charm). Funcția interceptează apelul <code>claude</code> la nivel de shell, te lasă să alegi un profil și rulează <code>command claude</code> cu <code>CLAUDE_CONFIG_DIR</code>-ul potrivit. <code>command</code> contează aici — ocolește funcția wrapper și invocă binarul real.</p>
<pre><code># brew install gum

# Claude Code: profile picker on launch
claude() {
  local profile
  profile=$(gum choose \
    --header &quot;Claude profile:&quot; \
    --cursor &quot;▸ &quot; \
    --selected.foreground 212 \
    --cursor.foreground 212 \
    --header.foreground 244 \
    &quot;personal&quot; &quot;promova&quot;) || return
  case &quot;$profile&quot; in
    personal) CLAUDE_CONFIG_DIR=&quot;$HOME/.config/claude-profiles/personal&quot; command claude &quot;$@&quot; ;;
    promova)  CLAUDE_CONFIG_DIR=&quot;$HOME/.config/claude-profiles/promova&quot;  command claude &quot;$@&quot; ;;
  esac
}</code></pre>
<p>Cum arată la pornire:</p>
<pre><code>Claude profile:
▸ personal
  promova</code></pre>
<p>↑↓ pentru navigare, Enter ca să alegi, Esc ca să anulezi (Claude pur și simplu nu pornește). Profilul e mereu vizibil — nu ai cum să-l ratezi.</p>
<h2>De la zero (fără v1 de migrat)</h2>
<p>Dacă rulezi deja <a href="/ro/blog/multiple-claude-accounts-one-device">vechiul hack cu două foldere + alias</a>, sari direct la scriptul de migrare de mai jos. Secțiunea asta e construcția curată de la zero — o mașină nouă, sau pur și simplu <code>~/.claude</code>-ul implicit rezultat din uz normal. Mai întâi decide forma: un cont cu mai multe profiluri (ca să izolezi proiectele și seturile lor de MCP — cazul obișnuit), sau conturi cu adevărat diferite pentru fiecare profil (personal vs. muncă).</p>
<h3>1. Așază directoarele</h3>
<pre><code># prerequisites
brew install gum jq

# 1. XDG parent for all profiles
mkdir -p ~/.config/claude-profiles

# 2. fold an existing default install in as your first profile (no-ops on a fresh machine)
if [ -d ~/.claude ] &amp;&amp; [ ! -L ~/.claude ]; then
  mv ~/.claude ~/.config/claude-profiles/personal
  [ -f ~/.claude.json ] &amp;&amp; mv ~/.claude.json ~/.config/claude-profiles/personal/.claude.json
fi

# 3. one directory = one profile
mkdir -p ~/.config/claude-profiles/personal ~/.config/claude-profiles/work

# 4. these files will hold OAuth tokens — lock them down
chmod 600 ~/.config/claude-profiles/*/.claude.json 2&gt;/dev/null</code></pre>
<p>Un director per profil, sub un singur părinte XDG. Dacă ai folosit deja Claude Code, snippetul îți pliază <code>~/.claude</code>-ul existent ca <code>personal</code> și cară cu el și starea live din <code>~/.claude.json</code> din rădăcina home; pe o mașină cu adevărat nouă, liniile astea pur și simplu nu fac nimic. Restul sunt directoare goale care se umplu singure la prima pornire — iar <code>chmod 600</code> contează, fiindcă fișierele astea ajung să conțină token-uri OAuth.</p>
<h3>2. Autentifică fiecare profil</h3>
<pre><code># launch each profile once and authenticate it
CLAUDE_CONFIG_DIR=~/.config/claude-profiles/work claude
#   → run /login in the session, sign in, done.
# Same account for every profile? One /login is enough — macOS keeps it in the
# shared Keychain, so the other profiles are already authenticated.</code></pre>
<p>Pornește fiecare profil o dată cu propriul <code>CLAUDE_CONFIG_DIR</code> și rulează <code>/login</code>. Detaliul-cheie: dacă fiecare profil folosește <strong>același</strong> cont, te loghezi o singură dată — macOS ține credențialul într-o singură intrare partajată în Keychain, așa că orice alt profil e autentificat automat.</p>
<p><strong>Conturi diferite per profil</strong> e calea mai spinoasă: acea intrare partajată din Keychain ține un singur login, așa că logarea unui al doilea cont într-alt profil poate suprascrie primul. Dacă ăsta e cazul tău, urmează ghidul dedicat din <a href="/ro/blog/multiple-claude-accounts-one-device">rularea a două conturi Claude pe aceeași mașină</a> și verifică izolarea credențialelor pe versiunea ta de Claude Code înainte să te bazezi pe ea.</p>
<h3>3. Symlink-ul și picker-ul</h3>
<pre><code># personal becomes the default target of ~/.claude (back-compat)
ln -s ~/.config/claude-profiles/personal ~/.claude

# add the claude() gum picker from the architecture section to ~/.zshrc, then:
source ~/.zshrc
claude        # ← now shows the profile menu on every launch</code></pre>
<p>Îndreaptă <code>~/.claude</code> spre profilul personal, ca tot ce codează hardcodat calea asta să funcționeze în continuare, apoi aruncă picker-ul <code>claude()</code> cu <code>gum</code> din secțiunea de arhitectură de mai sus în <code>~/.zshrc</code>. După un reload, <code>claude</code> afișează meniul de profiluri la fiecare pornire — aceeași stare finală ca la setup-ul migrat, doar construită de la zero.</p>
<h3>4. Curatoriază MCP-ul fiecărui profil</h3>
<p>În final, dă fiecărui profil doar uneltele pe care ar trebui să le vadă: pune <code>disableClaudeAiConnectors: true</code> în <code>settings.json</code>-ul acelui profil și adaugă înapoi ce-i trebuie cu <code>claude mcp add</code>, exact ca în secțiunea despre conectori de mai sus. Asta transformă zece profiluri interschimbabile în zece construite pe scop.</p>
<h2>Script de migrare</h2>
<p>Pentru oricine citește asta și vrea să <a href="/ro/blog/multiple-claude-accounts-one-device">treacă de la schema veche</a>. Pasul cel mai critic e al doilea: face merge între <code>~/.claude.json</code> din rădăcina home și ce e deja în profilul personal, combinând listele <code>mcpServers</code>. Fără pasul ăsta, profilul își pierde și MCP-urile, și toată starea live.</p>
<pre><code># 1. Move existing dotfolders into a new XDG-style parent.
#    APFS mv is an inode rename — safe even if claude --resume is open in
#    another terminal, file descriptors stay alive on the same inode.
mkdir -p ~/.config/claude-profiles
mv ~/.claude          ~/.config/claude-profiles/personal
mv ~/.claude-promova  ~/.config/claude-profiles/promova   # rename as needed

# 2. CRITICAL: merge ~/.claude.json (the live state — likely 100+ KB) into
#    the personal profile, unioning mcpServers so no MCP is dropped.
jq -s &apos;.[0] * {mcpServers: (.[0].mcpServers + .[1].mcpServers)}&apos; \
  ~/.claude.json \
  ~/.config/claude-profiles/personal/.claude.json \
  &gt; /tmp/personal-merged.json
mv /tmp/personal-merged.json ~/.config/claude-profiles/personal/.claude.json

# 3. Fix permissions — jq+mv inherits umask (likely 644), but this file
#    holds OAuth tokens. Tighten to 600 immediately.
chmod 600 ~/.config/claude-profiles/personal/.claude.json

# 4. Back up the orphaned home-root file (delete once verified)
mv ~/.claude.json ~/.claude.json.migrated.bak

# 5. Symlink for backward compatibility (statusline configs, IDE plugins,
#    anything that hardcodes ~/.claude/ keeps working unchanged)
ln -s ~/.config/claude-profiles/personal ~/.claude</code></pre>
<p>Al treilea pas, despre permisiuni, e important în sine. <code>jq | mv</code> creează un fișier cu umask 644 (citibil de oricine). Are token-uri OAuth în el. <code>chmod 600</code> imediat după merge e obligatoriu.</p>
<p>După migrare — închide și redeschide toate sesiunile Claude active, reîncarcă shell-ul (<code>source ~/.zshrc</code> sau un terminal nou), rulează <code>claude</code>, alege un profil, verifică prin <code>claude mcp list</code> că toate MCP-urile sunt la locul lor. Dacă totul e bine — șterge <code>~/.claude.json.migrated.bak</code>. Dacă ceva nu e în regulă — rollback-ul e trivial: <code>mv ~/.claude.json.migrated.bak ~/.claude.json</code> și scoate symlink-ul.</p>
<h2>Bonus: conectori MCP per profil (când organizația ți-i partajează)</h2>
<p>Arhitectura de mai sus deblochează o a doua axă de izolare — și a ajuns lucrul pe care de fapt îl folosesc cel mai mult. Dacă contul tău Claude aparține unei organizații, aceasta poate distribui un set partajat de <strong>conectori MCP remote</strong> (Figma, un tracker de tichete, un backend de metrici, un gateway de bază de date, un CMS…). Claude Code aduce automat <strong>fiecare</strong> conector activat de organizație în <strong>fiecare</strong> sesiune, fiindcă sunt legați de cont, nu de directorul din care ai pornit.</p>
<p>Cu doi-trei conectori e în regulă. Cu zece proiecte și o duzină de conectori de organizație încetează să mai fie: fiecare sesiune încarcă unelte de care n-ar avea nicio treabă să se atingă. E zgomot în lista de unelte a modelului, e context irosit și — partea care chiar doare — lasă o sesiune să întindă mâna într-un sistem care aparține altui proiect. Conectorul pentru proiectul A n-ar trebui nici măcar să existe cât timp lucrezi la proiectul B.</p>
<h3>Dezactivează auto-fetch-ul, apoi curatoriază</h3>
<p>Pârghia e o singură setare, <code>disableClaudeAiConnectors</code> (Claude Code v2.1.182+). Trăiește în orice scope de setări — inclusiv în <code>settings.json</code>-ul propriu al unui profil — deci e <strong>per-<code>CLAUDE_CONFIG_DIR</code></strong>. Pune-o pe <code>true</code> și profilul acela încetează complet să mai aducă automat conectorii de organizație:</p>
<pre><code>{
  &quot;disableClaudeAiConnectors&quot;: true
}</code></pre>
<p>Apoi adaugi înapoi doar conectorii de care profilul chiar are nevoie. Serverele adăugate explicit <strong>nu</strong> sunt atinse de flag, deci se încarcă normal — rezultatul e un subset curatoriat, profil cu profil:</p>
<pre><code># add back only what this profile uses — writes to the profile&apos;s own .claude.json
claude mcp add --scope user --transport http figma  https://&lt;your-figma-connector&gt;/mcp
claude mcp add --scope user --transport http linear https://&lt;your-linear-connector&gt;/mcp</code></pre>
<table><thead><tr><th>Profil</th><th>Ce încarcă</th></tr></thead><tbody><tr><td>&lt;code&gt;frontend&lt;/code&gt;</td><td>Figma, Linear, Sentry</td></tr><tr><td>&lt;code&gt;backend&lt;/code&gt;</td><td>Postgres, Grafana, Linear</td></tr><tr><td>&lt;code&gt;content&lt;/code&gt;</td><td>Notion, Figma, Linear</td></tr></tbody></table>
<p>Același cont, același catalog de organizație — dar fiecare profil vede doar felia lui, plus conectorii comuni pe care alegi să-i repeți (un tracker de tichete de obicei își merită locul peste tot).</p>
<h3>L-am testat înainte să mă încred în el</h3>
<p>N-am vrut să iau documentația de bună, așa că am rulat un A/B curat pe un singur profil — același cont, același token OAuth, singura diferență fiind acel unic <code>settings.json</code>:</p>
<pre><code>profile WITHOUT the flag
  /mcp → Figma, Linear, Grafana, Postgres, Notion, Sentry, …   (entire org catalog)

same account, profile WITH  disableClaudeAiConnectors: true
  /mcp → (nothing auto-loaded)

…then  claude mcp add  Figma + Linear
  /mcp → Figma, Linear                                         (exactly the curated subset)</code></pre>
<p>Face exact ce promite. Două avertismente pe care merită să le știi înainte să te bazezi pe el:</p>
<ul><li><strong>Readăugarea unui conector OAuth cere un singur login interactiv.</strong> Conectorii care se autentifică prin cheie-în-URL revin complet automat prin <code>claude mcp add</code>. Conectorii din spatele OAuth vor cere o autentificare unică în browser prima dată când îi folosești în acel profil.</li><li><strong><code>claude mcp list</code> nu afișează deloc conectorii de organizație</strong> — doar serverele definite local și adăugate explicit. Cei aduși automat sunt vizibili doar într-o sesiune vie, prin <code>/mcp</code>. Nu te panica atunci când <code>mcp list</code> pare golit de ei.</li></ul>
<h2>Ce câștigi</h2>
<ul><li>Un singur director-părinte în loc de două dotfoldere în $HOME — conform XDG</li><li>Symlink-ul păstrează compatibilitatea cu tot ce codează hardcodat <code>~/.claude/</code></li><li>Fiecare profil e autonom — starea lui completă și MCP-urile lui trăiesc în propriul <code>.claude.json</code></li><li>O singură sursă de adevăr per profil — gata cu configurarea orfană din rădăcina home care se depărtează tăcut de cea din profil</li><li>Datele sensibile (oauthAccount, token-uri) au garantat permisiuni 600</li><li>Confirmare vizuală a profilului activ la fiecare pornire — nu ai cum să uiți care profil e activ</li><li>Adăugarea unui al treilea profil = adăugarea unei singure linii în <code>case</code>-ul funcției, nu clonarea unui nou alias și ținerea minte a numelui lui</li></ul>
<h2>Unde lasă de dorit</h2>
<p>Vreau să fiu sincer. Nu e o îmbunătățire gratis — au venit la pachet câteva compromisuri și merită știute din start.</p>
<ul><li><strong><code>gum</code> e o dependență în plus</strong> (<code>brew install gum</code>, ~13 MB). Dacă chiar nu vrei să-l instalezi — poți cădea înapoi pe <code>select</code> din zsh sau pe un simplu <code>read</code>. Merge, dar nu arată la fel de bine și n-are navigare cu săgeți.</li><li><strong>O apăsare de Enter la fiecare pornire.</strong> Pentru cine dă drumul la <code>claude</code> de zeci de ori pe zi, asta poate deveni enervant. Alternativa mai jos (direnv).</li><li><strong>Symlink-ul <code>~/.claude → personal</code> face personal-ul default.</strong> Dacă îți trebuie profilul de muncă drept default, trebuie să redirecționezi symlink-ul (<code>ln -sf</code>). Nu e greu, dar nu e nici "uită de el și nu se strică nimic".</li><li><strong>Symlink-ul s-ar putea, teoretic, strica</strong> dacă vreo unealtă rescrie atomic <code>~/.claude.json</code> printr-un pattern temp+rename (write-file-atomic). În practică, Claude Code însuși nu face asta, dar dacă instalezi plugin-uri terțe — verifică.</li><li><strong>Dacă ai conturi Anthropic diferite pe profiluri diferite, cu planuri diferite</strong> — poate exista un lag sub o secundă după comutare, cât timp Claude Code sincronizează starea OAuth. În uzul meu nu se simte, dar nu e zero.</li></ul>
<h2>Alternative pe care le-am luat în calcul</h2>
<p><strong>direnv</strong> — setează automat <code>CLAUDE_CONFIG_DIR</code> pe baza unui <code>.envrc</code> din rădăcina fiecărui proiect. Zero interacțiune, zero clicuri. Dezavantaj: trebuie să pui un <code>.envrc</code> în fiecare work root, iar dacă rulezi <code>claude</code> într-un folder nerecunoscut, primești profilul default (care s-ar putea să nu fie cel dorit). Pentru cine trăiește într-un set restrâns de work roots și vrea să nu dea niciodată vreun clic, direnv e realmente mai bun.</p>
<p><strong>Comutarea pe bază de symlink</strong> (un singur profil activ prin redirecționarea symlink-ului <code>~/.claude</code>) am luat-o și eu în calcul și am renunțat imediat. Nu poți avea două terminale cu profiluri diferite deschise în același timp — "curentul" global e unic. Pentru mine, ăsta e un deal-breaker.</p>
<h2>Concluzie</h2>
<p>v2 nu e doar un UX mai bun peste v1. E o recunoaștere a faptului că Claude Code are o ciudățenie de arhitectură ascunsă (<code>~/.claude.json</code> ca fișier separat în rădăcina home, scris de comenzile <code>--scope user</code> indiferent de <code>CLAUDE_CONFIG_DIR</code>) de care trebuie să ții cont dacă vrei izolare reală între profiluri. <a href="/ro/blog/multiple-claude-accounts-one-device">Prima abordare (<code>~/.claude</code> + <code>~/.claude-promova</code> + alias)</a> mergea în proporție de 80%, dar restul de 20% se manifesta ca o derivă tăcută a stării între profiluri. Acum e luat în calcul. Dacă abia începi — pornește direct cu v2. Dacă ești deja pe v1 — scriptul de migrare e mai sus, mutarea durează cinci minute și nu strică nimic (tocmai merge-ul cu <code>jq</code> e pasul care te salvează de la pierderea stării).</p>]]></content:encoded>
      <pubDate>Sat, 13 Jun 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>productivity</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Cum să scrii agenți Claude Code care nu te mint</title>
      <link>https://oleksiimazurenko.dev/ro/blog/writing-specialized-agents</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/writing-specialized-agents</guid>
      <description>Două reguli pentru pipeline-uri de agenți Claude Code de încredere: un agent per specializare și comenzi de shell în locul prompturilor oriunde sunt implicate răspunsuri cantitative.</description>
      <content:encoded><![CDATA[<p>I-ai cerut lui Claude Code să <em>implementeze acest design și să verifice că se potrivește cu mockup-ul din Figma</em>. A revenit cu: <em>Gata. Toate secțiunile se potrivesc, spațierile sunt corecte, culorile sunt bune.</em> Ai deschis pagina. Jumătate din spațieri erau greșite. Starea de hover nu exista. Butoanele erau cu o nuanță pe lângă. Modelul nu a mințit din răutate — a prezis că vrei să auzi <em>verificat</em>, așa că a produs exact acea secvență de tokeni. Nu a existat niciun pas de verificare. Nici nu putea exista — verificarea necesită o comparație cu ground truth, iar un singur agent într-un singur context nu are cum să iasă din propriul răspuns și să se controleze pe sine.</p>
<p>Două reguli mi-au transformat fluxurile de lucru pline de halucinații în pipeline-uri de încredere: <strong>un agent, o specializare</strong> și <strong>tot ce poate rula ca o comandă de shell trebuie să ruleze ca o comandă de shell</strong>. Nu e teorie. Este ceea ce fac în fiecare zi cu Claude Code, iar acestea sunt tiparele care chiar contează.</p>
<h2>De ce mint agenții generaliști</h2>
<p>LLM-urile sunt predictori ai următorului token. Când un prompt cere două roluri — <em>construiește X</em> și <em>verifică X</em> — modelul termină primul rol, apoi prezice cum ar arăta rezultatul celui de-al doilea, fără să îl execute cu adevărat. Autoverificarea este slabă din punct de vedere structural: același context, același model, aceleași puncte oarbe. <em>Trecut</em> la verificare corelează cu <em>trecut</em> la construire — eșuează împreună.</p>
<p>Modelul nu știe că minte. Din perspectiva lui, a povesti <em>am verificat totul cu atenție</em> este o continuare coerentă a faptului că a scris codul. Din același motiv prompturile de tip <em>ești sigur?</em> nu prind halucinațiile: modelul este la fel de sigur la a doua trecere. Încrederea nu corelează cu corectitudinea — corelează cu cât de plauzibil sună propoziția următoare.</p>
<p>Soluția nu sunt <em>prompturi mai bune</em>. <em>Fii atent</em>, <em>verifică din nou</em>, <em>nu halucina</em> — aceste instrucțiuni nu fac nimic. Soluția este structurală: specializează agentul astfel încât să nu poată pretinde fizic că a făcut ceva și direcționează munca cantitativă prin shell, astfel încât răspunsul să vină din starea reală, nu din probabilitatea tokenilor.</p>
<h2>Regula 1: un agent, o specializare</h2>
<p>Împarte munca în agenți separați cu contexte separate. Fiecare agent are o singură responsabilitate și un set restrâns de unelte. Întregul flux devine o ștafetă în loc de un singur agent care aleargă în cerc:</p>
<ul><li><strong>Agentul builder:</strong> ia specificația, scrie codul. Asta e toată treaba lui. Are <code>Read</code>, <code>Edit</code>, <code>Write</code>, <code>Bash</code>.</li><li><strong>Agentul reviewer:</strong> ia specificația plus diff-ul, verifică criteriile de acceptanță. Context nou. Nu știe <em>cum</em> a fost scris codul, doar ce a ieșit. Are <code>Bash</code>, <code>Read</code>, <code>Grep</code>, <code>Glob</code> — niciun fel de unelte de scriere.</li><li><strong>Agentul de analytics:</strong> răspunde la întrebări despre date construind și rulând interogări. Doar <code>Bash</code>. Nu poate ajunge la răspuns fără să ruleze o comandă reală.</li><li><strong>Orchestratorul:</strong> sesiunea principală care trimite fiecare agent pe rând și niciodată nu cere unui agent să facă treaba altuia.</li></ul>
<p>Exemplu concret: implementarea unui UI plus o verificare vizuală față de un mockup Figma. Builderul scrie componentele și face commit la diff. Apoi orchestratorul invocă reviewerul cu URL-ul designului, diff-ul și criterii de acceptanță explicite. Reviewerul rulează Playwright, face capturi de ecran, le compară cu referința și returnează <code>PASS</code> sau <code>FAIL</code> cu căile reale ale capturilor și diferențele la nivel de pixel. Builderul nu se apropie niciodată de pasul de verificare — și tocmai de aceea verificarea este reală.</p>
<p>Antitiparul este mega-agentul: un singur prompt care spune <em>construiește acest UI și asigură-te că se potrivește cu mockup-ul</em>. Îți garantez că va raporta că totul se potrivește. Nu se potrivește. Narațiunea <em>am verificat</em> este pur și simplu cea mai probabilă secvență de tokeni după <em>l-am construit</em>.</p>
<h2>Regula 2: shell în loc de prompt, întotdeauna</h2>
<p>Orice este cantitativ, orice atinge starea reală, orice unde răspunsul poate fi greșit într-un fel care pare corect — trimite-l prin <code>sh</code>. Treaba agentului este să construiască și să ruleze comanda, apoi să îi citească rezultatul. Agentul nu este sursa adevărului. Rezultatul shell-ului este.</p>
<ul><li><strong>Numărare:</strong> <code>wc -l logs.txt</code> este adevărat. <em>Sunt aproximativ 47 de linii de log</em> spus de un model este o halucinație.</li><li><strong>Analytics:</strong> <code>psql -c "SELECT count(*) FROM events WHERE created_at &gt; now() - interval '30 days'"</code>. Nu <em>estimează volumul</em>.</li><li><strong>Teste:</strong> <code>pnpm test --reporter=json | jq '.numFailedTests'</code>. Nu <em>rezumă ce a eșuat</em>.</li><li><strong>Starea Git:</strong> <code>git rev-list --count main..HEAD</code>, <code>git diff --stat</code>. Nu <em>numără commit-urile</em> sau <em>descrie modificările</em>.</li></ul>
<p>Odată ce interiorizezi asta, începi să observi fiecare loc unde agentul era pe cale să inventeze un număr. <em>Se pare că sunt vreo 200 de înregistrări...</em> — nu. Rulează <code>SELECT count(*)</code>. <em>Majoritatea testelor trec...</em> — nu. Rulează suita de teste, parsează JSON-ul. Modelul este excelent la construirea comenzii. Este nesigur când vine vorba să fie comanda.</p>
<h2>Moduri de eșec pe care le-am întâlnit chiar eu</h2>
<p>Nu sunt scenarii ipotetice. Fiecare dintre acestea m-a costat timp real înainte să schimb tiparul:</p>
<ul><li><strong>Verificare fantomă.</strong> Agentul a spus <em>am verificat toate cele 14 secțiuni față de mockup</em>. Nu a deschis mockup-ul. Nu a făcut nicio captură de ecran. Verificarea a fost un pas halucinat din narațiune.</li><li><strong>Numere greșite spuse cu încredere.</strong> Am cerut monthly active users din datele de analytics. Am primit un număr greșit de aproximativ 3×. Modelul a interpolat din rânduri de eșantion în loc să ruleze interogarea reală.</li><li><strong>Modificări de fișiere inventate.</strong> Agentul a spus <em>am actualizat <code>config/feature-flags.json</code></em>. Nu o făcuse. Doar intenționase. <code>git diff</code> era gol.</li><li><strong>Rulări de teste false.</strong> <em>Toate testele trec.</em> Niciun test nu a fost executat. Agentul nu a invocat niciodată test runner-ul — a prezis cum ar fi arătat rezultatul acestuia.</li></ul>
<p>Toate patru se rezolvă cu aceleași două reguli: împarte agentul, împinge în shell. Reviewerul nu are <code>Write</code>, deci nu poate falsifica editarea fișierelor. Agentul de analytics are doar <code>Bash</code>, deci nu poate returna un număr care nu a venit dintr-o interogare. Imposibilitatea structurală bate bunele intenții de fiecare dată.</p>
<h2>Cum structurezi asta în Claude Code</h2>
<p>Claude Code acceptă sub-agenți definiți în <code>.claude/agents/*.md</code>. Fiecare fișier de agent declară un nume, o descriere, un set de unelte permise și un system prompt. Orchestratorul (sesiunea ta principală) îi trimite folosind unealta <code>Agent</code>. Iată genul de definiție pe care îl folosesc pentru reviewer — scurtă, îngustă și incapabilă fizic să scrie cod:</p>
<pre><code>---
name: reviewer
description: Reviews a diff against acceptance criteria. Cannot edit code.
tools: Bash, Read, Grep, Glob
---

You are a strict code reviewer. You receive:
- A diff (already produced by the builder)
- Acceptance criteria

Your job:
1. Run the build, the tests, the linter — through Bash.
2. Read the changed files directly.
3. Compare the actual behavior to the criteria.
4. Report PASS or FAIL with concrete evidence (command output, file excerpts).

You must NOT:
- Trust the builder&apos;s summary
- Assume anything was verified just because it was claimed
- Mark something PASS without running the actual check</code></pre>
<p>Observă setul de unelte: <code>Bash, Read, Grep, Glob</code>. Niciun <code>Write</code>, niciun <code>Edit</code>, niciun <code>Agent</code>. Reviewerul poate rula comenzi, citi fișiere, căuta tipare — și nimic altceva. Dacă încearcă să dea un diff halucinat drept <em>verificat</em>, forma apelurilor lui de unelte face asta evident: nu au existat verificări reale. Poți audita apelurile de unelte și vezi exact ce a fost inspectat.</p>
<p>Tiparul de orchestrare: sesiunea principală apelează Builderul → așteaptă → rulează ea însăși <code>git diff</code> pentru a captura modificarea reală → apelează Reviewerul cu specificația și diff-ul → citește verdictul. Sesiunea principală nu cere niciodată unui singur agent să facă ambele. Restricțiile de unelte sunt mai puternice decât instrucțiunile din prompt: <em>nu falsifica verificarea</em> este o dorință. Faptul de a nu avea <code>Write</code> este un fapt.</p>
<h2>Antitipare de abandonat</h2>
<p>Lucruri pe care le văd în prompturi și care nu fac nimic — sau, mai rău, dau un fals sentiment de siguranță:</p>
<ul><li><strong><em>Fii atent și verifică-ți munca din nou.</em></strong> Nu generează niciun comportament suplimentar. Modelul produce deja ceva ce arată ca muncă atentă.</li><li><strong><em>Asigură-te că verifici cu adevărat.</em></strong> Cuvintele <em>cu adevărat</em> nu adaugă o semantică pe care modelul să poată acționa. Va pretinde <em>cu adevărat</em> că a verificat.</li><li><strong><em>Nu halucina.</em></strong> Un meme din prompt engineering. Halucinația nu este un întrerupător pe care modelul îl poate opri.</li><li><strong>Să te încrezi în agent la numerele <em>mici</em>.</strong> La numerele mici minte cu cea mai mare încredere. Nu există un prag minim de onestitate.</li><li><strong>Să adaugi mai multe reguli în prompt pentru a <em>forța</em> onestitatea.</strong> Corecțiile structurale (împărțire + shell) bat ajustările de prompt de fiecare dată. Dacă o regulă trebuie impusă, codific-o în accesul la unelte, nu în limba română.</li></ul>
<p>Dacă strategia ta de a prinde halucinațiile este o formulare mai emfatică, nu ai o strategie. Ai o speranță.</p>
<h2>Modelul mental</h2>
<p>Un agent nu este un coleg. Este o funcție: <code>prompt → tokens</code>. Funcția este excelentă la scrierea codului și groaznică la a introspecta dacă a făcut lucrul corect. Tratează afirmațiile ei despre propria muncă drept o ipoteză. Diff-ul, codul de ieșire, captura de ecran, numărul de rânduri — acestea sunt dovezile. Rezumatul de la finalul turei este suprafața cea mai predispusă la minciună din tot sistemul.</p>
<p>Specializarea este asigurarea ta împotriva derivei narative. Shell-ul este singura ta sursă de adevăr. Builderul scrie. Reviewerul verifică. Bash decide.</p>
<h2>Concluzie</h2>
<p>Dacă reții un singur lucru: nu lăsa un singur agent să își producă și să își judece propriul rezultat și nu lăsa niciun agent să răspundă la o întrebare cantitativă fără să ruleze o comandă. Tot restul decurge din aceste două reguli. Configurează accesul la unelte agresiv, auditează apelurile de unelte în loc de rezumate, iar suprafața halucinațiilor se micșorează de la <em>peste tot</em> la <em>câteva locuri specifice pe care deja știi să le verifici</em>.</p>]]></content:encoded>
      <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>agents</category>
      <category>ai</category>
      <category>prompt-engineering</category>
      <category>automation</category>
    </item>
    <item>
      <title>Mai multe conturi Claude Code pe un singur dispozitiv</title>
      <link>https://oleksiimazurenko.dev/ro/blog/multiple-claude-accounts-one-device</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/multiple-claude-accounts-one-device</guid>
      <description>Cum să folosești două (sau mai multe) conturi Claude Code în paralel — personal și corporativ — cu izolare completă folosind o singură variabilă de mediu.</description>
      <content:encoded><![CDATA[<p>Folosesc Claude Code zilnic — și pentru proiecte personale, și pentru munca de la firmă. Problema: sunt două conturi complet diferite, cu sesiuni OAuth diferite, instrucțiuni CLAUDE.md diferite, servere MCP diferite și memorie de proiect separată. Iată cum le rulez în paralel pe același dispozitiv cu un singur alias de shell pe o linie.</p>
<h2>Problema</h2>
<p>În mod implicit, Claude Code stochează totul în <code>~/.claude</code> — tokenul OAuth, istoricul conversațiilor, fișierul global CLAUDE.md, memoria proiectelor, configurările serverelor MCP și setările. Când ai două conturi, ai nevoie de două lumi complet separate:</p>
<ul><li><strong>Contul personal:</strong> abonamentul tău Max/Pro, un CLAUDE.md personal cu preferințele tale, serverele tale MCP (Obsidian, unelte personale)</li><li><strong>Contul corporativ:</strong> planul gestionat de companie, un CLAUDE.md de lucru cu instrucțiuni de integrare Jira/Slack, serverele MCP ale companiei</li><li><strong>Sesiuni OAuth diferite:</strong> nu poți fi autentificat pe două conturi în același director de configurare</li><li><strong>Memorie de proiect separată:</strong> nu vrei ca contextul proiectelor de la muncă să se scurgă în sesiunile personale și invers</li></ul>
<p>Să te deconectezi și să te reconectezi de fiecare dată când schimbi contextul nu e o opțiune. Pierzi starea sesiunii, iar procesul e pur și simplu enervant.</p>
<h2>Soluția: CLAUDE_CONFIG_DIR</h2>
<p>Claude Code respectă o singură variabilă de mediu: <code>CLAUDE_CONFIG_DIR</code>. Setează-o pe orice cale, iar Claude va folosi acel director în locul lui <code>~/.claude</code> pentru tot — autentificare, istoric, setări, memorie, absolut tot. Întreaga configurare durează 60 de secunde.</p>
<h3>Pasul 1: Creează un al doilea director de configurare</h3>
<p>Alege un nume care are sens pentru cazul tău de utilizare:</p>
<pre><code>mkdir ~/.claude-work</code></pre>
<p>Atât. Claude îl va popula cu structura necesară la prima pornire.</p>
<h3>Pasul 2: Autentifică al doilea cont</h3>
<p>Rulează Claude o dată cu noul director de configurare pentru a declanșa autentificarea OAuth:</p>
<pre><code>CLAUDE_CONFIG_DIR=~/.claude-work claude</code></pre>
<p>Se deschide browserul. Autentifică-te cu contul corporativ. Tokenul OAuth se salvează în <code>~/.claude-work</code> — complet separat de sesiunea ta personală din <code>~/.claude</code>.</p>
<h3>Pasul 3: Adaugă un alias de shell</h3>
<p>Adaugă asta în configurarea shell-ului ca să nu fii nevoit să ții minte variabila:</p>
<pre><code>alias claude-work=&apos;CLAUDE_CONFIG_DIR=~/.claude-work claude&apos;</code></pre>
<p>Reîncarcă shell-ul:</p>
<pre><code>source ~/.zshrc</code></pre>
<h2>Ce obții</h2>
<p>Acum ai două medii Claude complet izolate:</p>
<ul><li><strong><code>claude</code></strong> — pornește cu contul personal, CLAUDE.md-ul personal, memoria personală</li><li><strong><code>claude-work</code></strong> — pornește cu contul corporativ, CLAUDE.md-ul de lucru, memorie separată</li><li><strong>Istoric izolat:</strong> conversațiile de la muncă rămân la muncă, cele personale rămân personale</li><li><strong>Servere MCP separate:</strong> MCP-ul personal pentru vault-ul tău Obsidian nu apare în sesiunile de lucru</li><li><strong>Setări independente:</strong> unelte permise diferite, niveluri de permisiuni diferite, preferințe de model diferite pentru fiecare cont</li></ul>
<h2>Cum funcționează în culise</h2>
<p>Directorul de configurare este sursa unică de adevăr pentru starea lui Claude Code. Iată ce se află înăuntrul fiecăruia:</p>
<pre><code>~/.claude/              ← personal account
├── CLAUDE.md           ← personal global instructions
├── projects/           ← personal project memory
├── settings.json       ← personal permissions &amp; MCP servers
└── ...                 ← OAuth session, history, cache

~/.claude-work/         ← corporate account
├── CLAUDE.md           ← company-specific instructions (Jira, Slack, etc.)
├── projects/           ← separate project memory
├── settings.json       ← different MCP servers, permissions
└── ...                 ← separate OAuth session</code></pre>
<p>Când rulezi <code>claude-work</code>, Claude citește totul din <code>~/.claude-work</code>. Habar nu are că <code>~/.claude</code> există. Cele două instanțe sunt complet independente — le poți rula chiar simultan în file diferite ale terminalului.</p>
<h2>Scalarea la N conturi</h2>
<p>Modelul se extinde la orice număr de conturi. Ești freelancer cu mai mulți clienți? Adaugă mai multe alias-uri:</p>
<pre><code># Personal (default — no alias needed)
# Just run: claude

# Corporate
alias claude-work=&apos;CLAUDE_CONFIG_DIR=~/.claude-work claude&apos;

# Freelance client
alias claude-client=&apos;CLAUDE_CONFIG_DIR=~/.claude-client claude&apos;</code></pre>
<p>Fiecare alias primește propriul director de configurare, propria sesiune OAuth, propriul CLAUDE.md cu instrucțiuni specifice clientului. Poți avea un CLAUDE.md pentru Clientul A care cunoaște convențiile codului și proiectul lui Jira și unul complet diferit pentru Clientul B.</p>
<h2>Sfaturi practice</h2>
<ul><li><strong>Numește directoarele clar:</strong> <code>~/.claude-work</code>, <code>~/.claude-clientname</code> — o să-ți mulțumești singur când ai trei sau patru</li><li><strong>Scrie un CLAUDE.md croit pentru fiecare:</strong> cel de lucru poate include instrucțiuni specifice companiei (cum se creează tichete Jira, ce canale Slack să referențieze, proceduri de deployment). Cel personal rămâne suplu.</li><li><strong>Servere MCP diferite pentru fiecare cont:</strong> configurează uneltele de lucru (Jira MCP, Slack MCP, API-uri interne) doar în configurarea de lucru. Ține-o pe cea personală curată.</li><li><strong>Verifică ce cont e activ:</strong> rulează <code>claude config list</code> într-o sesiune dacă nu ești sigur pe ce cont ești — îți arată calea directorului de configurare</li></ul>
<h2>Unde această abordare dă greș</h2>
<p><code>CLAUDE_CONFIG_DIR</code> izolează pe <em>cont</em>, nu pe <em>proiect</em>. În interiorul unui singur profil, Claude vede fiecare server MCP pe care l-ai înregistrat vreodată pentru acel cont — din toate proiectele tale. Pentru uz personal solo, de obicei e în regulă. În momentul în care ai mai multe proiecte critice pentru producție sub un singur cont, mai ales în domenii care se suprapun precum facturarea, uneltele de administrare sau infrastructura, apare un risc concret între proiecte: un asistent AI poate apela o unealtă din proiectul A în timp ce lucrează la proiectul B, mai ales când ambele expun operațiuni cu nume asemănătoare.</p>
<p>Modelul cu profile răspunde la întrebarea <em>pe ce cont sunt?</em>. Nu răspunde la întrebarea <em>uneltele cărui proiect ar trebui să fie active acum?</em>. Pentru munca cu miză mai mare, adaugă un al doilea strat de izolare peste separarea pe conturi:</p>
<ul><li><strong>Un profil per proiect critic pentru producție, nu doar per cont:</strong> în loc de <code>~/.claude</code> și <code>~/.claude-work</code>, creează <code>~/.claude-work-billing</code> și <code>~/.claude-work-admin</code>. Fiecare profil vede doar serverele MCP de care are realmente nevoie.</li><li><strong>MCP la nivel de proiect prin <code>.mcp.json</code>:</strong> comite un <code>.mcp.json</code> în rădăcina proiectului care listează doar serverele MCP ale acelui proiect. Claude le preia când e pornit din acel director. Ține-ți configurarea globală minimală — doar unelte universale (notițe, căutare), niciun endpoint de producție.</li><li><strong>Numește serverele MCP fără ambiguitate:</strong> evită nume generice precum <code>admin</code>, <code>billing</code>, <code>mcp-server</code>. Prefixează cu numele proiectului: <code>acme_billing_prod</code>, <code>acme_admin_stage</code>. Un nume descriptiv te obligă la o pauză când ceva e pe cale să fie apelat din contextul greșit.</li><li><strong>Verifică fiecare apel de unealtă MCP înainte să-l aprobi:</strong> apeluri precum <code>*_create_*</code>, <code>*_delete_*</code>, <code>*_charge_*</code> merită o a doua privire deliberată. Orice viteză câștigi din auto-aprobarea în bloc se evaporă prima dată când o unealtă din proiectul greșit trage în producție.</li></ul>
<p>Regula generală: împarte profilurile agresiv, ține MCP-urile de nivel producție în afara profilului implicit și tratează suprapunerea numelor de unelte între proiecte ca pe un semnal că merită refactorizat.</p>
<h2>Concluzie</h2>
<p>O singură variabilă de mediu. Un singur alias. Izolare completă între conturi. Fără dansul logout/login, fără conflicte de configurare, fără scurgeri de context. E genul de soluție aproape dezamăgitor de simplă — dar tocmai asta o face bună. Configureaz-o o dată și nu te mai gândi niciodată la ea.</p>]]></content:encoded>
      <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
      <category>claude-code</category>
      <category>productivity</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Cum conectezi Claude Desktop la Obsidian — O călătorie prin 4 servere MCP</title>
      <link>https://oleksiimazurenko.dev/ro/blog/claude-obsidian-mcp-servers</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/claude-obsidian-mcp-servers</guid>
      <description>O poveste reală despre găsirea unei modalități stabile de a automatiza refactoring-ul vault-ului Obsidian prin Claude. Ce s-a stricat, ce a funcționat și de ce VaultForge s-a dovedit a fi singura opțiune funcțională.</description>
      <content:encoded><![CDATA[<p>Imaginează-ți: ai peste 400 de note în Obsidian, adunate de-a lungul anilor. Totul e împrăștiat prin rădăcina vault-ului, conceptele amestecate cu notele tehnice, există duplicate (<code>ideas.md</code> și un folder <code>ideas/</code> cu 13 fișiere înăuntru), niciun sistem. Vrei să faci ordine — să construiești o arhitectură de foldere ca lumea, să adaugi fișiere MOC, să organizezi tag-urile. Manual e plictisitor și lent. Gândul firesc: <strong>conectezi Claude la Obsidian prin MCP și lași AI-ul să facă refactoring-ul</strong>. S-a dovedit — e un drum printr-un câmp minat. Iată prin ce a trebuit să trec ca să ajung la o soluție funcțională.</p>
<h2>Ce este MCP și de ce nu e chiar atât de simplu</h2>
<p>MCP (Model Context Protocol) este un protocol deschis de la Anthropic care îi permite lui Claude să se conecteze la instrumente și date externe. Principiul e simplu: rulează un server local care expune "instrumente" (tools), iar Claude le apelează în timpul conversației.</p>
<p>Pentru Obsidian există teoretic o grămadă de servere MCP. În practică — fiecare are problemele lui.</p>
<p><strong>Problema principală a ecosistemului Obsidian:</strong> Obsidian este o aplicație închisă, fără MCP oficial. Comunitatea a umplut golul, dar fiecare implementare o ia pe drumul ei și niciuna nu are "binecuvântarea oficială".</p>
<h2>Încercarea 1: MarkusPfundstein/mcp-obsidian</h2>
<p>Primul instrument pe care îl găsești când cauți. 3.400 de stele pe GitHub, prezent în toate tutorialele. Pare o alegere sigură.</p>
<p><strong>Cum funcționează:</strong> server Python bazat pe pluginul Local REST API din Obsidian. Serverul comunică cu pluginul prin HTTPS, iar pluginul execută operațiile prin Obsidian API.</p>
<h3>Ce a mers prost</h3>
<ul><li>Nu a mai fost actualizat de 17 luni</li><li>85 de issue-uri deschise</li><li><strong>Fără <code>move</code>/<code>rename</code></strong> — doar read, write, append, delete</li><li>Local REST API are un bug documentat de pierdere de date: endpoint-ul POST poate suprascrie în tăcere un fișier la append</li></ul>
<p>Nu e potrivit pentru refactoring — avem nevoie să mutăm fișiere și să păstrăm link-urile. Mergem mai departe.</p>
<h2>Încercarea 2: aaronsb/obsidian-mcp-plugin</h2>
<p>Am găsit o variantă care funcționează ca <strong>plugin nativ de Obsidian</strong>. Asta înseamnă acces direct la API-ul intern al Obsidian — backlinks, Dataview, graful de legături. Move-ul prin API-ul nativ actualizează automat toate wiki-link-urile, pentru că Obsidian se ocupă singur de asta.</p>
<h3>Dificultăți la configurare</h3>
<ul><li>Pluginul <strong>nu se află în catalogul oficial Obsidian</strong> (PR-ul stă în așteptare, cu erori de validare)</li><li>Trebuie instalat prin <strong>BRAT</strong> (Beta Reviewers Auto-update Tool)</li><li>Claude Desktop nu acceptă Bearer token direct din UI — m-a obligat să activez HTTPS în plugin</li><li>Certificatul self-signed pentru localhost creează probleme de încredere</li></ul>
<p>Prin toate aceste soluții de compromis l-am conectat în cele din urmă. Test de bază — <code>vault.move</code> chiar rescrie <code>[[wikilinks]]</code>, funcționează cum trebuie.</p>
<h3>Ce a mers prost în producție</h3>
<p>Când am început refactoringul în masă (drag-and-drop la zeci de foldere în Obsidian + operații MCP simultane), serverul <strong>s-a blocat 4+ minute</strong>. De ce: pluginul rulează <em>în interiorul</em> Obsidian. Când Obsidian reindexează mii de fișiere după o schimbare masivă de structură, pluginul se blochează odată cu el.</p>
<p>Concluzie: <strong>dependența de o instanță Obsidian deschisă și de indexul ei este fatală pentru operațiile în masă</strong>.</p>
<h2>Încercarea 3: @bitbonsai/mcpvault</h2>
<p>Logic — avem nevoie de un server care <strong>nu depinde de Obsidian</strong>. Lucrează direct cu fișierele de pe disc. <code>@bitbonsai/mcpvault</code> — recomandat în multe recenzii. Acces direct la sistemul de fișiere, configurare simplă (<code>npx @bitbonsai/mcpvault@latest /path/to/vault</code>), 14 instrumente. Obsidian nici măcar nu trebuie să fie deschis.</p>
<p><strong>Înainte să instalez, am verificat un lucru critic</strong> — dacă wiki-link-urile se actualizează la move. Am găsit un review de la un utilizator:</p>
<blockquote>Conectorul de sistem de fișiere nu știe că e în Obsidian — vede un folder cu fișiere &lt;code&gt;.md&lt;/code&gt; și atât. Nu știe că numele fișierelor poartă greutate semantică, că fiecare &lt;code&gt;[[wikilink]]&lt;/code&gt; se rupe în momentul în care redenumești sau muți. Auto-update links funcționează doar când redenumirea se face din interiorul aplicației. Am aflat asta după ce i-am cerut lui Claude să curețe numele fișierelor și m-am întors la un dashboard cu jumătate din link-uri rupte.</blockquote>
<p>Confirmat în documentația proprie a mcpvault: PR #101 (wiki link resolution) este <strong>în review, nu a fost merge-uit</strong>. Deci un move prin <code>mcpvault</code> ar rupe jumătate din vault. Nu e potrivit.</p>
<h2>Încercarea 4: VaultForge (finalul)</h2>
<p><code>blacksmithers/vaultforge</code> — construit special pentru agenți AI care fac refactoring.</p>
<h3>Corect arhitectural</h3>
<ul><li><strong>Sistem de fișiere direct</strong> — nu depinde de Obsidian</li><li><strong>Motor propriu de wikilink</strong> — implementează logica de rezolvare a <code>[[wikilink]]</code> care actualizează toate formele (stem, cale completă, alias, embed)</li><li><strong>Dry run implicit</strong> la toate operațiile distructive — întâi arată ce se va schimba, apoi confirmi tu</li><li><strong>27 de instrumente</strong> față de 8–14 la concurenți: batch_rename, update_links, backlinks (analiză de impact), prune_empty_dirs, frontmatter, smart_search (BM25), vault_themes (clustering TF-IDF)</li><li><strong>Licență MIT</strong>, TypeScript, zero sub-dependențe</li><li><strong>Instalare în 30 de secunde</strong> prin <code>.mcpb</code> (extensie one-click pentru Claude Desktop)</li></ul>
<h3>Test de siguranță pe fișiere izolate</h3>
<p>Am creat 4 fișiere de test cu link-uri încrucișate — link-uri stem, link-uri cu alias, link-uri cu cale completă. Mut un fișier într-un subfolder:</p>
<pre><code>delta.md → subfolder/delta-renamed.md</code></pre>
<p>VaultForge a arătat un dry run: "1 fișier va fi redenumit, 3 link-uri vor fi actualizate". Am executat pe bune.</p>
<table><thead><tr><th>Link type</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Stem</td><td>[[delta]]</td><td>[[delta-renamed]]</td></tr><tr><td>Alias</td><td>[[delta|D]]</td><td>[[delta-renamed|D]]</td></tr><tr><td>Full path + alias</td><td>[[_vf-test/delta|D]]</td><td>[[_vf-test/subfolder/delta-renamed|D]]</td></tr></tbody></table>
<p>Am verificat după — <strong>toate cele trei tipuri de link-uri s-au actualizat corect</strong>. Exact asta le lipsea tuturor instrumentelor anterioare.</p>
<h2>Cum instalezi VaultForge — instrucțiunea finală</h2>
<p>Dacă ai macOS și Claude Desktop:</p>
<h3>Pasul 1</h3>
<p>Descarcă fișierul <code>.mcpb</code>:</p>
<pre><code>curl -fsSL https://github.com/blacksmithers/vaultforge/releases/latest/download/vaultforge.mcpb \
  -o /tmp/vaultforge.mcpb &amp;&amp; open /tmp/vaultforge.mcpb</code></pre>
<h3>Pasul 2</h3>
<p>Claude Desktop va deschide dialogul de instalare a extensiei. Introdu <strong>calea absolută</strong> către vault — fără backslash-uri, cu spații normale:</p>
<pre><code>/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault</code></pre>
<h3>Pasul 3</h3>
<p>Apasă Save. Claude Desktop va adăuga extensia în config automat. <strong>Nu e nevoie de restart</strong> — extensiile <code>.mcpb</code> sunt preluate automat.</p>
<h3>Pasul 4</h3>
<p>Verifică: într-un chat nou întreabă: <em>"What is the status of my Obsidian vault?"</em> — ar trebui să returneze ceva de genul <code>totalFiles: 416, totalDirs: 135, ...</code></p>
<h2>Ce am învățat despre ecosistemul MCP al Obsidian</h2>
<p><strong>În primul rând, "cel mai popular" nu înseamnă "funcțional".</strong> MarkusPfundstein/mcp-obsidian are 3.400 de stele și e recomandarea implicită, dar e învechit și îi lipsesc operații esențiale.</p>
<p><strong>În al doilea rând, un plugin nativ are un cost ascuns.</strong> Pluginul aaronsb părea ideal — graf, Dataview, move nativ. Dar dependența de o instanță Obsidian pornită și de indexul ei îl face nepotrivit pentru operații în masă serioase.</p>
<p><strong>În al treilea rând, sistemul de fișiere direct fără un motor de link-uri este o capcană.</strong> Mcpvault e rapid și simplu, dar "doar mutarea fișierelor" distruge structura vault-ului. Link-urile poartă o <strong>semantică impusă</strong> pe care sistemul de fișiere nu o cunoaște. Fără o implementare proprie a logicii de wikilink, instrumentul devine o mină.</p>
<p><strong>În al patrulea rând, testează pe date izolate.</strong> Înainte să încredințezi vreunui instrument un refactoring în masă — creează un folder de test cu 4–5 fișiere cu link-uri încrucișate și vezi ce se întâmplă. 5 minute de testare economisesc ore de recuperare din backup.</p>
<p><strong>În al cincilea rând, ține un backup git al vault-ului.</strong> Cel mai important dintre toate. Un singur <code>git init</code> în interiorul vault-ului și commit-uri periodice — asta e asigurarea împotriva oricărei greșeli a unui agent AI sau instrument. Dacă se strică ceva — <code>git reset --hard</code> aduce totul înapoi.</p>
<h2>Concluzie</h2>
<p>Drumul a durat câteva ore și trei încercări eșuate. Arhitectura finală arată așa:</p>
<ul><li><strong>VaultForge</strong> — instrumentul principal de lucru. Sistem de fișiere direct + motor propriu de wikilink + 27 de instrumente = refactoring stabil la orice scară.</li><li><strong>Git</strong> — versionarea vault-ului. Rollback gratuit pentru orice greșeală.</li></ul>
<p>Acum pot face ceea ce a pornit totul: să-i cer lui Claude să organizeze 400 de note într-o arhitectură PARA ca lumea, să îmbine duplicatele, să adauge frontmatter, să construiască hărți MOC. Fiecare operație e sigură, link-urile se păstrează, iar dry run-ul arată ce se va întâmpla înainte să se schimbe ceva.</p>
<p>Dacă și tu te uiți la Obsidianul tău dezordonat și vrei un asistent AI — începe direct cu VaultForge. Nu repeta traseul meu prin proiecte moarte, plugin-uri beta și servere de sistem de fișiere fără logică de link-uri.</p>]]></content:encoded>
      <pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate>
      <category>Obsidian</category>
      <category>Claude</category>
      <category>MCP</category>
      <category>Automation</category>
      <category>AI</category>
    </item>
    <item>
      <title>Găurile negre ca universuri recursive: de la fizică la sensul existenței</title>
      <link>https://oleksiimazurenko.dev/ro/blog/black-holes-recursive-universes</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/black-holes-recursive-universes</guid>
      <description>Ce-ar fi dacă fiecare gaură neagră este un Big Bang al unui nou univers? O explorare a cosmologiei recursive, a radiației Hawking și a închiderii cognitive.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Ce-ar fi dacă fiecare gaură neagră este un Big Bang al unui nou univers? Acest articol explorează ideea că universul nostru ar putea fi un nod într-un arbore recursiv infinit — unde găurile negre nasc sub-universuri, energia se întoarce prin radiația Hawking, iar legile fundamentale ale fizicii sunt concepute în mod deliberat pentru a face imposibil contactul dintre universuri.</p>
<h2>Gaură neagră = univers</h2>
<p>Ideea a apărut într-un moment de reflecție: o gaură neagră se formează atunci când suficientă masă și presiune se concentrează într-un singur punct. Acea singularitate — densitate infinită, curbură infinită — seamănă în mod suspect cu condițiile pe care le descriem pentru Big Bang.</p>
<p>Ce-ar fi dacă acesta este unul și același eveniment, privit din părți diferite? Din exterior vedem o gaură neagră care înghite materia. Din interior — un nou univers care explodează în existență. Masa și energia care s-au prăbușit în gaura neagră devin materia primă a unui cosmos complet nou, cu propriile stele, planete și, posibil, propriile găuri negre.</p>
<blockquote>Fiecare gaură neagră din universul nostru ar putea conține un univers. Iar universul nostru ar putea exista în interiorul unei găuri negre a unui univers-părinte.</blockquote>
<h2>De ce universurile nu pot intra în contact unele cu altele</h2>
<p>Iată partea elegantă: odată ce treci de orizontul evenimentelor, nu mai există cale de întoarcere. Relativitatea generală garantează acest lucru — viitorul universului-părinte se află în întregime dincolo de orizontul evenimentelor, inaccesibil din interior. Din perspectiva sub-universului, universul-părinte s-a încheiat deja. Întreaga sa linie temporală a trecut deja.</p>
<p>Aceasta nu este o limitare tehnică pe care am putea-o depăși cu o tehnologie mai bună. Este înscrisă în însăși geometria spațiu-timpului. Universurile sunt fundamental izolate unele de altele — nu prin distanță, ci prin structura timpului.</p>
<h2>Ciclul energiei: împrumut și restituire</h2>
<p>Dar energia nu se pierde. Radiația Hawking — procesul cuantic prin care găurile negre se evaporă lent — creează un ciclu remarcabil:</p>
<ol><li>Un univers-părinte creează o gaură neagră, transferând energie într-un sub-univers</li><li>Sub-universul își trăiește întregul ciclu de viață de-a lungul a trilioane de ani</li><li>Gaura neagră se evaporă lent, restituind energia universului-părinte prin radiația Hawking</li><li>Universul-părinte își primește energia înapoi — cu dobândă</li></ol>
<p>Această „dobândă” este fascinantă: fizicienii cred acum că radiația Hawking păstrează informația. Universul-părinte nu primește înapoi doar energie goală — primește o amprentă a tot ceea ce s-a petrecut înăuntru. Fiecare stea care s-a format, fiecare planetă, fiecare moment de conștiință — codificate în radiație.</p>
<h2>Recursivitate până la capăt</h2>
<p>Dacă ești programator, tiparul este inconfundabil. Aceasta este recursivitate. Fiecare univers apelează <code>universe()</code> cu mai puțină energie, creând sub-universuri care creează sub-sub-universuri, până când nu mai rămâne suficientă energie pentru a forma găuri negre — cazul de bază.</p>
<pre><code>universe(energy)
  ├── creates black holes
  │     ├── universe(energy - n)
  │     │     ├── universe(energy - n - m)
  │     │     │     └── base case: not enough energy for black holes
  │     │     └── returns energy via Hawking radiation
  │     └── returns energy via Hawking radiation
  └── receives all energy back</code></pre>
<p>Fizicianul Lee Smolin a formalizat o idee similară sub numele de <strong>Selecție Naturală Cosmologică</strong>: universurile „se reproduc” prin găuri negre, iar fiecare generație are constante fizice ușor diferite — optimizate de-a lungul a nenumărate cicluri pentru a produce mai multe găuri negre, mai multe universuri.</p>
<h2>Unde ne aflăm în acest ciclu?</h2>
<p>Universul nostru are aproximativ 13,8 miliarde de ani. Sună străvechi, dar în contextul întregii sale durate de viață, asistăm la chiar începutul începutului:</p>
<table><thead><tr><th>Eveniment</th><th>Scară temporală</th></tr></thead><tbody><tr><td>Vârsta actuală a universului</td><td>~10¹⁰ years</td></tr><tr><td>Stelele încetează să se formeze</td><td>~10¹⁴ years</td></tr><tr><td>Era găurilor negre</td><td>~10⁴⁰ years</td></tr><tr><td>Ultima gaură neagră se evaporă</td><td>~10¹⁰⁰ years</td></tr></tbody></table>
<p>Existăm la aproximativ 0,00000000...01% din durata totală de viață a universului nostru. Era stelelor — tot ceea ce putem vedea — este o scurtă scânteie chiar la început. Adevărata poveste a universului este era lentă și răbdătoare a găurilor negre care creează și evaporă sub-universuri.</p>
<h2>Problema dimensiunilor superioare</h2>
<p>Tot ceea ce am discutat până acum operează în cadrul înțelegerii noastre tridimensionale. Dar dacă universul nostru este o „felie” a ceva de dimensiune superioară, atunci întregul arbore recursiv de găuri negre și sub-universuri ar putea fi doar umbra unei structuri pe care nu o putem percepe.</p>
<p>În 1884, Edwin Abbott a scris <em>Flatland</em> — o poveste despre ființe bidimensionale incapabile să conceapă o a treia dimensiune. O sferă care trece prin Flatland apare ca un cerc care crește și se micșorează. „Flatlandezii” o pot descrie matematic, dar nu vor înțelege niciodată cu adevărat ceea ce văd. S-ar putea să ne aflăm exact în aceeași situație în raport cu universul nostru.</p>
<blockquote>Ce este conștiința? De ce există experiența subiectivă? David Chalmers a numit acest lucru „problema dificilă” — și ar putea fi cea mai puternică dovadă că ceva operează dincolo de raza noastră dimensională.</blockquote>
<h2>Totul este blocat la nivel fundamental</h2>
<p>Cea mai frapantă realizare nu este că nu știm — ci că <em>nu putem</em> ști. Fiecare direcție de investigație se lovește de o barieră fundamentală:</p>
<ul><li><strong>Vrei să vezi universul-părinte?</strong> Blocat de orizontul evenimentelor</li><li><strong>Vrei să înțelegi conștiința?</strong> Blocat — un sistem nu se poate analiza complet pe sine însuși (teoremele de incompletitudine ale lui Gödel)</li><li><strong>Vrei să știi ce a fost „înainte”?</strong> Blocat — timpul a început cu Big Bang</li><li><strong>Vrei să percepi dimensiuni superioare?</strong> Blocat de limitările cognitive ale unei ființe tridimensionale</li></ul>
<p>Filosoful Colin McGinn numește acest lucru <strong>închidere cognitivă</strong>: unele întrebări sunt închise minții umane nu din cauza datelor insuficiente, ci din cauza arhitecturii minții înseși. Diferența dintre „încă nu știm” și „nu putem ști” este profundă.</p>
<h2>Singurul lucru care rămâne: perfecționarea de sine</h2>
<p>Dacă fiecare ieșire este blocată prin construcție — dacă nu poți privi în afară, nu poți privi înapoi, nu poți privi în sus — atunci rămâne o singură direcție: înăuntru. Universul pare construit în mod deliberat pentru a forța concentrarea asupra propriei ființe.</p>
<p>Această concluzie nu provine din religie sau din manualele de filosofie. Provine din urmărirea logicii găurilor negre, a recursivității, a teoriei informației și a limitelor cunoașterii. Existențialiștii, budiștii, stoicii și fizicienii ajung cu toții în același punct pe căi diferite: scopul existenței ar putea fi pur și simplu rafinarea ființei care există.</p>
<blockquote>Am ajuns aici nu prin credință, ci prin fizică — de la găurile negre, prin universurile recursive, până la blocajele fundamentale ale cunoașterii, până la singura ușă deschisă: a deveni mai buni.</blockquote>
<h2>Referințe</h2>
<ul><li><a href="https://en.wikipedia.org/wiki/Cosmological_natural_selection" target="_blank" rel="noopener">Lee Smolin — Selecție Naturală Cosmologică</a></li><li><a href="https://en.wikipedia.org/wiki/Hawking_radiation" target="_blank" rel="noopener">Stephen Hawking — Radiația Hawking</a></li><li><a href="https://en.wikipedia.org/wiki/Hard_problem_of_consciousness" target="_blank" rel="noopener">David Chalmers — Problema Dificilă a Conștiinței</a></li><li><a href="https://en.wikipedia.org/wiki/Flatland" target="_blank" rel="noopener">Edwin Abbott — Flatland: O poveste a multor dimensiuni (1884)</a></li><li><a href="https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems" target="_blank" rel="noopener">Kurt Gödel — Teoremele de Incompletitudine</a></li></ul>]]></content:encoded>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <category>physics</category>
      <category>philosophy</category>
      <category>consciousness</category>
      <category>black-holes</category>
      <category>cosmology</category>
    </item>
    <item>
      <title>AI a ucis CMS-ul — cel puțin pentru site-urile simple</title>
      <link>https://oleksiimazurenko.dev/ro/blog/ai-killed-cms-for-simple-sites</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/ai-killed-cms-for-simple-sites</guid>
      <description>De ce sistemele tradiționale de gestionare a conținutului devin inutile pentru portofolii, bloguri și landing page-uri — și cum instrumentele AI înlocuiesc întregul strat CMS cu limbaj natural.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Pentru site-uri simple — portofolii, bloguri, landing page-uri, site-uri de firme mici — un CMS tradițional devine o povară inutilă. Instrumente AI precum Claude Code, Cursor și GitHub Copilot pot acum să-ți editeze direct codul, să înțeleagă contextul, să traducă conținutul și să facă deploy la modificări prin git. Stratul de abstractizare oferit de CMS este înlocuit de o interfață mai inteligentă: limbajul natural.</p>
<h2>Taxa CMS pe care o plătești</h2>
<p>Orice CMS vine cu un cost ascuns. Nu doar abonamentul — ci întregul ecosistem de complexitate care se înfășoară în jurul site-ului tău altminteri simplu:</p>
<ul><li><strong>Infrastructură:</strong> O bază de date de găzduit, un API de întreținut, un dashboard de securizat. Doar WordPress reprezintă ~43% din web și ~90% dintre atacurile țintite pe CMS-uri.</li><li><strong>Performanță:</strong> Generare dinamică de pagini, apeluri API la fiecare cerere, hidratare pe client a datelor din CMS. Portofoliul tău de 3 pagini are acum arhitectura unui produs SaaS.</li><li><strong>Dependență de furnizor:</strong> Conținutul tău trăiește în schema de bază de date a altcuiva. Migrezi de la Contentful la Sanity? Ăsta e un proiect, nu o schimbare de configurație.</li><li><strong>Schimbarea contextului:</strong> Editezi cod în IDE, apoi treci la dashboard-ul CMS din browser ca să schimbi un titlu. Două modele mentale diferite pentru ceea ce este, în fond, aceeași operațiune.</li><li><strong>Cost:</strong> Prețurile CMS-urilor headless scalează adesea cu apelurile API sau cu numărul de intrări de conținut. Un blog personal nu are nevoie de o infrastructură de conținut de $99/lună.</li></ul>
<p>Pentru un site de marketing unde 50 de oameni editează conținut zilnic, acest cost este justificat. Pentru portofoliul unui dezvoltator sau landing page-ul unei firme mici? Construiești un pod ca să treci peste o baltă.</p>
<h2>Ce s-a schimbat: AI îți înțelege codul</h2>
<p>Motivul pentru care exista CMS-ul era simplu: oamenii non-tehnici (și chiar dezvoltatorii care nu voiau să atingă codul pentru modificări de conținut) aveau nevoie de o interfață vizuală ca să actualizeze site-uri. Codul era prea complex, prea fragil, prea ușor de stricat.</p>
<p>AI a schimbat fundamental această ecuație. Instrumentele AI moderne de scris cod nu doar completează automat — înțeleg structura proiectului, citesc pattern-urile existente și fac modificări corecte contextual. Schimbarea fluxului de lucru este dramatică:</p>
<pre><code># Old workflow: CMS
1. Open browser → log into CMS dashboard
2. Navigate to content → find the right page
3. Edit in WYSIWYG editor → fight with formatting
4. Preview → looks different from production
5. Hit publish → pray the cache invalidates

# New workflow: AI
1. Tell AI: &quot;Change the pricing on the landing page to $29/month&quot;
2. AI edits the file, you review the diff
3. Push to git → deploy</code></pre>
<p>Nu e ipotetic. Chiar acest blog rulează pe SolidStart, cu conținutul stocat ca fișiere TypeScript. Fiecare articol — inclusiv acesta — a fost creat spunându-i AI-ului ce să scrie, verificând rezultatul și făcând push în git. Fără dashboard CMS. Fără bază de date. Fără strat API între conținut și cod.</p>
<h2>Exemple reale de pe acest site</h2>
<p>Acest site suportă 10 limbi, are un blog, generează imagini OG dinamic și produce feed-uri RSS și sitemap-uri. Iată cum arată stratul de conținut — TypeScript pur:</p>
<pre><code>// This blog post you&apos;re reading right now is a TypeScript file.
// No database. No API. No CMS dashboard.
// Just a typed object that AI can read and edit directly.

export const myPost: BlogPost = {
  slug: &quot;ai-killed-cms&quot;,
  date: &quot;2026-04-17&quot;,
  translations: {
    en: {
      title: &quot;Why I stopped using a CMS&quot;,
      description: &quot;AI understands my codebase better than any CMS UI.&quot;,
      content: makeContent(proseEn),
    },
    uk: { /* ... */ },
    de: { /* ... */ },
    // 10 languages — AI translates them all
  },
};</code></pre>
<p>Lucruri pe care le fac cu AI și care în mod tradițional ar cere un CMS:</p>
<ul><li><strong>Adaug un articol nou:</strong> "Scrie un articol nou despre X, urmează aceeași structură ca postările existente" — AI creează fișierul, adaugă traducerile, îl înregistrează în index</li><li><strong>Actualizez textul de pe landing page:</strong> "Schimbă titlul hero în Y" — AI găsește fișierul corect și îl actualizează</li><li><strong>Traduc conținut:</strong> "Adaugă traducerea în germană pentru pagina de prețuri" — AI citește versiunea în engleză și produce o traducere adaptată cultural, nu una cuvânt cu cuvânt</li><li><strong>Corectez o greșeală de tastare:</strong> "E o greșeală pe pagina about, 'recieve' ar trebui să fie 'receive'" — gata în 3 secunde, făcut commit în git cu un mesaj relevant</li></ul>
<h2>Ce a rezolvat de fapt CMS-ul — și cum îl înlocuiește AI</h2>
<p>Să fim sinceri cu privire la ce a adus CMS-ul și cum se mapează fiecare capabilitate pe fluxul de lucru cu AI:</p>
<table><thead><tr><th>Problemă</th><th>Soluție CMS</th><th>Soluție AI</th></tr></thead><tbody><tr><td>Editare non-tehnică</td><td>Editor WYSIWYG</td><td>Instrucțiuni în limbaj natural</td></tr><tr><td>Conținut multilingv</td><td>Plugin-uri i18n, câmpuri de locale</td><td>AI traduce cu context cultural</td></tr><tr><td>Programarea conținutului</td><td>Date de publicare integrate</td><td>CI/CD bazat pe git cu cron sau câmpuri de dată în cod</td></tr><tr><td>Istoricul versiunilor</td><td>Sistemul de revizii al CMS-ului</td><td>Git — standardul de aur al controlului versiunilor</td></tr><tr><td>Gestionarea fișierelor media</td><td>Bibliotecă de resurse integrată</td><td>CDN + git LFS sau stocare în cloud</td></tr></tbody></table>
<p>Ideea-cheie: git este deja un sistem de control al versiunilor mai bun decât a construit vreodată vreun CMS. Iar limbajul natural este o interfață mai bună decât orice editor WYSIWYG — pentru că poartă intenția, nu doar formatarea.</p>
<h2>Schimbarea de paradigmă: codul este stratul de conținut</h2>
<p>Asistăm la o inversiune. Timp de două decenii, tendința a fost să separi conținutul de cod — să pui conținutul într-o bază de date, să-l expui prin API, să-l randezi pe frontend. Avea sens când codul era greu de editat, iar conținutul trebuia să fie accesibil non-dezvoltatorilor.</p>
<blockquote>AI nu a făcut CMS-ul învechit devenind un CMS mai bun. L-a făcut învechit făcând codul la fel de accesibil ca un dashboard.</blockquote>
<p>Evoluția gestionării conținutului web urmează o traiectorie clară:</p>
<ol><li><strong>Anii 2000:</strong> CMS-uri monolitice (WordPress, Drupal) — conținut și prezentare cuplate într-un singur sistem</li><li><strong>Anii 2010:</strong> CMS-uri headless (Contentful, Strapi) — conținut separat prin API, randat de framework-uri de frontend</li><li><strong>Anii 2020:</strong> Generatoare de site-uri statice + Markdown (Hugo, Astro) — conținut ca fișiere, construit la momentul deploy-ului</li><li><strong>2025+:</strong> Cod-ca-conținut + AI — conținutul trăiește în cod tipizat, AI este interfața de editare</li></ol>
<h2>Când încă ai nevoie de un CMS</h2>
<p>Nu e un discurs de tipul "CMS-ul a murit". CMS-ul rezolvă probleme reale la scară. Încă ai nevoie de unul când:</p>
<ul><li><strong>Echipe editoriale mari:</strong> 10+ editori de conținut care au nevoie de acces pe bază de roluri, flux de aprobare și editare simultană. Conflictele de merge din git nu sunt o problemă pe care s-o rezolve un editor de conținut.</li><li><strong>Conținut de înaltă frecvență:</strong> Site-urile de știri care publică 50+ articole pe zi au nevoie de pipeline-uri editoriale optimizate, nu de commit-uri git.</li><li><strong>Relații complexe între conținuturi:</strong> Cataloagele e-commerce cu mii de SKU-uri, variante de produs și prețuri dinamice au nevoie de baze de date structurate.</li><li><strong>Conformitate cu reglementările:</strong> Industriile care cer piste de audit, lanțuri de aprobare a conținutului și procese de revizuire impuse legal au nevoie de sisteme construite special.</li></ul>
<p>Granița este clară: dacă modificările tale de conținut cer coordonare între mai mulți stakeholderi non-tehnici la frecvență mare, un CMS își merită complexitatea. Dacă ești un dezvoltator solo, o echipă mică sau administrezi un site care se schimbă săptămânal, nu din oră în oră — AI + cod este mai simplu, mai rapid, mai ieftin și mai fiabil.</p>
<h2>Viitorul: AI ca interfață universală</h2>
<p>Tendința se întinde dincolo de CMS. Fiecare strat de abstractizare software care exista pentru că "sistemul de la bază este prea complex pentru interacțiune directă" este comprimat de AI. Dashboard-uri de administrare, interfețe de configurare, editoare vizuale de baze de date — toate acestea sunt interfețe care traduc intenția umană în modificări de sistem. AI face această traducere nativ.</p>
<p>Pentru site-urile simple, viitorul e deja aici. Conținutul tău este cod. Editorul tău este AI. Controlul tău al versiunilor este git. Deploy-ul tău este un push. Întregul strat CMS — dashboard-ul, baza de date, API-ul, găzduirea — era middleware între intenția ta și site-ul tău. AI a eliminat nevoia de acel middleware.</p>
<blockquote>Cel mai bun CMS este niciun CMS. Nu pentru că gestionarea conținutului nu contează — ci pentru că AI a transformat codul însuși în cea mai intuitivă interfață de gestionare a conținutului pe care am avut-o vreodată.</blockquote>]]></content:encoded>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <category>ai</category>
      <category>cms</category>
      <category>web-development</category>
      <category>opinion</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Cum conectezi Perplexity AI la Obsidian prin MCP — notițe direct din chat</title>
      <link>https://oleksiimazurenko.dev/ro/blog/perplexity-obsidian-mcp-integration</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/perplexity-obsidian-mcp-integration</guid>
      <description>Configurează Perplexity Desktop pentru a citi și scrie în Obsidian vault-ul tău folosind serverul MCP filesystem. Cercetează pe web și salvează în notele tale într-o singură conversație.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Perplexity Desktop acceptă conectori <strong>MCP (Model Context Protocol)</strong>. Adăugând serverul oficial <code>@modelcontextprotocol/server-filesystem</code> orientat către Obsidian vault-ul tău, îi poți cere lui Perplexity în limbaj natural să citească, să creeze și să editeze note — direct din chat. Fără pluginuri, fără extensii, fără copy-paste.</p>
<h2>Problema</h2>
<p>Perplexity este excelent la cercetare — caută pe web, rezumă surse și îți oferă răspunsuri cu citări. Dar când vrei să salvezi aceste descoperiri în Obsidian vault-ul tău, fluxul de lucru se blochează: copiezi textul, comuți pe Obsidian, găsești nota potrivită, lipești, formatezi. De fiecare. Dată.</p>
<p>Extensiile de browser precum "Perplexity to Obsidian" ajută la export, dar sunt unidirecționale — AI-ul nu îți poate <em>vedea</em> vault-ul, nu poate citi notele existente și nu poate decide unde să pună lucrurile în funcție de structura folderelor tale.</p>
<h2>Ce este MCP?</h2>
<p><strong>Model Context Protocol (MCP)</strong> este un standard deschis care permite modelelor AI să interacționeze cu instrumente și surse de date locale. Gândește-te la el ca la un port USB pentru AI — conectezi un "server" (un program mic), iar AI-ul capătă noi capabilități. În cazul nostru, serverul filesystem îi oferă lui Perplexity 14 instrumente pentru a lucra cu fișiere:</p>
<pre><code>Perplexity Desktop (Mac App)
  │
  ├── MCP Connector: &quot;Obsidian Vault&quot;
  │     │
  │     └── @modelcontextprotocol/server-filesystem
  │           │
  │           ├── read_file()      → read any .md file
  │           ├── write_file()     → create or overwrite files
  │           ├── edit_file()      → patch existing files
  │           ├── list_directory() → browse vault structure
  │           ├── search_files()   → find files by pattern
  │           └── ... 14 tools total
  │
  └── Perplexity AI Model
        │
        └── &quot;Save this to my daily note&quot; → calls write_file()</code></pre>
<p>Ideea cheie: modelul AI nu accesează direct fișierele tale. El apelează instrumente furnizate de serverul MCP, care rulează local pe mașina ta. Datele tale nu îți părăsesc niciodată calculatorul decât dacă îi ceri explicit AI-ului să facă ceva cu ele.</p>
<h2>Cerințe</h2>
<ul><li>Abonament <strong>Perplexity Pro</strong> (conectorii MCP sunt disponibili pentru utilizatorii plătitori)</li><li><strong>Perplexity Mac App</strong> din App Store (nu versiunea de browser)</li><li><strong>Node.js</strong> instalat pe Mac-ul tău (pentru ca <code>npx</code> să funcționeze)</li></ul>
<h2>Configurare pas cu pas</h2>
<p>Întreaga configurare durează aproximativ 2 minute:</p>
<table><thead><tr><th>Step</th><th>Action</th></tr></thead><tbody><tr><td>1</td><td>Open &lt;strong&gt;Perplexity Mac App&lt;/strong&gt; (App Store version)</td></tr><tr><td>2</td><td>Click avatar → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Connectors&lt;/strong&gt;</td></tr><tr><td>3</td><td>Click &lt;strong&gt;Add Connector&lt;/strong&gt;</td></tr><tr><td>4</td><td>Set Server Name: &lt;code&gt;Obsidian Vault&lt;/code&gt;, Type: &lt;code&gt;Standard I/O&lt;/code&gt;</td></tr><tr><td>5</td><td>Paste the npx command with your vault path</td></tr><tr><td>6</td><td>Leave Environment Variables empty</td></tr><tr><td>7</td><td>Click &lt;strong&gt;Save&lt;/strong&gt; → confirm Security Warning</td></tr><tr><td>8</td><td>Verify status shows &lt;strong&gt;Running&lt;/strong&gt; with 14 tools</td></tr></tbody></table>
<h3>Comanda</h3>
<p>Comanda de lipit în câmpul <strong>Command</strong>:</p>
<pre><code>npx -y @modelcontextprotocol/server-filesystem &quot;/Users/yourname/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault&quot;</code></pre>
<p>Înlocuiește calea cu locația reală a Obsidian vault-ului tău. Dacă vault-ul este sincronizat prin iCloud, calea va fi sub <code>~/Library/Mobile Documents/iCloud~md~obsidian/Documents/</code>. Asigură-te că păstrezi ghilimelele — calea conține probabil spații.</p>
<h2>Cum se folosește</h2>
<p>Odată ce conectorul afișează <strong>Running</strong> cu 14 instrumente disponibile, intră în orice chat Perplexity și începe să vorbești cu vault-ul tău:</p>
<pre><code>&gt; Show me the structure of my Obsidian vault
&gt; Add a new reflection to daily notes/2026-04-12.md: &quot;Started using MCP today&quot;
&gt; Find all notes that mention &quot;meditation&quot;
&gt; Create a new note in concepts/ about quantum computing
&gt; List all files in my ideas/ folder</code></pre>
<p>AI-ul înțelege structura vault-ului tău, respectă convențiile tale de formatare și poate lucra cu conținutul existent. Îi poți cere să cerceteze un subiect pe web și să salveze rezumatul direct într-o notă anume — combinând căutarea Perplexity cu organizarea din Obsidian într-un singur flux.</p>
<h2>De ce MCP întrece celelalte abordări</h2>
<p>Înainte de MCP, existau modalități limitate de a face legătura între Perplexity și Obsidian:</p>
<table><thead><tr><th>Feature</th><th>Copy-paste</th><th>Browser extension</th><th>MCP integration</th></tr></thead><tbody><tr><td>AI sees vault structure</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI reads existing notes</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI writes to vault</td><td>No</td><td>Export only</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>AI edits existing files</td><td>No</td><td>No</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>Works from chat</td><td>No</td><td>Partially</td><td>&lt;strong&gt;Yes&lt;/strong&gt;</td></tr><tr><td>Setup complexity</td><td>None</td><td>Low</td><td>Medium (one-time)</td></tr></tbody></table>
<h2>Limitări actuale</h2>
<ul><li><strong>Doar Mac</strong> — conectorii MCP ai Perplexity funcționează momentan doar în versiunea din Mac App Store</li><li><strong>Fără integrare cu API-ul Obsidian</strong> — serverul filesystem lucrează cu fișiere brute, nu prin API-ul Obsidian. Asta înseamnă că nu va declanșa pluginuri Obsidian (precum Linter sau Templater) la crearea fișierelor</li><li><strong>Aprobare necesară</strong> — operațiunile sensibile cu fișiere pot necesita confirmarea ta în aplicația Perplexity — este o funcție de securitate, nu un bug</li></ul>
<h2>Concluzii</h2>
<p>Această configurare transformă Perplexity dintr-un instrument de cercetare într-un instrument de cercetare-și-captură:</p>
<ol><li>Cercetează pe web și salvează în Obsidian într-o singură conversație</li><li>AI-ul vede structura vault-ului tău și se adaptează la sistemul tău de organizare</li><li>Zero comutări între aplicații — totul se întâmplă în chat-ul Perplexity</li></ol>
<h2>Surse</h2>
<ul><li><a href="https://modelcontextprotocol.io" target="_blank" rel="noopener noreferrer">Model Context Protocol</a> — official MCP specification</li><li><a href="https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" target="_blank" rel="noopener noreferrer">MCP Filesystem Server</a> — the server used in this setup</li><li><a href="https://www.perplexity.ai/help-center/en/articles/11502712-local-and-remote-mcps-for-perplexity" target="_blank" rel="noopener noreferrer">Perplexity Help Center</a> — Local and Remote MCPs for Perplexity</li><li><a href="https://www.perplexity.ai/hub/blog/everything-is-computer" target="_blank" rel="noopener noreferrer">Perplexity Blog</a> — Everything is Computer</li></ul>]]></content:encoded>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <category>Obsidian</category>
      <category>Perplexity</category>
      <category>MCP</category>
      <category>AI</category>
      <category>Productivity</category>
    </item>
    <item>
      <title>Digest zilnic de știri AI cu Claude Code CLI și Obsidian — zero dependențe</title>
      <link>https://oleksiimazurenko.dev/ro/blog/ai-news-digest-claude-code-obsidian</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/ai-news-digest-claude-code-obsidian</guid>
      <description>Cum am construit un agent zilnic de documentare a știrilor cu un script bash de 6 linii, modul headless Claude Code și macOS launchd. Caută 11 subiecte și scrie digesturi direct în Obsidian.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Un script bash de 6 linii care rulează <strong>Claude Code CLI</strong> în mod headless în fiecare dimineață la ora 9:00. Caută știri pe web pentru 11 subiecte configurabile, filtrează zgomotul și scrie un digest markdown formatat direct într-un <strong>Obsidian vault</strong> sincronizat prin iCloud. Zero dependențe. ~100 de linii de configurare în total.</p>
<h2>Problema</h2>
<p>Ca dezvoltator, să rămâi la curent cu mai multe tehnologii este o taxă zilnică. Feedurile RSS sunt zgomotoase, Twitter îți fură timpul, newsletterele ajung fix când ești adânc în flow. Aveam nevoie de ceva care să facă documentarea <em>în locul</em> meu și să prezinte rezultatele acolo unde deja lucrez — în Obsidian vault-ul meu.</p>
<p>Soluția tipică este să construiești un pipeline de scraping: un scheduler, un crawler, un pipeline NLP, o bază de date, un serviciu de notificări. Sunt săptămâni de muncă pentru ceva care se poate strica atunci când un site își schimbă HTML-ul. Voiam ceva ce pot construi într-o după-amiază.</p>
<h2>Arhitectura</h2>
<p>Întregul sistem înseamnă 4 fișiere și zero dependențe. Iată cum funcționează de la un capăt la altul:</p>
<pre><code>macOS launchd (9:00 AM daily)
  │
  └── digest.sh
        │
        └── claude -p &quot;$(cat prompt.md)&quot; --max-turns 20 --allowedTools Read,WebSearch,WebFetch,Write
              │
              ├── Reads topics.yaml (11 configurable topics)
              ├── WebSearch → finds news for each topic (last 24-48h)
              ├── WebFetch → reads full articles
              ├── Filters noise: old tutorials, promos, AI spam
              └── Write → saves digest to Obsidian Vault
                    │
                    └── ~/Obsidian Vault/digests/2026-04-12.md
                          │
                          └── iCloud sync → available on all devices</code></pre>
<h2>Codul (tot)</h2>
<p>Proiectul este intenționat minimal. Fiecare linie își merită locul.</p>
<h3>Punctul de intrare: digest.sh</h3>
<p>Întreaga aplicație este un script bash de 6 linii:</p>
<pre><code>#!/bin/bash
DIGEST_DIR=&quot;$HOME/Developer/news-digest&quot;

claude -p &quot;$(cat &quot;$DIGEST_DIR/prompt.md&quot;)&quot; \
  --max-turns 20 \
  --allowedTools Read,WebSearch,WebFetch,Write</code></pre>
<p>Flag-urile cheie: <code>-p</code> rulează Claude în mod headless (fără terminal interactiv), <code>--max-turns 20</code> îi dă agentului spațiu suficient pentru a documenta toate subiectele, iar <code>--allowedTools</code> restrânge agentul doar la citirea fișierelor, căutarea pe web și scrierea rezultatului.</p>
<h3>Creierul: prompt.md</h3>
<p>Aici trăiește inteligența. Promptul transformă Claude într-un agent de documentare a știrilor, cu instrucțiuni precise despre ce să caute, cum să filtreze și unde să salveze:</p>
<pre><code># News Digest Agent

You are a news research agent. Your job is to find today&apos;s most important
and interesting news for a senior frontend developer.

## Instructions

1. Read the topics file at ~/Developer/news-digest/topics.yaml
2. For EACH topic, search the web for news from the last 24-48 hours
3. Filter: only include genuinely new and noteworthy items
4. Write the digest as a markdown file to Obsidian Vault digests/YYYY-MM-DD.md
5. IMPORTANT: Use the Write tool to save the file. Do NOT output to stdout.

## Rules

- Language: Ukrainian for summaries, English for titles and technical terms
- If there&apos;s no real news for a topic — SKIP IT ENTIRELY
- Prioritize: releases &gt; breaking changes &gt; security &gt; new patterns &gt; discussions
- Max 5 items per topic, sorted by importance
- Include direct links to sources
- Skip promotional content, generic tutorials, and AI-generated spam</code></pre>
<h3>Configurare: topics.yaml</h3>
<p>Subiectele sunt complet configurabile — adaugi un subiect nou și apare în digestul de mâine. Fiecare subiect are un nume și un context opțional care ghidează căutarea AI-ului:</p>
<pre><code>topics:
  - name: better-auth
    context: &quot;auth library for TypeScript. New releases, breaking changes&quot;

  - name: Next.js
    context: &quot;GitHub issues, releases, App Router, Turbopack, performance&quot;

  - name: SolidJS
    context: &quot;SolidStart, releases, ecosystem, comparison with React&quot;

  - name: Tailwind CSS
    context: &quot;v4 updates, new utilities, plugins&quot;

  - name: Claude AI
    context: &quot;Anthropic announcements, Claude Code, new models, MCP, API&quot;

  - name: GPT AI
    context: &quot;OpenAI announcements, new models, ChatGPT features&quot;

  - name: React
    context: &quot;React 19+, Server Components, new patterns, ecosystem&quot;

  - name: Apple
    context: &quot;Hardware, software, WWDC, developer tools, Apple Intelligence&quot;

  - name: Notable People
    context: &gt;
      Latest tweets from: Elon Musk, Dario Amodei, Sam Altman,
      Jensen Huang, Andrej Karpathy, Simon Willison, Swyx...

  - name: AI Global
    context: &quot;Major AI news, new models, regulations, open-source AI&quot;</code></pre>
<h2>Programare cu launchd</h2>
<p>Pe macOS, <code>launchd</code> este modalitatea nativă de a programa sarcini recurente (precum cron pe Linux). Fișierul plist definește când și cum rulează scriptul:</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot;
  &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;key&gt;Label&lt;/key&gt;
    &lt;string&gt;com.news-digest&lt;/string&gt;

    &lt;key&gt;ProgramArguments&lt;/key&gt;
    &lt;array&gt;
        &lt;string&gt;/bin/bash&lt;/string&gt;
        &lt;string&gt;/Users/you/Developer/news-digest/digest.sh&lt;/string&gt;
    &lt;/array&gt;

    &lt;key&gt;StartCalendarInterval&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;Hour&lt;/key&gt;
        &lt;integer&gt;9&lt;/integer&gt;
        &lt;key&gt;Minute&lt;/key&gt;
        &lt;integer&gt;0&lt;/integer&gt;
    &lt;/dict&gt;

    &lt;key&gt;StandardOutPath&lt;/key&gt;
    &lt;string&gt;/Users/you/Developer/news-digest/logs/stdout.log&lt;/string&gt;
    &lt;key&gt;StandardErrorPath&lt;/key&gt;
    &lt;string&gt;/Users/you/Developer/news-digest/logs/stderr.log&lt;/string&gt;

    &lt;key&gt;EnvironmentVariables&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;PATH&lt;/key&gt;
        &lt;string&gt;/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin&lt;/string&gt;
        &lt;key&gt;HOME&lt;/key&gt;
        &lt;string&gt;/Users/you&lt;/string&gt;
    &lt;/dict&gt;
&lt;/dict&gt;
&lt;/plist&gt;</code></pre>
<p>Îl instalezi cu <code>launchctl load ~/Library/LaunchAgents/com.news-digest.plist</code>. Scriptul rulează în fiecare zi la ora 9:00, chiar dacă erai deconectat — launchd va executa sarcinile ratate atunci când sistemul se trezește.</p>
<h2>Cum arată rezultatul</h2>
<p>În fiecare dimineață, un nou fișier markdown apare în Obsidian vault cu știri structurate și prioritizate:</p>
<pre><code>---
date: 2026-04-12
---

# News Digest — 2026-04-12

## Next.js

### Next.js 16.1 Released with Improved Turbopack Caching
Нова версія Next.js 16.1 включає покращене кешування для Turbopack,
що зменшує час холодного старту на ~40%.
[Посилання](https://nextjs.org/blog/next-16-1)

### Critical Memory Leak Fix in App Router
Виправлено витік пам&apos;яті в App Router при частому перемиканні
між динамічними маршрутами.
[GitHub Issue](https://github.com/vercel/next.js/issues/...)

## Claude AI

### Claude Code 1.5 — MCP Server Auto-Discovery
Anthropic випустив оновлення Claude Code з автоматичним
виявленням MCP серверів у проєкті.
[Блог](https://www.anthropic.com/news/...)</code></pre>
<h2>În cifre</h2>
<table><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody><tr><td>Total files in project</td><td>&lt;strong&gt;4&lt;/strong&gt; (digest.sh, prompt.md, topics.yaml, .gitignore)</td></tr><tr><td>Lines of code</td><td>&lt;strong&gt;~100&lt;/strong&gt;</td></tr><tr><td>External dependencies</td><td>&lt;strong&gt;0&lt;/strong&gt;</td></tr><tr><td>Setup time</td><td>&lt;strong&gt;~10 minutes&lt;/strong&gt;</td></tr><tr><td>Daily execution time</td><td>&lt;strong&gt;2-5 minutes&lt;/strong&gt;</td></tr><tr><td>Cost per run</td><td>&lt;strong&gt;~$0.10-0.30&lt;/strong&gt; (Claude API usage)</td></tr></tbody></table>
<h2>Decizii cheie de design</h2>
<ul><li><strong>Claude Code CLI în loc de API</strong> — nu e nevoie să gestionezi chei API, clienți HTTP sau parsarea răspunsurilor. CLI-ul se ocupă din start de autentificare, execuția uneltelor și recuperarea după erori</li><li><strong>Obsidian în loc de email</strong> — digesturile pot fi căutate, legate între ele și rămân permanente. Trăiesc lângă notițele mele, nu se îneacă într-un inbox</li><li><strong>launchd în loc de cron</strong> — launchd este schedulerul nativ pe macOS. Gestionează curat rulările ratate, logarea și variabilele de mediu</li><li><strong>YAML pentru subiecte</strong> — adăugarea unui subiect nou înseamnă o modificare de 2 linii. Fără schimbări de cod</li><li><strong>Sari peste subiectele goale</strong> — dacă nu există știri reale, secțiunea este omisă complet. Fără umplutură, fără conținut întins artificial</li></ul>
<h2>Cum îți construiești propriul agent</h2>
<p>Îl poți avea funcțional în 10 minute:</p>
<ol><li>Instalează <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer">Claude Code CLI</a> și autentifică-te</li><li>Clonează repo-ul: <code>git clone https://github.com/oleksiimazurenko/news-digest</code></li><li>Editează <code>topics.yaml</code> cu interesele tale și <code>prompt.md</code> cu calea ta de salvare</li><li>Editează fișierul plist cu numele tău de utilizator și căile tale, apoi rulează <code>launchctl load</code></li><li>Așteaptă ora 9:00 — sau testează manual cu <code>bash digest.sh</code></li></ol>
<h2>Concluzii</h2>
<p>Cel mai interesant lucru la acest proiect este ceea ce <em>nu</em> se află în el. Nicio bază de date, niciun server API, niciun container Docker, niciun pachet npm, niciun virtualenv Python, niciun parser HTML, niciun pipeline NLP. Doar un prompt, o listă de subiecte și un script de 6 linii care deleagă toată munca grea unui agent AI.</p>
<p>Așa arată să construiești cu agenți AI în practică: tu definești <em>ce</em> și <em>unde</em>, iar agentul se ocupă de <em>cum</em>. Timpul total de dezvoltare a fost de aproximativ 2 ore, incluzând iterațiile pe prompt pentru a obține o filtrare de calitate.</p>
<h2>Surse</h2>
<ul><li><a href="https://github.com/oleksiimazurenko/news-digest" target="_blank" rel="noopener noreferrer">news-digest on GitHub</a> — full source code</li><li><a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer">Claude Code Documentation</a> — headless mode and CLI flags</li><li><a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" target="_blank" rel="noopener noreferrer">Apple Developer</a> — creating launchd jobs</li><li><a href="https://obsidian.md" target="_blank" rel="noopener noreferrer">Obsidian</a> — markdown knowledge base</li></ul>]]></content:encoded>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <category>Claude Code</category>
      <category>AI</category>
      <category>Obsidian</category>
      <category>Automation</category>
      <category>Productivity</category>
    </item>
    <item>
      <title>Next.js Dark Mode fără flash sau avertismente React 19</title>
      <link>https://oleksiimazurenko.dev/ro/blog/nextjs-dark-mode-without-flash</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/nextjs-dark-mode-without-flash</guid>
      <description>Cum să înlocuiești next-themes cu Zustand + useServerInsertedHTML pentru dark mode fără pâlpâire în Next.js 15+, fără avertismente de script React 19.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p><code>next-themes</code> randează un tag <code>&lt;script&gt;</code> în interiorul unui React Client Component pentru a preveni flash-ul de temă (FOUC). React 19 avertizează acum despre asta — și nu există nicio modalitate de a-l suprima. Biblioteca nu a mai fost actualizată din martie 2025. Soluția: înlocuiește <code>next-themes</code> cu un store Zustand + <code>useServerInsertedHTML</code> pentru a injecta scriptul în afara arborelui React. Zero dependențe adăugate. Zero FOUC. Zero avertismente.</p>
<h2>Problema</h2>
<p>Dacă folosești <code>next-themes</code> cu Next.js 15+ și React 19, primești această eroare în consolă la fiecare încărcare a paginii:</p>
<pre><code>Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.
Consider using template tag instead.

src/providers/theme-provider.tsx (7:10) @ ThemeProvider</code></pre>
<p>Aceasta nu este o nepotrivire de hidratare. Este React 19 care avertizează explicit că tagurile <code>&lt;script&gt;</code> randate de componente React pe client <strong>nu vor rula niciodată</strong>. Scriptul funcționează în timpul SSR (se află în HTML), dar React îl marchează ca fiind incorect.</p>
<h2>De ce se întâmplă</h2>
<p><code>next-themes</code> trebuie să seteze clasa de temă corectă pe <code>&lt;html&gt;</code> înainte ca React să hidrateze — altfel obții un flash cu tema greșită. Pentru asta, injectează un <code>&lt;script&gt;</code> inline prin <code>React.createElement</code>:</p>
<pre><code>// next-themes renders a &lt;script&gt; inside a Client Component
return React.createElement(Provider, { value },
  React.createElement(&quot;script&quot;, {
    suppressHydrationWarning: true,
    dangerouslySetInnerHTML: { __html: `(...theme init code...)` }
  }),
  children
)</code></pre>
<p>React 19 și-a schimbat comportamentul: tagurile script din interiorul componentelor sunt acum marcate explicit. Înainte de React 19, acest lucru era ignorat în tăcere. Prop-ul <code>suppressHydrationWarning</code> de pe script nu ajută — el suprimă avertismentele de hidratare, nu avertismentul „script în componentă".</p>
<h2>Ce am încercat (și de ce a eșuat)</h2>
<p>Am încercat sistematic fiecare abordare înainte de a găsi soluția:</p>
<table><thead><tr><th>Attempt</th><th>Result</th></tr></thead><tbody><tr><td>&lt;code&gt;suppressHydrationWarning&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;</td><td>Suppresses hydration mismatch, but NOT the script tag warning</td></tr><tr><td>Delay mount with &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt;</td><td>Warning disappears, but causes FOUC (theme flash)</td></tr><tr><td>Raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in layout &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;</td><td>React 19 catches it — same warning even in Server Components</td></tr><tr><td>&lt;code&gt;next/script&lt;/code&gt; with &lt;code&gt;beforeInteractive&lt;/code&gt;</td><td>Still rendered inside React tree — same warning</td></tr><tr><td>Remove &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, put Script in body</td><td>Same warning — it&apos;s inside &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; which is React-managed</td></tr><tr><td>&lt;code&gt;next-themes@1.0.0-beta.0&lt;/code&gt;</td><td>Beta, no release date, unclear if fixed</td></tr></tbody></table>
<h2>Soluția: Zustand + useServerInsertedHTML</h2>
<p>Ideea cheie: <code>useServerInsertedHTML</code> este un hook Next.js care injectează HTML în stream-ul SSR <strong>în afara arborelui de componente React</strong>. Scriptul ajunge în HTML, dar React nu îl „vede" niciodată în timpul randării pe client — deci niciun avertisment. Combinat cu un store Zustand pentru stare reactivă a temei, obținem un înlocuitor complet, fără dependențe.</p>
<h3>Cum funcționează</h3>
<pre><code>SSR (server):
  useServerInsertedHTML → injects &lt;script&gt; into HTML stream
  ↓
  Browser receives HTML with theme script already in it
  ↓
  Script runs BEFORE React hydrates → correct class on &lt;html&gt;
  ↓
  No flash. No mismatch. No warning.

Client (after hydration):
  useEffect → _init() → Zustand store syncs with localStorage
  ↓
  useTheme() → reactive theme state for components</code></pre>
<h3>Pasul 1: Store Zustand</h3>
<p>Store-ul gestionează starea temei, aplică clase pe DOM, se ocupă de detectarea temei sistemului și sincronizează între taburi. Metoda <code>_init()</code> returnează o funcție de curățare pentru a fi folosită în <code>useEffect</code>:</p>
<pre><code>import { create } from &quot;zustand&quot;;

const STORAGE_KEY = &quot;theme&quot;;
const MEDIA_QUERY = &quot;(prefers-color-scheme: dark)&quot;;

function getSystemTheme(): &quot;light&quot; | &quot;dark&quot; {
  return window.matchMedia(MEDIA_QUERY).matches ? &quot;dark&quot; : &quot;light&quot;;
}

function applyTheme(resolved: string, disableTransition: boolean) {
  const d = document.documentElement;

  if (disableTransition) {
    const style = document.createElement(&quot;style&quot;);
    style.appendChild(
      document.createTextNode(
        &quot;*,*::before,*::after{transition:none!important}&quot;
      )
    );
    document.head.appendChild(style);
    window.getComputedStyle(document.body);
    setTimeout(() =&gt; document.head.removeChild(style), 1);
  }

  d.classList.remove(&quot;light&quot;, &quot;dark&quot;);
  d.classList.add(resolved);
  d.style.colorScheme = resolved;
}

interface ThemeState {
  theme: string;
  systemTheme: &quot;light&quot; | &quot;dark&quot;;
  resolvedTheme: string;
  setTheme: (theme: string) =&gt; void;
  _init: (disableTransition: boolean) =&gt; () =&gt; void;
}

export const useThemeStore = create&lt;ThemeState&gt;((set, get) =&gt; ({
  theme: &quot;system&quot;,
  systemTheme: &quot;light&quot;,
  resolvedTheme: &quot;light&quot;,

  setTheme: (newTheme) =&gt; {
    const { systemTheme, disableTransitionOnChange } = get();
    const resolved = newTheme === &quot;system&quot; ? systemTheme : newTheme;
    localStorage.setItem(STORAGE_KEY, newTheme);
    applyTheme(resolved, disableTransitionOnChange);
    set({ theme: newTheme, resolvedTheme: resolved });
  },

  _init: (disableTransition) =&gt; {
    const stored = localStorage.getItem(STORAGE_KEY) || &quot;system&quot;;
    const system = getSystemTheme();
    const resolved = stored === &quot;system&quot; ? system : stored;

    set({ theme: stored, systemTheme: system, resolvedTheme: resolved });
    applyTheme(resolved, disableTransition);

    // System theme changes
    const mq = window.matchMedia(MEDIA_QUERY);
    const onChange = (e: MediaQueryListEvent) =&gt; {
      const newSystem = e.matches ? &quot;dark&quot; : &quot;light&quot;;
      const { theme } = get();
      set({ systemTheme: newSystem,
            resolvedTheme: theme === &quot;system&quot; ? newSystem : theme });
      if (theme === &quot;system&quot;) applyTheme(newSystem, disableTransition);
    };
    mq.addEventListener(&quot;change&quot;, onChange);

    // Cross-tab sync
    const onStorage = (e: StorageEvent) =&gt; {
      if (e.key !== STORAGE_KEY || !e.newValue) return;
      const system = getSystemTheme();
      const resolved = e.newValue === &quot;system&quot; ? system : e.newValue;
      set({ theme: e.newValue, resolvedTheme: resolved });
      applyTheme(resolved, disableTransition);
    };
    window.addEventListener(&quot;storage&quot;, onStorage);

    return () =&gt; {
      mq.removeEventListener(&quot;change&quot;, onChange);
      window.removeEventListener(&quot;storage&quot;, onStorage);
    };
  },
}));

export function useTheme() {
  return useThemeStore((s) =&gt; ({
    theme: s.theme,
    setTheme: s.setTheme,
    resolvedTheme: s.resolvedTheme,
    systemTheme: s.systemTheme,
  }));
}</code></pre>
<h3>Pasul 2: ThemeProvider</h3>
<p>Provider-ul face două lucruri: injectează scriptul de prevenire a FOUC prin <code>useServerInsertedHTML</code> și inițializează store-ul Zustand la montare:</p>
<pre><code>&quot;use client&quot;

import { useEffect } from &quot;react&quot;
import { useServerInsertedHTML } from &quot;next/navigation&quot;
import { useThemeStore } from &quot;@/store/use-theme-store&quot;

const THEME_INIT_SCRIPT = `(function(){
  try {
    var t = localStorage.getItem(&quot;theme&quot;) || &quot;system&quot;;
    var r = t;
    if (t === &quot;system&quot;) {
      r = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches
        ? &quot;dark&quot; : &quot;light&quot;;
    }
    document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
    document.documentElement.classList.add(r);
    document.documentElement.style.colorScheme = r;
  } catch(e) {}
})()`

export function ThemeProvider({
  children,
  disableTransitionOnChange = false,
}) {
  // Injects script into SSR HTML outside the React tree
  // — prevents FOUC without triggering React 19 warning
  useServerInsertedHTML(() =&gt; (
    &lt;script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} /&gt;
  ))

  useEffect(() =&gt; {
    return useThemeStore.getState()._init(disableTransitionOnChange)
  }, [disableTransitionOnChange])

  return &lt;&gt;{children}&lt;/&gt;
}</code></pre>
<h3>Pasul 3: Layout</h3>
<pre><code>import { ThemeProvider } from &quot;@/providers/theme-provider&quot;

export default function RootLayout({ children }) {
  return (
    &lt;html lang=&quot;en&quot; suppressHydrationWarning&gt;
      &lt;body&gt;
        &lt;ThemeProvider disableTransitionOnChange&gt;
          {children}
        &lt;/ThemeProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre>
<h3>Pasul 4: Folosește-l</h3>
<pre><code>import { useTheme } from &quot;@/store/use-theme-store&quot;

export function ThemeSwitch() {
  const { theme, setTheme } = useTheme();

  return (
    &lt;button onClick={() =&gt; setTheme(
      theme === &quot;dark&quot; ? &quot;light&quot; : &quot;dark&quot;
    )}&gt;
      {theme === &quot;dark&quot; ? &quot;☀️&quot; : &quot;🌙&quot;}
    &lt;/button&gt;
  );
}</code></pre>
<h2>Migrare de la next-themes</h2>
<p>API-ul este identic în mod intenționat. Migrarea înseamnă o singură modificare de import per fișier:</p>
<pre><code>- import { useTheme } from &quot;next-themes&quot;
+ import { useTheme } from &quot;@/store/use-theme-store&quot;

  // API is identical — no other changes needed
  const { theme, setTheme, resolvedTheme, systemTheme } = useTheme()</code></pre>
<h2>Comparație</h2>
<table><thead><tr><th></th><th>next-themes</th><th>Zustand + useServerInsertedHTML</th></tr></thead><tbody><tr><td>React 19 warning</td><td>Yes — &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in client component</td><td>No</td></tr><tr><td>FOUC prevention</td><td>Yes</td><td>Yes</td></tr><tr><td>Cross-tab sync</td><td>Yes</td><td>Yes</td></tr><tr><td>System theme detection</td><td>Yes</td><td>Yes</td></tr><tr><td>&lt;code&gt;disableTransitionOnChange&lt;/code&gt;</td><td>Yes</td><td>Yes</td></tr><tr><td>Bundle size</td><td>~3.5 KB</td><td>~1.5 KB (uses existing Zustand)</td></tr><tr><td>Dependencies</td><td>+1 (next-themes)</td><td>0 (Zustand already in project)</td></tr><tr><td>Maintenance risk</td><td>Abandoned since March 2025</td><td>Your own code</td></tr></tbody></table>
<h2>De ce nu alte alternative?</h2>
<h3>@wrksz/themes</h3>
<p>Un înlocuitor direct care folosește și el <code>useServerInsertedHTML</code>. Funcționează, dar este încă o dependență de la un singur maintainer. Dacă <code>next-themes</code> ne-a învățat ceva — dependențele sunt abandonate. Cu ~100 de linii de cod, poți deține soluția în întregime.</p>
<h3>next-themes@1.0.0-beta.0</h3>
<p>Există pe npm, dar fără dată de lansare, fără changelog și fără vreo indicație clară că avertismentul React 19 este rezolvat. A paria cod de producție pe o versiune beta pe termen nedefinit nu este un risc care merită asumat.</p>
<h3>Doar CSS (prefers-color-scheme)</h3>
<p>Funcționează pentru detectarea temei sistemului, dar nu poate gestiona persistența preferinței utilizatorului (localStorage), schimbarea manuală a temei sau opțiunea „system". Pentru asta ai nevoie de JavaScript.</p>
<h2>Concluzii</h2>
<ol><li><code>next-themes</code> este practic abandonat — ultima lansare martie 2025, avertismentul React 19 nerezolvat</li><li><code>useServerInsertedHTML</code> este primitiva Next.js corectă pentru injectarea scripturilor fără avertismente React</li><li>Zustand oferă stare reactivă a temei cu mai puțin cod decât un provider de Context</li><li>Întreaga soluție are ~100 de linii, zero dependențe noi, și deții fiecare linie</li></ol>
<h2>Surse</h2>
<ul><li><a href="https://github.com/shadcn-ui/ui/issues/10104" target="_blank" rel="noopener noreferrer">shadcn/ui #10104</a> — Dark mode guide should address React 19 script warning</li><li><a href="https://github.com/pacocoursey/next-themes/issues/296" target="_blank" rel="noopener noreferrer">next-themes #296</a> — React 19 support request (open since 2024)</li><li><a href="https://github.com/facebook/react/issues/34008" target="_blank" rel="noopener noreferrer">React #34008</a> — Script tags not executing in components (by design)</li><li><a href="https://dev.to/wrksz/i-was-tired-of-next-themes-being-abandoned-so-i-built-a-drop-in-replacement-4dbk" target="_blank" rel="noopener noreferrer">@wrksz/themes</a> — Drop-in replacement using useServerInsertedHTML</li><li><a href="https://nextjs.org/docs/app/building-your-application/styling/css#server-inserted-html" target="_blank" rel="noopener noreferrer">Next.js Docs</a> — useServerInsertedHTML API</li></ul>]]></content:encoded>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <category>next.js</category>
      <category>react-19</category>
      <category>dark-mode</category>
      <category>zustand</category>
      <category>performance</category>
    </item>
    <item>
      <title>Cum am eliminat 94 de fișiere CSS care blochează randarea în Next.js 16 cu o funcționalitate Turbopack slab documentată</title>
      <link>https://oleksiimazurenko.dev/ro/blog/eliminating-render-blocking-css</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/eliminating-render-blocking-css</guid>
      <description>După zile în care am încercat fiecare abordare — de la experimental.inlineCss până la hack-uri cu MutationObserver — am descoperit Turbopack Import Attributes care rezolvă problema CSS-ului ce blochează randarea în Next.js App Router.</description>
      <content:encoded><![CDATA[<h2>Problema</h2>
<p>Aplicația noastră (<a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a>) folosește Next.js 16 cu un <strong>Landing Builder</strong> — un sistem bazat pe CMS care asamblează pagini de marketing din ~90 de componente de secțiune diferite (heroes, FAQ-uri, prețuri, recenzii etc.). Arhitectura folosește un <code>sectionRegistry.tsx</code> care mapează numele secțiunilor la apeluri <code>next/dynamic()</code>:</p>
<pre><code>// sectionRegistry.tsx — imports all 90 sections
export const sectionRegistry = {
  HeroSection: dynamic(() =&gt; import(&apos;./HeroSection/HeroSection&apos;)),
  FAQSection: dynamic(() =&gt; import(&apos;./FAQSection/FAQSection&apos;)),
  ReviewsSection: dynamic(() =&gt; import(&apos;./ReviewsSection/ReviewsSection&apos;)),
  // ... 87 more
}</code></pre>
<p>O singură pagină de destinație randează doar <strong>5-8 secțiuni</strong>. Dar Lighthouse arăta:</p>
<pre><code>Eliminate render-blocking resources
  94 CSS resources (~330 KB)
  Potential savings: 5,440 ms</code></pre>
<p><strong>De ce?</strong> Turbopack vede toate cele 90 de căi <code>import()</code> ca fiind accesibile și generează <code>&lt;link rel="stylesheet"&gt;</code> pentru <strong>fiecare</strong> modul SCSS. Chiar și secțiunile care nu se randează niciodată pe pagină își primesc CSS-ul injectat în <code>&lt;head&gt;</code>. Acesta este un <a href="https://github.com/vercel/next.js/issues/62485" target="_blank" rel="noopener noreferrer">comportament confirmat și așteptat</a> al Next.js App Router. CSS-ul nu este împărțit pe bucăți de cod pentru importurile <code>dynamic()</code> din Server Components. Maintainerii îl consideră un compromis deliberat pentru a preveni FOUC. <strong>Nu este planificată o remediere.</strong></p>
<h2>Tot ce am încercat (și de ce a eșuat)</h2>
<p>Am petrecut zile întregi parcurgând fiecare abordare pe care am putut-o găsi. Iată lista completă:</p>
<table><thead><tr><th>Approach</th><th>Why it doesn&apos;t work</th></tr></thead><tbody><tr><td>Split &lt;code&gt;sectionRegistry&lt;/code&gt; into per-section files</td><td>CSS still loaded — &lt;a href=&quot;https://github.com/vercel/next.js/issues/61066&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#61066&lt;/a&gt;</td></tr><tr><td>&lt;code&gt;experimental.inlineCss: true&lt;/code&gt;</td><td>Inlines CSS on &lt;strong&gt;every SSR request&lt;/strong&gt; — crashed our CMS under load</td></tr><tr><td>&lt;code&gt;experimental.optimizeCss&lt;/code&gt; (Critters)</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/62485&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Incompatible with streaming&lt;/a&gt; in App Router</td></tr><tr><td>CSS-in-JS rewrite</td><td>Rewriting 90 sections from SCSS to CSS-in-JS is not realistic</td></tr><tr><td>&lt;code&gt;media=&quot;print&quot;&lt;/code&gt; hack</td><td>Doesn&apos;t work with CSS Modules in Turbopack — &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags are managed by the framework</td></tr><tr><td>Switch back to Webpack</td><td>Has &lt;code&gt;experiments.css&lt;/code&gt; options, but we&apos;re committed to Turbopack</td></tr><tr><td>Turbopack plugins</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/discussions/85465&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Don&apos;t exist&lt;/a&gt; — no plugin API</td></tr><tr><td>Turbopack CSS loaders</td><td>Not supported — only JS output</td></tr><tr><td>SWC plugins for CSS</td><td>SWC only processes JavaScript</td></tr><tr><td>Client Component wrapper for &lt;code&gt;dynamic()&lt;/code&gt;</td><td>Registry is a global constant — bundler sees all dependencies</td></tr><tr><td>Next.js middleware HTML rewrite</td><td>&lt;a href=&quot;https://github.com/orgs/vercel/discussions/3874&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Middleware can&apos;t modify response body&lt;/a&gt;</td></tr><tr><td>Suspense + streaming with async import</td><td>React Float always pulls CSS into initial &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;</td></tr><tr><td>Suspense + 3s delay</td><td>Content streams later, but CSS is in the initial &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; chunk</td></tr><tr><td>&lt;code&gt;experimental.cssChunking&lt;/code&gt;</td><td>Already default. Merges chunks but doesn&apos;t remove irrelevant CSS</td></tr><tr><td>Post-build Beasties/Critters</td><td>Only for static export, &lt;a href=&quot;https://github.com/vercel/next.js/discussions/59989&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;not with ISR&lt;/a&gt;</td></tr><tr><td>npm libraries (next-critical, fg-loadcss)</td><td>Pages Router only or abandoned (6+ years)</td></tr><tr><td>&lt;code&gt;inlineCss&lt;/code&gt; per-route exclusion</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/pull/72195&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Not supported&lt;/a&gt; — all-or-nothing global flag</td></tr><tr><td>Turbopack &lt;code&gt;as: &apos;raw&apos;&lt;/code&gt; for SCSS</td><td>Returns &lt;code&gt;undefined&lt;/code&gt; instead of text</td></tr><tr><td>Turbopack rule for &lt;code&gt;*.inline.module.scss&lt;/code&gt;</td><td>Turbopack intercepts &lt;code&gt;.module.scss&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; custom rules</td></tr><tr><td>Turbopack rule for &lt;code&gt;*.inline.scss&lt;/code&gt;</td><td>Turbopack intercepts &lt;code&gt;.scss&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; custom rules</td></tr><tr><td>MutationObserver &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;</td><td>Tested in dev, risky — CSS still loads as separate files, possible FOUC</td></tr></tbody></table>
<h3>Capcana inlineCss</h3>
<p>Next.js are un flag <a href="https://github.com/vercel/next.js/pull/72195" target="_blank" rel="noopener noreferrer"><code>experimental.inlineCss</code></a> care înlocuiește toate <code>&lt;link rel="stylesheet"&gt;</code> cu etichete <code>&lt;style&gt;</code> inline. Sună perfect, nu?</p>
<p>Problema: este <strong>totul sau nimic</strong>. Nu îl poți activa per rută. Dacă ai pagini SSR (<code>force-dynamic</code>), fiecare cerere reconstruiește tot CSS-ul inline. Am încercat — CMS-ul nostru headless nu a făcut față încărcării și a picat. Pentru ca acest lucru să funcționeze în siguranță, <strong>100% dintre paginile tale</strong> trebuie să fie <code>force-static</code> sau ISR. Cu peste 20 de pagini SSR (autentificare, tablouri de bord, pagini dinamice), aceasta este o migrare masivă.</p>
<h2>Descoperirea: Turbopack Import Attributes</h2>
<p>Săpând prin <a href="https://nextjs.org/blog/next-16-2-turbopack#inline-loader-configuration" target="_blank" rel="noopener noreferrer">notele de lansare Next.js 16.2</a>, am găsit o funcționalitate slab documentată: <strong>Turbopack Import Attributes</strong>. Îți permite să înlocuiești pipeline-ul de bundling încorporat pentru un import specific folosind sintaxa TC39 <code>with {}</code>:</p>
<pre><code>import { cssText, styles } from &apos;./hero.module.scss&apos; with {
  turbopackLoader: &apos;@promova/scss-to-inline-loader&apos;,
  turbopackAs: &apos;*.js&apos;
}</code></pre>
<p>Aceasta îi spune lui Turbopack: <em>"Nu procesa acest import ca pe o foaie de stil. Trece-l prin loader-ul meu personalizat și tratează rezultatul ca JavaScript."</em></p>
<p><strong>Aceasta este perspectiva-cheie.</strong> În loc ca Turbopack să genereze un <code>&lt;link rel="stylesheet"&gt;</code> care blochează randarea, loader-ul nostru compilează SCSS-ul și îl exportă ca un șir JS. Apoi îl injectăm ca o etichetă <code>&lt;style&gt;</code> inline direct în componentă. Rezultatul: <strong>doar CSS-ul pentru secțiunile care se randează efectiv ajunge în HTML-ul paginii</strong>. O pagină cu 5 secțiuni primește CSS pentru 5 secțiuni — nu pentru 94.</p>
<h2>Soluția</h2>
<h3>1. Loader Turbopack personalizat</h3>
<p>Un script Node.js de ~70 de linii ca pachet yarn workspace (<code>@promova/scss-to-inline-loader</code>):</p>
<pre><code>const { createHash } = require(&apos;node:crypto&apos;)
const sass = require(&apos;sass&apos;)
const postcss = require(&apos;postcss&apos;)
const postcssModules = require(&apos;postcss-modules&apos;)
const path = require(&apos;path&apos;)

const SHARED_STYLES = path.resolve(__dirname, &apos;../shared/styles&apos;)

// Same prependData as sassOptions.prependData in next.config.ts
const PREPEND_DATA = `
@use &quot;sass:math&quot; as math;
@use &quot;sass:list&quot; as list;
@use &quot;${SHARED_STYLES}/_variables.scss&quot; as *;
@use &quot;${SHARED_STYLES}/_breakpoints.scss&quot; as *;
@use &quot;${SHARED_STYLES}/_mixins.scss&quot; as *;
`

module.exports = function scssToInlineLoader(source) {
  const callback = this.async()
  processScss(source, this.resourcePath)
    .then((js) =&gt; callback(null, js))
    .catch((err) =&gt; callback(err))
}

async function processScss(source, resourcePath) {
  // 1. Compile SCSS → CSS (with global variables, mixins, breakpoints)
  const sassResult = sass.compileString(PREPEND_DATA + &apos;\n&apos; + source, {
    loadPaths: [path.dirname(resourcePath), SHARED_STYLES],
    style: &apos;compressed&apos;,
    sourceMap: false,
    url: new URL(&apos;file://&apos; + resourcePath),
  })

  // 2. Scope class names with postcss-modules (CSS Modules compatible)
  let classNames = {}
  const result = await postcss([
    postcssModules({
      generateScopedName(name, filename) {
        const file = path.basename(filename, path.extname(filename))
          .replace(&apos;.module&apos;, &apos;&apos;)
        const hash = createHash(&apos;md5&apos;)
          .update(filename + name)
          .digest(&apos;base64url&apos;)
          .slice(0, 6)
        return `${file}__${name}__${hash}`
      },
      getJSON(_, json) { classNames = json },
    }),
  ]).process(sassResult.css, { from: resourcePath })

  // 3. Export as JS module — same interface as CSS Modules
  return [
    `export const cssText = ${JSON.stringify(result.css)};`,
    `export const styles = ${JSON.stringify(classNames)};`,
    `export default styles;`,
  ].join(&apos;\n&apos;)
}</code></pre>
<p>Ce face: <strong><code>styles</code></strong> — aceeași hartă de nume de clase cu scop ca la CSS Modules standard. <strong><code>cssText</code></strong> — CSS-ul compilat ca șir de caractere.</p>
<h3>2. Componenta InlineStyle</h3>
<p>Folosește API-ul încorporat <code>&lt;style href precedence&gt;</code> din React 19 pentru deduplicare automată:</p>
<pre><code>export function InlineStyle({ css, id }: { css: string; id: string }) {
  return (
    &lt;style href={id} precedence=&quot;default&quot;&gt;
      {css}
    &lt;/style&gt;
  )
}</code></pre>
<p>React 19 garantează: același <code>href</code> → un singur <code>&lt;style&gt;</code> în DOM. Dacă o secțiune se randează de două ori pe o pagină, CSS-ul este totuși injectat o singură dată.</p>
<h3>3. Migrare per componentă (~6 linii per secțiune)</h3>
<pre><code>// BEFORE: Turbopack → &lt;link rel=&quot;stylesheet&quot;&gt; (render-blocking)
import styles from &apos;./hero_section.module.scss&apos;</code></pre>
<pre><code>// AFTER: Custom loader → JS module → inline &lt;style&gt; (non-blocking)
import { cssText, styles } from &apos;./hero_section.module.scss&apos; with {
  turbopackLoader: &apos;@promova/scss-to-inline-loader&apos;,
  turbopackAs: &apos;*.js&apos;
}
import { InlineStyle } from &apos;@promova/scss-to-inline-loader/InlineStyle&apos;

export function HeroSection() {
  return (
    &lt;section className={styles.hero}&gt;
      &lt;InlineStyle css={cssText} id=&quot;hero-section&quot; /&gt;
      {/* ... component JSX, className usage is identical */}
    &lt;/section&gt;
  )
}</code></pre>
<p><strong>Fișierele <code>.module.scss</code> rămân exact la fel.</strong> Fără rescrierea CSS-ului. Stylelint, Prettier, suportul IDE — toate păstrate.</p>
<h2>De ce este mai bine decât inlineCss: true</h2>
<p>Aceasta este diferența critică:</p>
<table><thead><tr><th></th><th>&lt;code&gt;inlineCss: true&lt;/code&gt;</th><th>Import Attributes + Loader</th></tr></thead><tbody><tr><td>What gets inlined</td><td>&lt;strong&gt;ALL CSS on the page&lt;/strong&gt; (94 files)</td><td>&lt;strong&gt;Only CSS for rendered sections&lt;/strong&gt; (5-8 files)</td></tr><tr><td>SSR overhead</td><td>CSS rebuilt on every request</td><td>CSS compiled at build time</td></tr><tr><td>Per-route control</td><td>No (all-or-nothing)</td><td>Yes (per-import)</td></tr><tr><td>SSR pages safety</td><td>Risky (overloads server)</td><td>Safe (component-level)</td></tr></tbody></table>
<p>Cu <code>inlineCss: true</code>, o pagină tot primește TOATE cele 94 de foi de stil inline — sunt doar etichete <code>&lt;style&gt;</code> în loc de etichete <code>&lt;link&gt;</code>. HTML-ul se umflă. Cu abordarea noastră, <strong>doar CSS-ul care se randează efectiv ajunge în HTML</strong>. 5 secțiuni randate → 5 etichete <code>&lt;style&gt;</code>. Atât.</p>
<h2>Capcana Turbopack: fără reguli globale pentru .module.scss</h2>
<p>O capcană în care am căzut: ai putea crede că poți adăuga o regulă Turbopack în <code>next.config.ts</code> pentru a aplica loader-ul global:</p>
<pre><code>// ❌ THIS CAUSES A FATAL PANIC
turbopack: {
  rules: {
    &apos;**/components/**/*.module.scss&apos;: {
      loaders: [&apos;@promova/scss-to-inline-loader&apos;],
      as: &apos;*.js&apos;
    }
  }
}</code></pre>
<p><strong>Nu face asta.</strong> Pipeline-ul încorporat de module CSS al Turbopack interceptează fișierele <code>.module.scss</code> <strong>înainte</strong> ca regulile personalizate să fie aplicate. Creează un asset <code>EcmascriptCssModule</code> care intră în conflict cu ieșirea <code>as: '*.js'</code>, cauzând:</p>
<pre><code>FATAL PANIC: inner asset should be CSS processable</code></pre>
<p>Atributele <code>with {}</code> funcționează pentru că îi indică lui Turbopack <strong>la locul importului</strong> să ocolească complet pipeline-ul de module CSS. Nu există nicio modalitate de a face acest lucru global prin configurare.</p>
<h2>Rezultate</h2>
<p>Am migrat 127 de componente de secțiune în Landing Builder. Build-ul de producție verificat.</p>
<table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Render-blocking CSS files</td><td>94</td><td>0 (for landing sections)</td></tr><tr><td>CSS in HTML per page</td><td>~330 KB (all sections)</td><td>~20-40 KB (rendered sections only)</td></tr><tr><td>CSS delivery</td><td>&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; (network request, blocking)</td><td>&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; (inline, non-blocking)</td></tr><tr><td>Sass features</td><td>Full</td><td>Full (variables, mixins, nesting, @use)</td></tr><tr><td>CSS Modules scoping</td><td>Built-in</td><td>postcss-modules (compatible)</td></tr><tr><td>Code change per section</td><td>—</td><td>~6 lines (import + InlineStyle)</td></tr></tbody></table>
<h2>Limitări</h2>
<ul><li><strong><code>with {}</code> per import este verbos</strong> — fiecare import are nevoie de 3 linii suplimentare. Nicio modalitate de a-l aplica global (vezi capcana de mai sus).</li><li><strong>Doar Turbopack</strong> — atributele <code>with {}</code> nu sunt suportate de Webpack. Dacă ai nevoie de fallback Webpack, importurile revin la CSS Modules standard (<code>cssText</code> va fi <code>undefined</code>).</li><li><strong>Hashing-ul numelor de clase</strong> — loader-ul nostru folosește un algoritm de hashing diferit de cel încorporat în Turbopack. Numele claselor arată diferit în DevTools. Funcțional identice.</li><li><strong>Dimensiunea HTML crește</strong> — CSS-ul este inline în HTML în loc să fie stocat în cache ca fișiere separate. Pentru paginile statice/ISR servite din CDN, aceasta nu este o problemă.</li></ul>
<h2>Când să folosești asta</h2>
<p>Această tehnică este cea mai eficientă atunci când:</p>
<ol><li><strong>Ai un pattern registry/barrel</strong> — un fișier importă multe componente, dar doar câteva se randează per pagină</li><li><strong>Ești pe Turbopack</strong> — Import Attributes sunt specifice Turbopack</li><li><strong>Vrei control per componentă</strong> — nu un flag totul-sau-nimic</li><li><strong>SCSS-ul tău este complex</strong> — variabile, mixin-uri, breakpoint-uri, imbricare — toate suportate</li><li><strong>Nu poți folosi <code>experimental.inlineCss</code></strong> — pentru că ai pagini SSR sau vrei control granular</li></ol>
<h2>Probleme GitHub asociate</h2>
<p>Dacă ești afectat de CSS-ul care blochează randarea în Next.js App Router — nu ești singur:</p>
<h3>Problema fundamentală</h3>
<ul><li><a href="https://github.com/vercel/next.js/issues/62485" target="_blank" rel="noopener noreferrer"><strong>#62485</strong></a> — Render blocking CSS (maintainers: "expected behavior")</li><li><a href="https://github.com/vercel/next.js/issues/61066" target="_blank" rel="noopener noreferrer"><strong>#61066</strong></a> — Dynamic imports from Server Components are not code-split</li><li><a href="https://github.com/vercel/next.js/issues/54935" target="_blank" rel="noopener noreferrer"><strong>#54935</strong></a> — Server-side dynamic imports don't split client modules</li><li><a href="https://github.com/vercel/next.js/issues/61574" target="_blank" rel="noopener noreferrer"><strong>#61574</strong></a> — JS/CSS code splitting doesn't work as documented</li><li><a href="https://github.com/vercel/next.js/issues/57634" target="_blank" rel="noopener noreferrer"><strong>#57634</strong></a> — Add support for critical CSS inlining with App Router</li><li><a href="https://github.com/vercel/next.js/issues/50300" target="_blank" rel="noopener noreferrer"><strong>#50300</strong></a> — next/dynamic on server component does not build CSS modules</li></ul>
<h3>Funcționalitatea inlineCss și problemele</h3>
<ul><li><a href="https://github.com/vercel/next.js/pull/72195" target="_blank" rel="noopener noreferrer"><strong>PR #72195</strong></a> — experimental: css inlining (the implementation)</li><li><a href="https://github.com/vercel/next.js/pull/73182" target="_blank" rel="noopener noreferrer"><strong>PR #73182</strong></a> — Don't inline CSS in RSC payload for client navigation</li><li><a href="https://github.com/vercel/next.js/issues/75648" target="_blank" rel="noopener noreferrer"><strong>#75648</strong></a> — WhatsApp preview broken with inlineCss + Tailwind</li><li><a href="https://github.com/vercel/next.js/issues/83612" target="_blank" rel="noopener noreferrer"><strong>#83612</strong></a> — Turbopack wrong font URL with inline CSS</li></ul>
<h3>Comunitatea caută soluții</h3>
<ul><li><a href="https://github.com/vercel/next.js/discussions/82894" target="_blank" rel="noopener noreferrer"><strong>Discussion #82894</strong></a> — How to prevent render-blocking with CSS Modules? (2025)</li><li><a href="https://github.com/vercel/next.js/discussions/70526" target="_blank" rel="noopener noreferrer"><strong>Discussion #70526</strong></a> — Ideas for reducing render-blocking CSS</li><li><a href="https://github.com/vercel/next.js/discussions/59814" target="_blank" rel="noopener noreferrer"><strong>Discussion #59814</strong></a> — Render blocking styles with Tailwind</li><li><a href="https://github.com/vercel/next.js/discussions/49691" target="_blank" rel="noopener noreferrer"><strong>Discussion #49691</strong></a> — How to deal with render-blocking modular CSS</li><li><a href="https://github.com/vercel/next.js/discussions/59989" target="_blank" rel="noopener noreferrer"><strong>Discussion #59989</strong></a> — Critical CSS inlining with App Router</li><li><a href="https://github.com/vercel/next.js/discussions/80486" target="_blank" rel="noopener noreferrer"><strong>Discussion #80486</strong></a> — Is optimizeCss still in use?</li><li><a href="https://github.com/vercel/next.js/discussions/85465" target="_blank" rel="noopener noreferrer"><strong>Discussion #85465</strong></a> — Turbopack plugin API (doesn't exist)</li></ul>
<h3>Buguri CSS în Turbopack</h3>
<ul><li><a href="https://github.com/vercel/next.js/issues/68412" target="_blank" rel="noopener noreferrer"><strong>#68412</strong></a> — Incorrect CSS modules load order with external components</li><li><a href="https://github.com/vercel/next.js/issues/76464" target="_blank" rel="noopener noreferrer"><strong>#76464</strong></a> — Turbopack sometimes strips SCSS imports</li><li><a href="https://github.com/vercel/next.js/issues/82497" target="_blank" rel="noopener noreferrer"><strong>#82497</strong></a> — SCSS module not loading when not-found.tsx is present</li><li><a href="https://github.com/vercel/next.js/issues/88544" target="_blank" rel="noopener noreferrer"><strong>#88544</strong></a> — Exporting Sass variables from CSS modules doesn't work with Turbopack</li></ul>
<p>Construit la <a href="https://promova.com" target="_blank" rel="noopener noreferrer">Promova</a> — o platformă de învățare a limbilor care deservește milioane de utilizatori. Dacă te lupți cu aceeași problemă de CSS, nu ezita să faci referire la acest articol în issue-urile GitHub de mai sus.</p>]]></content:encoded>
      <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
      <category>Next.js</category>
      <category>CSS</category>
      <category>Performance</category>
      <category>Turbopack</category>
      <category>SCSS</category>
    </item>
    <item>
      <title>Next.js Memory Leak: Fetch + Mod Standalone — 2 ani fără rezolvare</title>
      <link>https://oleksiimazurenko.dev/ro/blog/nextjs-memory-leak-fetch-standalone</link>
      <guid isPermaLink="true">https://oleksiimazurenko.dev/ro/blog/nextjs-memory-leak-fetch-standalone</guid>
      <description>Next.js aplică un patch peste fetch-ul global și adaugă un strat de cache care pierde memorie la fiecare cerere. În Docker/K8s asta provoacă crash-uri OOM la fiecare câteva ore. Bug-ul există din Next.js 14 și rămâne nerezolvat în 16.2.x.</description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Next.js aplică un patch peste <code>fetch</code>-ul global și adaugă un strat de cache care păstrează referințe către datele de răspuns după ce ar fi trebuit eliberate. Fiecare apel <code>fetch</code> adaugă memorie care nu este niciodată returnată către GC. În Docker/Kubernetes acest lucru duce la crash-uri OOM la fiecare câteva ore. Bug-ul există din Next.js 14 (aprilie 2024) și rămâne nerezolvat în 16.2.x (martie 2026). Pe Vercel problema nu se manifestă datorită funcțiilor serverless efemere.</p>
<h2>Cum funcționează un fetch normal</h2>
<pre><code>Request → fetch → got data → response to user → GC cleans up → memory free</code></pre>
<h2>Cum funcționează fetch în Next.js</h2>
<p>Next.js interceptează <code>fetch</code>-ul global și îl împachetează cu propriul strat de cache/tracking:</p>
<pre><code>Request → Next.js fetch wrapper → creates:
  - Performance entry (speed tracking)    ← not cleaned
  - Request metadata object               ← not cleaned
  - Internal promise wrapper              ← not cleaned
  - Cache lookup entry                    ← not cleaned
  - Response body (for unique URL)        ← not cleaned
→ Response to user
→ GC sees objects are still referenced → doesn&apos;t touch them
→ Memory grows indefinitely</code></pre>
<h2>Ce se întâmplă în producție</h2>
<pre><code>Self-hosted (Docker/K8s):
  Request 1:      fetch → +100KB → RAM: 100KB
  Request 2:      fetch → +100KB → RAM: 200KB
  Request 1000:   fetch → +100KB → RAM: 100MB
  Request 50,000: fetch → +100KB → RAM: 5GB → OOM Kill 💀
  → Kubernetes restarts the pod → cycle repeats

Vercel (Serverless):
  Request 1: [start] → fetch → +100KB → [process dies] → 0 KB ✅
  Request 2: [start] → fetch → +100KB → [process dies] → 0 KB ✅
  → Memory never accumulates</code></pre>
<h2>Versiuni afectate</h2>
<h3>Next.js — Toate versiunile cu App Router</h3>
<table><thead><tr><th>Version</th><th>Issue</th><th>Date</th></tr></thead><tbody><tr><td>14.2.x</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/64212&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#64212&lt;/a&gt;</td><td>April 2024</td></tr><tr><td>14.x-15.x</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/68578&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#68578&lt;/a&gt;</td><td>August 2024</td></tr><tr><td>14.3.0-canary</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/79588&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#79588&lt;/a&gt;</td><td>May 2025</td></tr><tr><td>16.0.1</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/85914&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#85914&lt;/a&gt;</td><td>November 2025</td></tr><tr><td>16.1.0</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/discussions/88603&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#88603&lt;/a&gt;</td><td>January 2026</td></tr><tr><td>16.0.10</td><td>&lt;a href=&quot;https://github.com/vercel/next.js/issues/90433&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#90433&lt;/a&gt;</td><td>February 2026</td></tr><tr><td>16.2.0-canary.51</td><td>Confirmed in #90433 comments</td><td>March 2026</td></tr></tbody></table>
<h3>Node.js</h3>
<p>Testat pe Node.js 20, 22, 24, 25 — pierde memorie pe toate.</p>
<h2>Ce nu funcționează</h2>
<table><thead><tr><th>Attempt</th><th>Result</th></tr></thead><tbody><tr><td>&lt;code&gt;cacheMaxMemorySize: 0&lt;/code&gt;</td><td>Doesn&apos;t help — leak is not from this cache</td></tr><tr><td>Disable image optimization</td><td>Doesn&apos;t help</td></tr><tr><td>&lt;code&gt;--max-old-space-size=6144&lt;/code&gt;</td><td>Just crashes slower</td></tr><tr><td>Read response via &lt;code&gt;.json()&lt;/code&gt; / &lt;code&gt;.text()&lt;/code&gt;</td><td>Doesn&apos;t help</td></tr><tr><td>Remove React fetch patching (canary.45)</td><td>Doesn&apos;t help</td></tr><tr><td>Upgrade to latest version</td><td>Doesn&apos;t help (leaks on 16.2.0-canary.51 too)</td></tr></tbody></table>
<h2>Ce funcționează (workaround-uri)</h2>
<table><thead><tr><th>Workaround</th><th>Why it helps</th></tr></thead><tbody><tr><td>Replace &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;axios&lt;/code&gt;</td><td>Bypasses Next.js wrapper</td></tr><tr><td>Replace &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;node-fetch&lt;/code&gt;</td><td>Same reason</td></tr><tr><td>Downgrade Node.js to 20.15.1</td><td>Older undici has fewer leaks (for some)</td></tr><tr><td>Docker image &lt;code&gt;node:20-alpine3.21&lt;/code&gt;</td><td>Helps in some cases</td></tr><tr><td>Deploy to Lambda (SST + OpenNext)</td><td>Ephemeral functions — memory doesn&apos;t accumulate</td></tr><tr><td>Deploy to Vercel</td><td>Same — serverless</td></tr></tbody></table>
<h3>Limitarea workaround-ului cu axios</h3>
<p>Propriile apeluri API pot fi înlocuite cu axios. Dar Next.js folosește intern fetch-ul modificat pentru:</p>
<ul><li>ISR (Incremental Static Regeneration)</li><li><code>revalidatePath</code> / <code>revalidateTag</code></li><li>Preluarea datelor în Server Components cu deduplicare</li><li><code>use cache</code> (Next.js 16)</li></ul>
<p>Chiar și fără un singur <code>fetch</code> în codul tău — Next.js îl folosește tot intern.</p>
<h2>De ce Vercel nu rezolvă problema</h2>
<h3>Logica de business</h3>
<p>Vercel este o companie care câștigă bani din găzduirea Next.js. Pe platforma lor problema nu se manifestă (serverless = efemer). Bug-ul afectează doar self-hosted (Docker, K8s, VPS) — adică pe cei care nu plătesc Vercel.</p>
<h3>Poziția oficială</h3>
<p>Tim Neutkens (maintainer Vercel) a analizat problema și a declarat că este o problemă de <strong>undici</strong> (biblioteca fetch din Node.js), nu de Next.js. Issue-ul #90433 a fost închis. În ciuda faptului că:</p>
<ul><li>axios și node-fetch pe același Node.js funcționează fără scurgeri</li><li>Scurgerea apare doar când fetch trece prin wrapper-ul Next.js</li><li>Bug-ul este deschis de 2 ani fără o rezolvare</li></ul>
<h3>Priorități</h3>
<p>În acești 2 ani echipa Next.js a livrat:</p>
<ul><li>Turbopack (build-uri de 2-5x mai rapide) — avantaj de marketing</li><li>Cache Components / <code>use cache</code> — reduce încărcarea pe serverele Vercel</li><li><code>proxy.ts</code> în locul middleware-ului — simplifică deployment-ul edge pe Vercel</li><li>DevTools MCP — hype-ul AI</li></ul>
<p>Memory leak în self-hosted? Nu e o prioritate.</p>
<h2>Soluția: AWS Lambda (SST + OpenNext)</h2>
<h3>Ce este</h3>
<p>OpenNext este un adaptor open-source care convertește un build Next.js într-un format pentru AWS Lambda. SST este un framework care automatizează infrastructura.</p>
<h3>Arhitectură</h3>
<pre><code>Next.js build
  → OpenNext
    → AWS Lambda (SSR, API routes)
    → S3 (static, assets)
    → CloudFront (CDN)
    → SQS + DynamoDB (ISR revalidation)</code></pre>
<h3>De ce asta rezolvă memory leak-ul</h3>
<p>Funcțiile Lambda procesează cererile și sunt reciclate după 5-15 minute de inactivitate. Memoria nu apucă să se acumuleze.</p>
<h3>Deployment</h3>
<pre><code>npx sst@latest init
npx sst deploy --stage production</code></pre>
<h3>Comparație</h3>
<table><thead><tr><th></th><th>Vercel</th><th>AWS Lambda (SST)</th><th>Docker self-hosted</th></tr></thead><tbody><tr><td>Memory leak</td><td>Not felt</td><td>Not felt</td><td>Critical</td></tr><tr><td>Cold starts</td><td>Yes</td><td>Yes (~200-500ms)</td><td>No</td></tr><tr><td>Price (~medium traffic)</td><td>$20-150/mo</td><td>$5-30/mo</td><td>$5-50/mo</td></tr><tr><td>Control</td><td>Minimal</td><td>Full</td><td>Full</td></tr><tr><td>ISR/Revalidation</td><td>Works</td><td>Works (SQS)</td><td>Works (with leak)</td></tr><tr><td>Vendor lock-in</td><td>Vercel</td><td>AWS</td><td>None</td></tr></tbody></table>
<h3>Nuanțe Lambda</h3>
<ul><li><strong>Cold starts</strong> — prima cerere este mai lentă (~200-500ms)</li><li><strong>Securitate</strong> — activează OAC (Origin Access Control), altfel URL-ul Lambda este public</li><li><strong>OpenNext</strong> — proiect comunitar, nu oficial Vercel. Funcțiile noi din Next.js se pot strica</li><li><strong>Atac asupra portofelului</strong> — în timpul unui DDoS, auto-scaling-ul Lambda poate genera o factură mare</li></ul>
<h2>De ce o rezolvare reală este nerealistă</h2>
<h3>1. Problemă arhitecturală</h3>
<p>Scurgerea nu este un bug accidental, ci consecința unei decizii de design: Next.js interceptează <code>fetch</code>-ul global și adaugă cache/tracking peste el. Ca să o repari — trebuie regândit modul în care App Router interacționează cu fetch. Asta afectează ISR, revalidation, data cache, request deduplication — nucleul framework-ului.</p>
<h3>2. Conflict de interese</h3>
<p>Vercel nu este motivat să repare ceva ce nu le afectează platforma. Self-hosted concurează cu business-ul lor. Cu cât sunt mai multe probleme în self-hosted — cu atât mai mulți oameni migrează pe Vercel.</p>
<h3>3. Pasarea vinei</h3>
<p>Poziția oficială este "e undici, nu noi". Până când asta nu se schimbă — nu vor lucra la o rezolvare.</p>
<h3>4. Nicio rezolvare din comunitate</h3>
<p>Licența AGPL-3.0 a Next.js permite fork-uri, dar codebase-ul este uriaș și strâns cuplat cu infrastructura Vercel. Un PR din comunitate pentru a repara wrapper-ul fetch ar necesita o înțelegere profundă a arhitecturii interne și aprobarea maintainerilor — care au închis deja issue-ul.</p>
<h2>Concluzii</h2>
<ol><li><strong>Dacă ești pe Vercel</strong> — nicio problemă, nu ai ce face</li><li><strong>Dacă ești self-hosted și ai nevoie de serverless</strong> — SST + OpenNext pe AWS Lambda</li><li><strong>Dacă ești self-hosted Docker</strong> — înlocuiește fetch cu axios unde se poate, monitorizează RAM-ul, configurează restart automat al pod-urilor</li><li><strong>Dacă începi un proiect nou</strong> — ia în calcul SvelteKit sau Nuxt ca alternative fără această problemă</li></ol>
<h2>Surse</h2>
<ul><li><a href="https://github.com/vercel/next.js/issues/64212" target="_blank" rel="noopener noreferrer">Issue #64212</a> — Memory Leak with global fetch (April 2024)</li><li><a href="https://github.com/vercel/next.js/issues/68578" target="_blank" rel="noopener noreferrer">Issue #68578</a> — Possible memory leak in Fetch API (August 2024)</li><li><a href="https://github.com/vercel/next.js/issues/85914" target="_blank" rel="noopener noreferrer">Issue #85914</a> — Memory Leak with fetch + standalone (November 2025)</li><li><a href="https://github.com/vercel/next.js/issues/90433" target="_blank" rel="noopener noreferrer">Issue #90433</a> — OOM in 16.0.10 (February 2026)</li><li><a href="https://github.com/vercel/next.js/discussions/88603" target="_blank" rel="noopener noreferrer">Discussion #88603</a> — OOM in Docker/K8s (January 2026)</li><li><a href="https://github.com/vercel/next.js/discussions/88078" target="_blank" rel="noopener noreferrer">Discussion #88078</a> — cacheMaxMemorySize behavior</li><li><a href="https://opennext.js.org/" target="_blank" rel="noopener noreferrer">OpenNext</a></li><li><a href="https://sst.dev/docs/start/aws/nextjs/" target="_blank" rel="noopener noreferrer">SST — Next.js on AWS</a></li><li><a href="https://www.flightcontrol.dev/blog/secret-knowledge-to-self-host-nextjs" target="_blank" rel="noopener noreferrer">Secret knowledge to self-host Next.js</a></li><li><a href="https://nextjs.org/docs/app/guides/memory-usage" target="_blank" rel="noopener noreferrer">Next.js Memory Usage Guide</a></li></ul>]]></content:encoded>
      <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
      <category>Next.js</category>
      <category>Memory Leak</category>
      <category>Docker</category>
      <category>AWS Lambda</category>
      <category>Node.js</category>
    </item>
  </channel>
</rss>