ADR Maker

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ć?
- Otwórz aplikację w przeglądarce
- Utwórz ADR z pomocą formularza
- Zapisz, edytuj, aktualizuj decyzje
- 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ęć.
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
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.