Przejdź do głównej treści

ADR Maker

Obraz ADR

ADR Maker — decyzje architektoniczne, które mają sens

Ile razy w projekcie padło: „czemu my to w ogóle tak zrobiliśmy?” ADR Maker powstał właśnie po to, żeby nigdy więcej nie zgadywać.

To lekka aplikacja webowa, która pomaga szybko tworzyć i utrzymywać Architecture Decision Records w formacie Markdown, gotowym do wrzucenia do repozytorium.

Dlaczego warto?

  • Zero backendu — działa lokalnie, offline, w przeglądarce
  • Spójny format ADR w całym zespole
  • Live preview Markdown — widzisz dokładnie to, co trafi do repo
  • Lista wielu ADR-ów — porządek zamiast chaosu
  • Eksport do ZIP — jeden klik i cały zestaw decyzji jest gotowy
  • Git-friendly — każdy ADR jako osobny plik .md

Dla kogo?

  • zespoły developerskie,
  • architektów i tech leadów,
  • projekty, które chcą dokumentować dlaczego, a nie tylko co.

Jak używać?

  1. Otwórz aplikację w przeglądarce
  2. Utwórz ADR z pomocą formularza
  3. Zapisz, edytuj, aktualizuj decyzje
  4. Wyeksportuj Markdown lub cały ZIP do repo

Bez konfiguracji. Bez narzędzi pośrednich. Bez bólu.

Wskazówka: w polach tekstowych stosuj składnię markdown.

Efekt?

  • lepsze decyzje,
  • łatwiejszy onboarding,
  • mniej „legacy mysteries”,
  • dokumentacja, która żyje razem z kodem.

ADR Maker — bo dobra architektura zasługuje na dobrą pamięć.

ADR
    Wypełnij pola aby zobaczyć wynik...
    Wypełnij pola aby zobaczyć wynik...

    Kod po stronie przeglądarki

    <link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet">
    <style>
        .adr-item { cursor: pointer; }
    </style>
    <div id="app" class="py-3">
        <div class="row g-3">
            <!-- ADR LIST -->
            <div class="col-md-3">
                <div class="card h-100">
                    <div class="card-header d-flex justify-content-between align-items-center">
                        <strong>ADR</strong>
                        <div class="btn-group">
                            <button class="btn btn-sm btn-primary" onclick="newADR()" aria-label="Nowy ADR" title="Nowy ADR"><i class="bi bi-plus-lg"></i></button>
                            <button class="btn btn-sm btn-success" onclick="exportAllToZip()" aria-label="Eksportuj do ZIP" title="Eksportuj do ZIP"><i class="bi bi-download"></i></button>
                        </div>
                    </div>
                    <ul class="list-group list-group-flush" id="adrList"></ul>
                </div>
            </div>
            <!-- FORM -->
            <div class="col-md-9">
                <div class="card">
                    <div class="card-body">
                        <input type="hidden" id="currentADR">
                        <div class="mb-2">
                            <label for="adrNumber" class="form-label">Numer ADR</label>
                            <input id="adrNumber" class="form-control">
                        </div>
                        <div class="mb-2">
                            <label for="adrTitle" class="form-label">Tytuł</label>
                            <input id="adrTitle" class="form-control">
                        </div>
                        <div class="mb-2">
                            <label for="adrStatus" class="form-label">Status</label>
                            <select id="adrStatus" class="form-select">
                                <option>Proposed</option>
                                <option>Accepted</option>
                                <option>Deprecated</option>
                                <option>Superseded</option>
                            </select>
                        </div>
                        <div class="mb-2">
                            <label for="adrDate" class="form-label">Data</label>
                            <input id="adrDate" type="date" class="form-control">
                        </div>
                        <div class="mb-2">
                            <label for="adrContext" class="form-label">Kontekst</label>
                            <textarea id="adrContext" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="mb-2">
                            <label for="adrDecision" class="form-label">Decyzja</label>
                            <textarea id="adrDecision" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="mb-2">
                            <label for="adrRationale" class="form-label">Uzasadnienie</label>
                            <textarea id="adrRationale" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="mb-2">
                            <label for="adrPros" class="form-label">Pozytywne</label>
                            <textarea id="adrPros" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="mb-2">
                            <label for="adrCons" class="form-label">Negatywne</label>
                            <textarea id="adrCons" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="mb-2">
                            <label for="adrAlternatives" class="form-label">Alternatywy</label>
                            <textarea id="adrAlternatives" class="form-control" rows="10"></textarea>
                        </div>
                        <div class="d-flex gap-2">
                            <button class="btn btn-success" onclick="saveADR()"><i class="bi bi-floppy"></i> Zapisz dokument</button>
                            <button class="btn btn-outline-secondary" onclick="loadExample()"><i class="bi bi-folder2-open"></i> Załaduj przykład</button>
                            <button class="btn btn-outline-danger" onclick="deleteADR()"><i class="bi bi-trash"></i> Usuń dokument</button>
                        </div>
                    </div>
                </div>
            </div>
            <!-- PREVIEW -->
            <div class="col-md-12">
                <div class="card p-3">
                    <ul class="nav nav-underline mt-3">
                        <li class="nav-item" role="presentation">
                            <button class="nav-link text-body active" id="tab-mdview-tab" data-bs-toggle="tab" data-bs-target="#tab-mdview" type="button" role="tab">Markdown</button>  
                        </li>
                        <li class="nav-item" role="presentation">
                            <button class="nav-link text-body" id="tab-htmlview-tab" data-bs-toggle="tab" data-bs-target="#tab-htmlview" type="button" role="tab">HTML</button>            
                        </li>
                    </ul>
                    <div class="tab-content">
                        <div id="tab-mdview" class="tab-pane fade show active" role="tabpanel">
                            <pre><code id="md-preview" class="language-markdown">Wypełnij pola aby zobaczyć wynik...</code></pre>
                        </div>
                        <div id="tab-htmlview" class="tab-pane fade" role="tabpanel">
                            <div id="html-preview" class="p-3">Wypełnij pola aby zobaczyć wynik...</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
    <script>
    const mdPreview = document.getElementById('md-preview');
    const htmlPreview = document.getElementById('html-preview');
    const fields = [
        'adrNumber', 'adrTitle', 'adrStatus', 'adrDate',
        'adrContext', 'adrDecision', 'adrRationale',
        'adrPros', 'adrCons', 'adrAlternatives'
    ];
        
    const example = {
        "adrNumber": "ADR-001",
        "adrTitle": "Wybór bazy danych dla systemu zamówień",
        "adrStatus": "Accepted",
        "adrDate": "2026-02-05",
        "adrContext": "System zamówień obsługuje dane transakcyjne (zamówienia, płatności, faktury). Wymagana jest spójność danych (ACID), relacyjny model oraz możliwość łatwego skalowania w chmurze.\n\nRozważane opcje:\n\n- PostgreSQL\n- MySQL\n- MongoDB",
        "adrDecision": "Wybrano **PostgreSQL** jako główną bazę danych systemu.",
        "adrRationale": "PostgreSQL został wybrany ze względu na pełne wsparcie dla właściwości ACID, co jest kluczowe dla systemu obsługującego zamówienia, płatności i faktury. Relacyjny model danych dobrze odpowiada strukturze domeny biznesowej oraz ułatwia egzekwowanie integralności danych (klucze obce, ograniczenia, transakcje).\n\nDodatkowo PostgreSQL oferuje:\n\n- zaawansowane mechanizmy transakcyjne (MVCC),\n- bogate wsparcie dla zapytań analitycznych,\n- możliwość rozszerzania funkcjonalności (np. indeksy GIN/GiST, JSONB),\n- bardzo dobrą integrację z dostawcami chmurowymi (AWS RDS, Azure Database, GCP Cloud SQL).\n\nTe cechy czynią PostgreSQL rozwiązaniem bezpiecznym i przyszłościowym dla systemu o rosnącej skali.",
        "adrPros": "- pełne wsparcie transakcji,\n- bogaty ekosystem,\n- dobre wsparcie w chmurze.",
        "adrCons": "- większe zużycie zasobów niż MySQL,\n- konieczność monitorowania wydajności przy dużej skali.",
        "adrAlternatives": "**MySQL**\n\nMySQL był rozważany jako lżejsza alternatywa relacyjna, jednak:\n\n- oferuje mniej zaawansowane mechanizmy transakcyjne i spójności danych niż PostgreSQL,\n- ma ograniczenia w zakresie złożonych zapytań i rozszerzalności,\n- w praktyce gorzej radzi sobie z bardziej skomplikowaną logiką domenową.\n\nZ tego względu został odrzucony mimo niższego zużycia zasobów.\n\n**MongoDB**\n\nMongoDB zapewnia wysoką elastyczność schematu oraz łatwe skalowanie horyzontalne, jednak:\n\n- nie jest bazą relacyjną, co utrudnia modelowanie silnie powiązanych danych,\n- pełne wsparcie transakcji wielodokumentowych jest mniej dojrzałe i wiąże się z narzutem wydajnościowym,\n- brak relacyjnych mechanizmów integralności danych zwiększa ryzyko niespójności w systemie finansowym.\n\nZ tych powodów MongoDB nie spełnia kluczowych wymagań systemu zamówień."
    };
    
    fields.forEach(id =>
        document.getElementById(id).addEventListener('input', updatePreview)
    );
    
    function getAllADR() {
        return JSON.parse(localStorage.getItem('adrs') || '{}');
    }
    
    function saveAllADR(data) {
        localStorage.setItem('adrs', JSON.stringify(data));
    }
    
    function newADR() {
        fields.forEach(id => document.getElementById(id).value = '');
        currentADR.value = '';
        updatePreview();
    }
    
    function saveADR() {
        if (!adrNumber.value) return alert('Numer ADR jest wymagany');
    
        const adrs = getAllADR();
        const data = {};
        fields.forEach(id => data[id] = document.getElementById(id).value);
    
        adrs[adrNumber.value] = data;
        saveAllADR(adrs);
        currentADR.value = adrNumber.value;
    
        renderList();
    }
    
    function loadExample() {
        const data = example;
    
        fields.forEach(f => document.getElementById(f).value = data[f] || '');
        currentADR.value = 0;
    
        updatePreview();
    }
        
    function loadADR(id) {
        const adrs = getAllADR();
        const data = adrs[id];
        if (!data) return;
    
        fields.forEach(f => document.getElementById(f).value = data[f] || '');
        currentADR.value = id;
    
        document.querySelectorAll('.adr-item').forEach(i => i.classList.remove('active'));
        document.getElementById('item-' + id).classList.add('active');
    
        updatePreview();
    }
    
    function deleteADR() {
        if (!currentADR.value) return;
        if (!confirm('Usunąć dokument?')) return;
    
        const adrs = getAllADR();
        delete adrs[currentADR.value];
        saveAllADR(adrs);
    
        newADR();
        renderList();
    }
    
    function renderList() {
        const adrs = getAllADR();
        adrList.innerHTML = '';
    
        Object.keys(adrs).sort().forEach(id => {
            const li = document.createElement('li');
            li.className = 'list-group-item adr-item';
            li.id = 'item-' + id;
            li.textContent = id + ' – ' + adrs[id].adrTitle;
            li.onclick = () => loadADR(id);
            adrList.appendChild(li);
        });
    }
    
    function mdFromData(d) {
        const list = t => t.split('\n').filter(Boolean).map(x => `- ${x}`).join('\n');
        return `
    # ${d.adrNumber}: ${d.adrTitle}
    
    **Status:** ${d.adrStatus}  
    **Data:** ${d.adrDate}
    
    ## Kontekst
    ${d.adrContext}
    
    ## Decyzja
    ${d.adrDecision}
    
    ## Uzasadnienie
    ${d.adrRationale}
    
    ## Konsekwencje
    
    ### Pozytywne
    ${d.adrPros}
    
    ### Negatywne
    ${d.adrCons}
    
    ## Alternatywy
    ${d.adrAlternatives}
    `.trim();
    }
    
    function generateMarkdown() {
        const d = {};
        fields.forEach(f => d[f] = document.getElementById(f).value);
        return mdFromData(d);
    }
    
    function escapeHtml(s) {
        if (!s) return '';
        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }
    
    function updatePreview() {
        const md = generateMarkdown();
        mdPreview.textContent = md;
        htmlPreview.innerHTML = marked.parse(md);
    
        const el = mdPreview;
        if (el) {
            if (el.hasAttribute('data-highlighted'))
                el.removeAttribute('data-highlighted');
            if (el.hasAttribute('data-highlighter'))
                el.removeAttribute('data-highlighter');
            if (el.classList.contains('hljs'))
                el.classList.remove(...Array.from(el.classList).filter(cls => cls.startsWith('hljs')));
            el.innerHTML = escapeHtml(el.textContent);
            hljs.highlightElement(el);
        }
    
    }
    
    async function exportAllToZip() {
        const adrs = getAllADR();
        if (Object.keys(adrs).length === 0) {
            alert('Brak ADR do eksportu');
            return;
        }
    
        const zip = new JSZip();
    
        Object.values(adrs).forEach(d => {
            const filename = (d.adrNumber || 'ADR') + '.md';
            zip.file(filename, mdFromData(d));
        });
    
        const blob = await zip.generateAsync({
            type: 'blob'
        });
        saveAs(blob, 'adrs.zip');
    }
    
    renderList();    
    </script>
    

    Kod po stronie serwera

    Brak kodu serwera

    Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.

    Licencja

    ## BSD-3-Clause License Agreement
    BSD-3-Clause
    Сopyright (c) 2026 Dariusz Rorat
    
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    1. Redistributions of source code must retain the above copyright notice,
       this list of conditions and the following disclaimer.
    
    2. Redistributions in binary form must reproduce the above copyright notice,
       this list of conditions and the following disclaimer in the documentation
       and/or other materials provided with the distribution.
    
    3. Neither the name of the copyright holder nor the names of its
       contributors may be used to endorse or promote products derived from
       this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    5 lutego 2026 3

    Kategorie

    Technologie

    Dziękujemy!
    ()

    Informacja o cookies

    Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.