KabaLang örnekleri: konsol, masaüstü ve web için dokuz proje

Konsol: fraktal (paralel şeritli 24-bit Mandelbrot/Julia + yakınlaşma),
sudoku (geri izlemeli, canlı animasyon), fatura (JSON → terminal tablosu,
PDF ve Excel).
Masaüstü: hesap makinesi (işlem önceliği, canlı bağlama), odak (Pomodoro),
parola atölyesi (entropi ölçer, SHA-256).
Web: pano (Kanban REST API + SQLite), kisalt (302 yönlendirme, SVG
istatistik), anket (adres başına tek oy, sunucuda çizilen grafik).

Hepsi `kabalang calistir` ile derlenip çalışır; web uygulamaları yalnız
127.0.0.1'i dinler, SQL parametre bağlamalı, HTML çıktısı kaçırılır.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
2026-10-02 22:31:40 +03:00
co-authored by Claude Opus 5.5
commit ed201188fb
39 changed files with 2650 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Pano — Kanban görev panosu
![Pano](../../medya/pano.png)
Sunucusu **KabaLang** ile yazılmış bir Kanban panosu: görevler *Yapılacak · Sürüyor · Bitti*
sütunlarında, kartlar sürükle-bırak ile taşınır, her değişiklik SQLite'a yazılır.
```bash
cd web/pano
kabalang calistir pano.kbl
# → http://127.0.0.1:8471
```
## Neyi gösteriyor
| Konu | Nerede |
|---|---|
| `kaba.web` ile JSON REST API — `al` · `gonder` · `guncelle` · `kaldir` | `pano.kbl` sonu |
| Yol parametresi `/api/gorevler/{id}` | `guncelle`, `kaldir` |
| `kaba.veritabani` — **her sorgu parametre bağlamalı** (`?`) | tüm `veritabani.sqlite(..., [..])` çağrıları |
| Gelen JSON'u `json.gecerli_mi` + `json.al` ile okumak | `olustur` |
| Durum kodlu yanıtlar: 201 · 204 · 400 · 404 · 422 | `hata_json`, `web.json(g, 201)` |
| Sunucunun kendi hatalarının da JSON olması (405, 413) | `sunucu.hatalar_json()` |
| Kapalı küme denetimi (`SUTUNLAR.icerir(sutun)`) | `olustur`, `guncelle` |
## API
```bash
curl http://127.0.0.1:8471/api/gorevler
curl -X POST -d '{"baslik":"Yeni iş","oncelik":"yuksek"}' http://127.0.0.1:8471/api/gorevler
curl -X PUT -d '{"sutun":"bitti"}' http://127.0.0.1:8471/api/gorevler/3
curl -X DELETE http://127.0.0.1:8471/api/gorevler/3
```
## Güvenlik
- **SQL enjeksiyonu:** kullanıcı verisi sorguya hiçbir zaman metin olarak eklenmez, `?` ile
bağlanır. `"x'); DROP TABLE gorev; --"` başlıklı bir görev yalnızca tuhaf adlı bir görevdir.
- **XSS:** ön yüz kart içeriğini `textContent` ile basar; `<script>` başlıklı bir görev ekranda
düz yazı olarak görünür.
- Sunucu yalnız `127.0.0.1`'i dinler ve tek iş parçacığıyla çalışır (SQLite'a sırayla yazılır).
Veritabanı yolu `PANO_DB` ortam değişkeniyle değiştirilebilir (varsayılan `pano.db`).
![Mobil](../../medya/pano-mobil.png)
+217
View File
@@ -0,0 +1,217 @@
<!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pano · KabaLang</title>
<style>
:root {
--zemin: #0a0d15; --yuzey: #121826; --yuzey2: #182033; --cizgi: #232c42;
--metin: #e6ebf5; --soluk: #8a94ab; --mavi: #377ef4; --cam: #22d3ee;
--yesil: #34d399; --sari: #fbbf24; --kirmizi: #f87171;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; color: var(--metin);
font: 15px/1.5 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
background:
radial-gradient(900px 500px at 10% -10%, #377ef42a, transparent 60%),
radial-gradient(700px 400px at 100% 0%, #22d3ee1c, transparent 60%),
var(--zemin);
}
header {
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
padding: 22px 32px; border-bottom: 1px solid var(--cizgi);
backdrop-filter: blur(8px);
}
.logo {
width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
background: linear-gradient(135deg, var(--mavi), var(--cam)); font-weight: 800; color: #fff;
box-shadow: 0 6px 22px #377ef455;
}
h1 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
.alt { color: var(--soluk); font-size: 13px; }
.rozet {
margin-left: auto; font-size: 12px; color: var(--cam); border: 1px solid #22d3ee44;
padding: 5px 11px; border-radius: 999px; background: #22d3ee10;
}
main { padding: 26px 32px 40px; max-width: 1360px; margin: 0 auto; }
form.ekle {
display: grid; grid-template-columns: 2fr 2fr 1fr auto; gap: 10px;
background: var(--yuzey); border: 1px solid var(--cizgi); border-radius: 16px; padding: 14px;
margin-bottom: 24px;
}
input, select, button {
font: inherit; color: var(--metin); background: var(--yuzey2);
border: 1px solid var(--cizgi); border-radius: 10px; padding: 10px 12px; min-width: 0;
}
input:focus, select:focus { outline: 2px solid #377ef488; border-color: transparent; }
button {
background: linear-gradient(135deg, var(--mavi), #2563eb); border: 0; font-weight: 600;
cursor: pointer; padding: 10px 18px; box-shadow: 0 6px 18px #377ef440;
}
.sutunlar { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.sutun {
background: #121826cc; border: 1px solid var(--cizgi); border-radius: 18px;
padding: 14px; min-height: 380px; transition: border-color .15s, background .15s;
}
.sutun.ustunde { border-color: var(--mavi); background: #377ef412; }
.sutun h2 {
font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--soluk);
margin: 4px 6px 14px; display: flex; align-items: center; gap: 8px;
}
.nokta { width: 9px; height: 9px; border-radius: 50%; }
.sayi { margin-left: auto; background: var(--yuzey2); border-radius: 999px; padding: 1px 9px; font-size: 12px; }
.kart {
background: var(--yuzey2); border: 1px solid var(--cizgi); border-radius: 14px;
padding: 12px 14px; margin-bottom: 10px; cursor: grab; position: relative;
transition: transform .12s, box-shadow .12s;
}
.kart:hover { transform: translateY(-2px); box-shadow: 0 10px 26px #0008; }
.kart.sur { opacity: .45; }
.kart b { display: block; font-weight: 600; padding-right: 26px; }
.kart p { margin: 4px 0 10px; color: var(--soluk); font-size: 13px; }
.alt-satir { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--soluk); }
.etiket { padding: 2px 9px; border-radius: 999px; font-weight: 600; font-size: 11px; }
.yuksek { color: var(--kirmizi); background: #f8717118; }
.orta { color: var(--sari); background: #fbbf2418; }
.dusuk { color: var(--yesil); background: #34d39918; }
.sil {
position: absolute; top: 8px; right: 8px; background: none; box-shadow: none; color: var(--soluk);
padding: 2px 8px; font-size: 16px; opacity: 0; transition: opacity .15s;
}
.kart:hover .sil { opacity: 1; }
.tasi { display: none; gap: 6px; margin-top: 8px; }
.tasi button { padding: 5px 10px; font-size: 12px; box-shadow: none; background: var(--yuzey); border: 1px solid var(--cizgi); }
footer { text-align: center; color: var(--soluk); font-size: 12px; padding: 0 0 28px; }
footer code { color: var(--cam); }
@media (max-width: 820px) {
header, main { padding-left: 16px; padding-right: 16px; }
form.ekle { grid-template-columns: 1fr 1fr; }
form.ekle input[name=baslik], form.ekle button { grid-column: 1 / -1; }
.sutunlar { grid-template-columns: 1fr; }
.sutun { min-height: 0; }
.tasi { display: flex; }
.sil { opacity: 1; }
}
</style>
</head>
<body>
<header>
<div class="logo">K</div>
<div>
<h1>Proje Panosu</h1>
<div class="alt">Sunucusu KabaLang ile yazıldı · SQLite · REST API</div>
</div>
<span class="rozet" id="ozet">yükleniyor…</span>
</header>
<main>
<form class="ekle" id="form">
<input name="baslik" placeholder="Yeni görev başlığı" maxlength="120" required>
<input name="aciklama" placeholder="Kısa açıklama (isteğe bağlı)" maxlength="200">
<select name="oncelik">
<option value="orta">Orta öncelik</option>
<option value="yuksek">Yüksek öncelik</option>
<option value="dusuk">Düşük öncelik</option>
</select>
<button type="submit">+ Ekle</button>
</form>
<div class="sutunlar" id="sutunlar"></div>
</main>
<footer>Kartları sütunlar arasında sürükleyin · <code>GET /api/gorevler</code></footer>
<script>
const SUTUNLAR = [
["yapilacak", "Yapılacak", "#8a94ab"],
["suruyor", "Sürüyor", "#377ef4"],
["bitti", "Bitti", "#34d399"],
];
const ONCELIK = { yuksek: "Yüksek", orta: "Orta", dusuk: "Düşük" };
const kok = document.getElementById("sutunlar");
let gorevler = [];
async function api(yontem, yol, govde) {
const y = await fetch(yol, {
method: yontem,
headers: govde ? { "Content-Type": "application/json" } : {},
body: govde ? JSON.stringify(govde) : undefined,
});
if (!y.ok && y.status !== 204) throw new Error((await y.json()).hata || y.status);
return y.status === 204 ? null : y.json();
}
function el(etiket, sinif, metin) {
const e = document.createElement(etiket);
if (sinif) e.className = sinif;
if (metin !== undefined) e.textContent = metin; // textContent → XSS yok
return e;
}
function ciz() {
kok.replaceChildren();
for (const [ad, baslik, renk] of SUTUNLAR) {
const liste = gorevler.filter(g => g.sutun === ad);
const s = el("section", "sutun");
s.dataset.sutun = ad;
const h = el("h2");
const n = el("span", "nokta"); n.style.background = renk;
h.append(n, baslik, el("span", "sayi", liste.length));
s.append(h);
for (const g of liste) s.append(kart(g));
s.addEventListener("dragover", e => { e.preventDefault(); s.classList.add("ustunde"); });
s.addEventListener("dragleave", () => s.classList.remove("ustunde"));
s.addEventListener("drop", e => {
e.preventDefault(); s.classList.remove("ustunde");
tasi(+e.dataTransfer.getData("text/plain"), ad);
});
kok.append(s);
}
const bitti = gorevler.filter(g => g.sutun === "bitti").length;
document.getElementById("ozet").textContent =
`${gorevler.length} görev · %${gorevler.length ? Math.round(100 * bitti / gorevler.length) : 0} tamam`;
}
function kart(g) {
const k = el("article", "kart");
k.draggable = true;
k.addEventListener("dragstart", e => { e.dataTransfer.setData("text/plain", g.id); k.classList.add("sur"); });
k.addEventListener("dragend", () => k.classList.remove("sur"));
k.append(el("b", "", g.baslik));
if (g.aciklama) k.append(el("p", "", g.aciklama));
const alt = el("div", "alt-satir");
alt.append(el("span", "etiket " + g.oncelik, ONCELIK[g.oncelik] || g.oncelik), "#" + g.id);
k.append(alt);
const sil = el("button", "sil", "×");
sil.title = "Sil";
sil.onclick = async () => { await api("DELETE", "/api/gorevler/" + g.id); yukle(); };
k.append(sil);
const t = el("div", "tasi");
for (const [ad, baslik] of SUTUNLAR) if (ad !== g.sutun) {
const b = el("button", "", "→ " + baslik);
b.onclick = () => tasi(g.id, ad);
t.append(b);
}
k.append(t);
return k;
}
async function tasi(id, sutun) {
await api("PUT", "/api/gorevler/" + id, { sutun });
yukle();
}
async function yukle() { gorevler = await api("GET", "/api/gorevler"); ciz(); }
document.getElementById("form").addEventListener("submit", async e => {
e.preventDefault();
const f = new FormData(e.target);
await api("POST", "/api/gorevler", {
baslik: f.get("baslik"), aciklama: f.get("aciklama"), oncelik: f.get("oncelik"), sutun: "yapilacak",
});
e.target.reset();
yukle();
});
yukle();
</script>
</body>
</html>
+158
View File
@@ -0,0 +1,158 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ PANO — KabaLang ile Kanban görev panosu ║
# ║ ║
# ║ · JSON REST API: GET · POST · PUT · DELETE /api/gorevler ║
# ║ · Kalıcı kayıt: SQLite, her sorgu PARAMETRE BAĞLAMALI ║
# ║ · Ön yüz: arayuz.html (tek sayfa, dış bağımlılık yok) ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# Çalıştır: cd web/pano && kabalang calistir pano.kbl
# Aç: http://127.0.0.1:8471
kullan kaba.web
kullan kaba.json
kullan kaba.veritabani
kullan kaba.sistem
# Panoda yalnız bu üç sütun vardır. Kapalı küme → istemciden gelen
# sütun adı bu listede değilse istek reddedilir.
SUTUNLAR = ["yapilacak", "suruyor", "bitti"]
ONCELIKLER = ["dusuk", "orta", "yuksek"]
db = cevre.al("PANO_DB")
eger db == "":
db = "pano.db"
# ── Veritabanı ──────────────────────────────────────────────────────
fonksiyon hazirla(db):
veritabani.sqlite(db, "CREATE TABLE IF NOT EXISTS gorev(id INTEGER PRIMARY KEY AUTOINCREMENT, baslik TEXT NOT NULL, aciklama TEXT DEFAULT '', sutun TEXT NOT NULL, oncelik TEXT DEFAULT 'orta', olusturma TEXT);")
# İlk açılışta pano boş görünmesin: birkaç örnek kart.
eger veritabani.sqlite(db, "SELECT count(*) FROM gorev") == "0":
ornekler = [
["Giriş ekranını tasarla", "Koyu tema, mobil öncelikli", "bitti", "yuksek"],
["REST API taslağı", "GET · POST · PUT · DELETE uçları", "bitti", "orta"],
["SQLite şeması", "Parametre bağlamalı sorgular", "suruyor", "yuksek"],
["Sürükle-bırak kartlar", "Sütunlar arası taşıma", "suruyor", "orta"],
["Bildirim e-postaları", "Görev atanınca haber ver", "yapilacak", "dusuk"],
["Karanlık/aydınlık tema", "Sistem tercihine uy", "yapilacak", "orta"],
["Yük testi", "1000 eşzamanlı istek", "yapilacak", "yuksek"]
]
dongu o icinde ornekler:
veritabani.sqlite(db, "INSERT INTO gorev(baslik, aciklama, sutun, oncelik, olusturma) VALUES (?, ?, ?, ?, ?)", [o[0], o[1], o[2], o[3], zaman.simdi()])
# Bütün görevleri JSON dizi olarak verir. JSON'u SQLite'ın kendi
# json_object/json_group_array işlevleri kurar → elle kaçış yazmayız.
fonksiyon gorevler_json(db):
sonuc = veritabani.sqlite(db, "SELECT json_group_array(json_object('id', id, 'baslik', baslik, 'aciklama', aciklama, 'sutun', sutun, 'oncelik', oncelik, 'olusturma', olusturma)) FROM (SELECT * FROM gorev ORDER BY id)")
eger sonuc == "":
don "[]"
don sonuc
fonksiyon gorev_json(db, kimlik):
don veritabani.sqlite(db, "SELECT json_object('id', id, 'baslik', baslik, 'aciklama', aciklama, 'sutun', sutun, 'oncelik', oncelik, 'olusturma', olusturma) FROM gorev WHERE id = ?", [kimlik])
# ── Doğrulama ───────────────────────────────────────────────────────
fonksiyon hata_json(ileti, durum):
don web.json(json.uret({"hata": ileti}), durum)
# Kimlik yalnız rakamlardan oluşmalı; "1 OR 1=1" gibi bir değer
# zaten parametre olarak bağlanır, ama anlamsız istek erkenden döner.
fonksiyon kimlik_gecerli(k):
eger k.uzunluk() == 0 veya k.uzunluk() > 9:
don yanlis
dongu i icinde 0..k.uzunluk() - 1:
eger degil "0123456789".icerir(k.dilim(i, 1)):
don yanlis
don dogru
# ── Uçlar ───────────────────────────────────────────────────────────
hazirla(db)
arayuz = dosya.oku("arayuz.html")
eger arayuz == "":
yazdir("⚠ arayuz.html bulunamadı — programı web/pano klasöründen çalıştırın.")
fonksiyon ana_sayfa(_istek):
don web.html(arayuz)
fonksiyon listele(_istek):
don web.json(gorevler_json(db))
fonksiyon olustur(istek):
govde = istek.govde()
eger degil json.gecerli_mi(govde):
don hata_json("gövde geçerli JSON değil", 400)
baslik = json.al(govde, "baslik").kirp()
aciklama = json.al(govde, "aciklama").kirp()
sutun = json.al(govde, "sutun")
oncelik = json.al(govde, "oncelik")
eger baslik == "":
don hata_json("başlık boş olamaz", 422)
eger baslik.uzunluk() > 120:
don hata_json("başlık en çok 120 karakter", 422)
eger sutun == "":
sutun = "yapilacak"
eger oncelik == "":
oncelik = "orta"
eger degil SUTUNLAR.icerir(sutun):
don hata_json("bilinmeyen sütun: " + sutun, 422)
eger degil ONCELIKLER.icerir(oncelik):
don hata_json("bilinmeyen öncelik: " + oncelik, 422)
veritabani.sqlite(db, "INSERT INTO gorev(baslik, aciklama, sutun, oncelik, olusturma) VALUES (?, ?, ?, ?, ?)", [baslik, aciklama, sutun, oncelik, zaman.simdi()])
yeni = veritabani.sqlite(db, "SELECT max(id) FROM gorev")
don web.json(gorev_json(db, yeni), 201)
fonksiyon guncelle(istek):
kimlik = istek.parametre("id")
eger degil kimlik_gecerli(kimlik):
don hata_json("geçersiz kimlik", 400)
eger veritabani.sqlite(db, "SELECT count(*) FROM gorev WHERE id = ?", [kimlik]) == "0":
don hata_json("görev bulunamadı", 404)
govde = istek.govde()
eger degil json.gecerli_mi(govde):
don hata_json("gövde geçerli JSON değil", 400)
# Yalnız gönderilen alanlar güncellenir.
sutun = json.al(govde, "sutun")
eger sutun != "":
eger degil SUTUNLAR.icerir(sutun):
don hata_json("bilinmeyen sütun: " + sutun, 422)
veritabani.sqlite(db, "UPDATE gorev SET sutun = ? WHERE id = ?", [sutun, kimlik])
baslik = json.al(govde, "baslik").kirp()
eger baslik != "":
veritabani.sqlite(db, "UPDATE gorev SET baslik = ? WHERE id = ?", [baslik, kimlik])
oncelik = json.al(govde, "oncelik")
eger oncelik != "":
eger degil ONCELIKLER.icerir(oncelik):
don hata_json("bilinmeyen öncelik: " + oncelik, 422)
veritabani.sqlite(db, "UPDATE gorev SET oncelik = ? WHERE id = ?", [oncelik, kimlik])
don web.json(gorev_json(db, kimlik))
fonksiyon kaldir(istek):
kimlik = istek.parametre("id")
eger degil kimlik_gecerli(kimlik):
don hata_json("geçersiz kimlik", 400)
eger veritabani.sqlite(db, "SELECT count(*) FROM gorev WHERE id = ?", [kimlik]) == "0":
don hata_json("görev bulunamadı", 404)
veritabani.sqlite(db, "DELETE FROM gorev WHERE id = ?", [kimlik])
don web.yanit("", 204)
# Tek iş parçacığı: SQLite'a yazan işleyiciler sırayla koşar.
sunucu = web.sunucu(8471, "127.0.0.1", 1)
sunucu.hatalar_json()
sunucu.govde_siniri(16384)
sunucu.al("/", ana_sayfa)
sunucu.al("/api/gorevler", listele)
sunucu.gonder("/api/gorevler", olustur)
sunucu.guncelle("/api/gorevler/{id}", guncelle)
# DELETE yöntemi `sunucu.kaldir` ile bağlanır
sunucu.kaldir("/api/gorevler/{id}", kaldir)
yazdir("📋 Pano hazır — http://127.0.0.1:8471")
sunucu.basla()