Yazılım Bileşenleri ve SBOM Risk Takip Formu, kurumların kullandığı yazılımların bileşenlerini kayıt altına almak, güvenlik açıklarını (CVE) izlemek ve risklerini değerlendirmek amacıyla hazırlanmıştır.
CVSS, EPSS ve CISA KEV verileriyle risklerin önceliklendirilmesini, düzeltici faaliyetlerin takibini ve Excel/PDF üzerinden raporlanmasını destekler. Temel hedefi, yazılım tedarik zinciri güvenliğini güçlendirmek ve kurumsal siber risk yönetimini standartlaştırmaktır.
Bir Not defteri açarak, aşağıdaki (Mavi satırlar) html kodu içine kaydedin. Not defteri uzantısını .txt yerine ".html" olarak değiştirip kaydedin. Ör: "Yazılım Bileşenleri ve SBOM Risk Takip Formu.html" sayfayı kapatıp tekrar açtığınızda, web browser üzerinden sayfanız açılacaktır.
Sol tarafta görünen logo için, html dosyanız nerede ise, masaüstü veya oluşturulan bir klasör. Logonuzda aynı yerde olursa forma yansır. Logo ismi "logo.jpg" olmalı. Çift tıklayarak açtığınızda, browser üzerinde form açılışı gerçekleşir. Veya alanına 5 kez peş peşe tıklayarak çalışma esnasında logo değişimi de yapılabilir.
Başlık, başlığın altındaki, başlığın sağındaki ve alttaki yazıları, not defteri içinde "Ctrl + F" tuşları ile kelime yazarak bulabilir, ilgili metinleri kendinize göre uyarlayarak kaydederek kullanabilirsiniz. Başlık alanındaki "OLGUN BAŞKANLIĞI" alanına 5 kez peş peşe tıklayarak; Departman, Müdürlük veya Başkanlığınıza ait bilgiyi manuel girebilirsiniz.
Formun boş hali;
Formun dolu hali:
PDF / Yazdır
Dışarı Aktarılan Excel Verisi:
HTML Kod:
<!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light">
<title>Yazılım Bileşenleri ve SBOM Risk Takip Formu</title>
<style>
:root{--bg:#f8fafc;--paper:#fff;--blue:#0284c7;--ink:#172439;--muted:#64748b;--border:#dbe3eb;--soft:#f0f9ff}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.5 'Segoe UI',Arial,sans-serif}
button,input,select,textarea{font:inherit}
button{cursor:pointer}
.page{width:100%;max-width:none;margin:0;padding:12px 14px 15px;min-height:100vh}
.header{border:1px solid var(--border);border-radius:6px;background:#fff;display:flex;align-items:center;gap:13px;min-height:91px;padding:14px 17px;box-shadow:0 2px 12px #0f172a08}
.logo{width:68px;height:60px;object-fit:contain;flex:none}
.head-text{flex:1;min-width:0}.head-text h1{margin:0 0 4px;font-size:20px;line-height:1.2}.head-text p{margin:0;color:var(--muted);font-size:12px}
.agency{margin-left:auto;max-width:42%;flex:0 1 auto;text-align:right;font-size:12px;font-weight:700;color:#000;overflow-wrap:anywhere;outline:none}
.agency.editable{padding:4px;outline:2px solid #f59e0b;background:#fffbeb;border-radius:4px}
.tools{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin:12px 0 8px}
.btn{background:white;border:1px solid #ccd7e2;border-radius:6px;padding:8px 12px;min-height:34px;color:#334155;font-weight:600}
.btn:hover{background:#eff6ff}.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}.btn.primary:hover{background:#0369a1}
.btn.mini{padding:4px 9px;font-size:11px;min-height:28px}.btn.delete{color:#be123c}
.status{min-height:19px;margin-bottom:8px;font-size:11px;color:var(--muted)}.status.error{color:#b91c1c}.status.ok{color:#047857}
.form{border:1px solid var(--border);background:var(--paper);border-radius:6px;overflow:hidden;box-shadow:0 2px 12px #0f172a07}
.form-top{padding:10px 15px;border-left:4px solid var(--blue);background:#f8fbff;border-bottom:1px solid var(--border);font-weight:700;color:#075985;font-size:14px}
.form-body{padding:13px 16px 17px}
.subhead{display:flex;align-items:center;gap:8px;justify-content:space-between;border-bottom:1px solid #e2e8f0;margin:12px 0 12px;padding:0 0 7px}
.subhead:first-child{margin-top:0}.subhead h2{font-size:13px;color:#075985;margin:0;font-weight:700}
.fields{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:11px 14px}
.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-weight:600;font-size:11px;color:#334155}
.field :is(input,select,textarea){background:#fff;border:1px solid #cbd5e1;border-radius:5px;width:100%;min-width:0;min-height:34px;padding:7px 9px;color:#111827;outline:none}
.field :is(input,select,textarea):focus,.tbl :is(input,select):focus{border-color:var(--blue);box-shadow:0 0 0 2px #0284c71c}
.field textarea{resize:vertical;min-height:67px;line-height:1.5}.span2{grid-column:span 2}.span4{grid-column:1/-1}
.table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:5px}
.tbl{width:100%;border-collapse:collapse;table-layout:fixed;min-width:900px;font-size:11px}
.tbl th{color:#334155;background:#f1f5f9;text-align:left;font-weight:700;padding:7px;border-bottom:1px solid var(--border)}
.tbl td{padding:5px;border-bottom:1px solid #e8eef4;vertical-align:middle}.tbl tbody tr:last-child td{border-bottom:none}
.tbl :is(input,select){border:1px solid #d6e0ea;border-radius:4px;padding:6px;width:100%;min-width:0;min-height:31px;background:#fff}
.tbl td:last-child,.tbl th:last-child{width:44px;text-align:center}
.tbl .w-sm{width:76px}.tbl .w-md{width:110px}.tbl .w-lg{width:18%}
.tbl .risk-cell{font-weight:700;text-align:center;white-space:nowrap}.tbl .risk-high{color:#be123c}.tbl .risk-med{color:#b45309}.tbl .risk-low{color:#047857}
.empty{padding:16px;text-align:center;color:#94a3b8;font-size:11px}
.summary{background:#f8fafc;border:1px solid #e2e8f0;border-radius:5px;padding:9px 11px;margin:12px 0;font-size:11px;color:#475569}
.help{font-size:10.5px;color:#64748b;margin:7px 0 2px;line-height:1.5}
.print-only{display:none}
@media(max-width:950px){.fields{grid-template-columns:repeat(2,minmax(0,1fr))}.span4{grid-column:1/-1}}
@media(max-width:600px){.page{padding:8px}.header{gap:7px;padding:10px;align-items:flex-start}.logo{width:46px;height:44px}.head-text h1{font-size:15px}.head-text p{font-size:10px}.agency{font-size:10px;max-width:30%}.fields{grid-template-columns:1fr}.span2,.span4{grid-column:1}.form-body{padding:10px}.tools .btn{flex:1}}
/* OLGUN UI2 — dış çerçeve, başlık kartı ve sol hizalı buton paneli */
:root{--bg:#eaf1f7;--border:#cbd5e1;--panel:#ffffff;--panel-border:#cbd5e1}
body{background:#eaf1f7;background:linear-gradient(180deg,#eaf2f9 0%,#f1f5f9 60%,#eaf1f7 100%);min-height:100vh}
.page{max-width:none;padding:12px 14px 15px}
.app-shell{
border:1px solid #bed0e1;border-top:5px solid var(--blue);border-radius:8px;
padding:15px 16px 18px;background:#f7fafc;
box-shadow:0 9px 28px rgba(15,44,74,.095),0 1px 3px rgba(15,44,74,.05)
}
.header{
background:linear-gradient(110deg,#fff 0%,#f4f9ff 100%);
border:1px solid #ccd9e6;border-left:4px solid var(--blue);
border-radius:6px;padding:16px 18px;min-height:94px;
box-shadow:0 1px 5px rgba(15,23,42,.045)
}
.head-text h1{color:#142a42;letter-spacing:-.2px}
.agency{flex-shrink:1;max-width:44%;min-width:0;text-align:right;overflow-wrap:anywhere}
.toolbar-panel{
background:#fff;border:1px solid var(--panel-border);
border-radius:6px;padding:9px 12px 10px;margin:12px 0;
box-shadow:0 1px 4px rgba(15,23,42,.035)
}
.toolbar-label{font-size:10px;font-weight:700;color:#53718d;letter-spacing:.07em;margin:0 0 6px 1px}
.tools{display:flex;align-items:center;justify-content:flex-start;gap:8px;flex-wrap:wrap;margin:0}
.tools .btn{background:#fff;border-color:#bdd0e3;min-height:34px;box-shadow:0 1px 2px rgba(15,23,42,.025)}
.tools .btn:hover{background:#e0f2fe;border-color:#7dd3fc}
.tools .btn.primary{background:#0284c7;border-color:#0284c7;color:#fff}
.tools .btn.primary:hover{background:#0369a1}
.status{min-height:0;margin:6px 0 0 1px;color:#64748b;line-height:1.35}
.form{border-color:#cbd9e7;background:#fff;box-shadow:0 2px 8px rgba(15,23,42,.045)}
.form-top{border-left:4px solid #0284c7;background:#eaf5fd;border-bottom:1px solid #cbddea;padding:11px 15px}
.form-body{padding:14px 16px 17px}
.subhead{background:#f1f8fd;border:1px solid #e0ebf5;border-left:4px solid #0284c7;border-radius:4px;
padding:7px 10px;margin:15px 0 12px;min-height:33px}
.subhead:first-child{margin-top:0}
.subhead h2{font-size:12.5px;color:#075985}
.summary{background:#f2f8fd;border-color:#d7e4ee}
@media(max-width:950px){.page{padding:12px}.app-shell{padding:10px}}
@media(max-width:600px){
.page{padding:7px}.app-shell{padding:7px;border-radius:6px;border-top-width:4px}
.header{gap:8px;padding:10px;min-height:72px}.toolbar-panel{padding:8px;margin:9px 0}
.tools .btn{flex:1 1 160px}.agency{max-width:31%}.subhead{padding:7px 8px}
}
/* OLGUN UI2 — TAM EKRAN KURUMSAL YERLEŞİM
Ekran boyutuna göre tüm kullanılabilir yatay/dikey alanı doldurur. */
html{min-height:100%;}
body{min-height:100vh;}
.page{
width:100%;max-width:none;margin:0;
padding:12px 14px 15px;min-height:100vh;
}
.app-shell{
width:100%;min-height:calc(100vh - 27px);
display:flex;flex-direction:column;min-width:0;
}
.header,.toolbar-panel,.form{width:100%;min-width:0;}
.form{flex:1;}
.toolbar-panel{flex:none;}
/* Buton renkleri Olgun UI2 standardının kalıcı parçasıdır. */
.tools #excelImportBtn{
background:#dbeafe;border-color:#93c5fd;color:#1e40af;
}
.tools #excelImportBtn:hover,.tools #excelImportBtn:focus-visible{
background:#bfdbfe;border-color:#60a5fa;color:#1e3a8a;
}
.tools #excelExportBtn{
background:#dcfce7;border-color:#86efac;color:#166534;
}
.tools #excelExportBtn:hover,.tools #excelExportBtn:focus-visible{
background:#bbf7d0;border-color:#4ade80;color:#14532d;
}
.tools #printBtn{
background:#f1f5f9;border-color:#cbd5e1;color:#334155;
}
.tools #printBtn:hover,.tools #printBtn:focus-visible{
background:#e2e8f0;border-color:#94a3b8;color:#1e293b;
}
@media(max-width:950px){
.page{padding:9px;}
.app-shell{min-height:calc(100vh - 18px);}
}
@media(max-width:600px){
.page{padding:6px;}
.app-shell{min-height:calc(100vh - 12px);}
}
@page{size:A4 landscape;margin:10mm}
@media print{
html,body{background:#fff!important;color:#111;font-size:9pt}.page{padding:0;max-width:none}.noprint,.tools,.status{display:none!important}
.header,.form{box-shadow:none;border:1px solid #bdcbd8}.header{min-height:17mm;padding:2mm 3mm;margin-bottom:3mm}.logo{height:14mm;width:16mm}.head-text h1{font-size:15pt}.head-text p{font-size:8pt}.agency{font-size:9pt}
.form{overflow:visible}.form-top{padding:2mm 3mm;font-size:10pt;print-color-adjust:exact;-webkit-print-color-adjust:exact}.form-body{padding:2mm 3mm}
.subhead{margin:3mm 0 2mm;break-after:avoid}.subhead h2{font-size:9pt}.fields{grid-template-columns:repeat(4,minmax(0,1fr));gap:2mm}
.field{break-inside:avoid}.field label{font-size:7pt}.field :is(input,select,textarea){font-size:8pt;min-height:6mm;padding:1mm 2mm;border:1px solid #cbd5e1;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.field textarea{min-height:12mm;overflow:visible}.table-wrap{overflow:visible}.tbl{min-width:0;font-size:7pt;table-layout:fixed;break-inside:auto}.tbl thead{display:table-header-group}.tbl tr{break-inside:avoid}.tbl th,.tbl td{padding:1mm;border:1px solid #e2e8f0;overflow-wrap:anywhere}
.tbl :is(input,select){min-height:5mm;font-size:7pt;padding:.5mm;border:0;background:transparent;white-space:normal}.tbl td:last-child,.tbl th:last-child{display:none}
.summary{padding:1.5mm 2mm;font-size:8pt;break-inside:avoid}.help{display:none}.print-only{display:block}
}
/* Baskı ekran genişletme stilinden etkilenmesin; belge sayfaya otursun. */
@media print{
html,body{min-height:0!important;}
.page{width:100%;min-height:0!important;max-width:none!important;padding:0!important;}
.app-shell{width:100%;min-height:0!important;display:block!important;}
.header,.form{width:100%;}
.form{flex:none;}
}
/* Baskıda da Olgun UI2 kurum başlığı, ana çerçeve ve mavi bölüm şeritleri korunur. */
@media print{
body{background:#fff!important}
.page{max-width:none;margin:0;padding:0}
.app-shell{border:1px solid #bfcfdf;border-top:3px solid #0284c7;
border-radius:3px;background:#fff;padding:2mm 2.5mm;box-shadow:none}
.toolbar-panel{display:none!important}
.header{background:#f7fbff!important;border:1px solid #cbd5e1;border-left:3px solid #0284c7;
box-shadow:none;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.form{box-shadow:none;border:1px solid #bdcbd8}
.form-top{background:#eaf5fd!important;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.subhead{margin:2mm 0 1.8mm;background:#f1f8fd!important;border:1px solid #e0ebf5;
border-left:3px solid #0284c7;padding:1.4mm 2mm;min-height:0;
print-color-adjust:exact;-webkit-print-color-adjust:exact}
.subhead h2{font-size:9pt}
}
/* Olgun UI2: önbellekteki logo.jpg kontrolü, görsel yoksa LOGO alanı,
tıklamayla kullanıcı logosu seçimi, sağa sabit düzenlenebilir kurum adı. */
.brand-logo{
position:relative;display:flex;align-items:center;justify-content:center;
width:72px;height:66px;min-width:72px;flex:0 0 72px;
border:1px dashed #a9c5dc;border-radius:6px;background:#f5faff;
padding:3px;overflow:hidden;cursor:pointer;color:#55728a;box-shadow:none;
}
.brand-logo:hover,.brand-logo:focus-visible{border-color:#0284c7;background:#ecf7ff;outline:2px solid #0284c72b;outline-offset:1px}
.brand-logo .logo{display:none;width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;flex:none}
.brand-logo.has-logo .logo{display:block}
.brand-logo.has-logo .logo-fallback{display:none}
.logo-fallback{font-size:10px;letter-spacing:.15em;font-weight:700;color:#5f7791}
.logo-edit{position:absolute;bottom:0;right:0;background:#e0f2fe;color:#075985;font-size:10px;line-height:15px;padding:0 4px;border-top-left-radius:5px;opacity:0;transition:opacity .15s}
.brand-logo:hover .logo-edit,.brand-logo:focus-visible .logo-edit{opacity:1}
.header .head-text{flex:1 1 auto;min-width:0}
.header .agency{margin-left:auto;flex:0 1 auto;max-width:48%;min-width:0;align-self:center;text-align:right;overflow-wrap:anywhere;word-break:normal;white-space:normal;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:manipulation}
.header .agency:focus-visible{outline:2px solid #0284c7;outline-offset:3px;border-radius:3px}
.header .agency.editable{cursor:text;user-select:text;-webkit-user-select:text;outline:2px solid #f59e0b;outline-offset:2px;background:#fffbeb;padding:4px;min-width:120px;}
@media(max-width:600px){
.brand-logo{width:50px;min-width:50px;flex-basis:50px;height:50px;padding:2px}
.logo-fallback{font-size:8px}.header .agency{font-size:10px;max-width:37%}
}
@media print{
.brand-logo{width:16mm!important;min-width:16mm!important;flex-basis:16mm!important;height:14mm!important;padding:0;border:0;background:transparent!important;outline:none!important}
.brand-logo .logo{width:100%;height:100%;object-fit:contain}
.brand-logo .logo-edit{display:none!important}
.logo-fallback{font-size:8pt}
.agency.editable{outline:none!important;background:transparent!important}
}
/* SBOM_UI2_PRINT_PAGE_NUMBERS_V1 - Olgun UI2 ekranda tek form, yazdırmada gerçek sayfa numaraları. */
.form-page-footer{
display:flex;justify-content:flex-end;align-items:center;min-height:24px;
margin-top:7px;padding:0 3px;color:#64748b;font-size:10.5px;
text-align:right;font-weight:600;letter-spacing:.01em;
}
/* Sayfa numaraları yalnızca basılı sayfa altında üretilir; ekrandaki tek form sayacı tekrarlanmaz. */
@page{
size:A4 landscape;
margin:8mm 10mm 11mm 10mm;
@bottom-right{
content:"Sayfa " counter(page) " / " counter(pages);
font-family:"Segoe UI",Arial,sans-serif;
font-size:8.5pt;font-weight:600;color:#475569;
text-align:right;
vertical-align:middle;
}
}
@media print{
html,body{height:auto!important;min-height:0!important;overflow:visible!important}
.page{margin:0!important;padding:0!important;height:auto!important;min-height:0!important;break-before:auto;break-after:auto}
.app-shell{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important;break-before:auto;break-after:auto;
margin:0!important;padding:2mm 2.5mm 1mm!important}
.form{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important;break-after:auto}
.form-body{padding-bottom:1mm!important}
.form-page-footer{display:none!important}
.subhead{break-inside:avoid;break-after:avoid-page;page-break-after:avoid}
.fields{break-before:auto;break-after:auto;}
.tbl{break-before:auto;break-after:auto;}
.tbl tr{break-inside:avoid-page;page-break-inside:avoid}
.field{break-inside:avoid-page;page-break-inside:avoid}
.toolbar-panel,.help,.noprint{display:none!important}
.form-top,.subhead,.summary,.tbl th{print-color-adjust:exact;-webkit-print-color-adjust:exact}
}
/* OLGUN_UI2_AGENCY_TEXT_SIZE_V2 — Kurum adı ana başlıkla uyumlu,
sağ kenarı sabit; uzun kurum adları sola doğru yayılır ve satıra kırılır. */
.header .agency{
font-size:19px;
line-height:1.22;
font-weight:700;
letter-spacing:-.2px;
color:#000;
max-width:48%;
min-width:0;
margin-left:auto;
text-align:right;
overflow-wrap:anywhere;
white-space:normal;
}
@media (min-width:601px) and (max-width:950px){
.header .agency{font-size:17px;}
}
@media (max-width:600px){
.header .agency{font-size:13px;max-width:40%;line-height:1.2;}
}
@media print{
.header .agency{font-size:14pt!important;line-height:1.18!important;max-width:48%;font-weight:700!important;}
}
</style>
</head>
<body>
<div class="page">
<div class="app-shell">
<header class="header">
<button type="button" id="logoArea" class="brand-logo noprint-control" title="Logoyu değiştirmek için tıklayın" aria-label="Kurum logosu seç veya değiştir">
<img id="logoImage" src="logo.jpg" class="logo" alt="Kurum logosu">
<span class="logo-fallback" id="logoFallback" aria-hidden="true">LOGO</span>
<span class="logo-edit" aria-hidden="true">✎</span>
</button>
<div class="head-text"><h1>Yazılım Bileşenleri ve SBOM Risk Takip Formu</h1><p>(Yazılım bileşenleri, güvenlik açıkları ve düzeltici faaliyetlerin izlenmesi)</p></div>
<div id="agency" class="agency" title="Kurum adını düzenlemek için 5 kez art arda tıklayın veya dokunun" role="button" aria-label="Kurum adını değiştirmek için beş kez tıklayın" tabindex="0">OLGUN BAŞKANLIĞI</div>
</header>
<section class="toolbar-panel noprint" aria-label="Form işlemleri">
<div class="toolbar-label">FORM İŞLEMLERİ</div>
<div class="tools">
<button type="button" class="btn" id="excelImportBtn">↑ Excel İçeri Aktar</button>
<button type="button" class="btn" id="excelExportBtn">↓ Excel Dışarı Aktar</button>
<button type="button" class="btn" id="printBtn">▣ PDF / Yazdır</button>
</div>
<div id="status" class="status noprint" role="status" aria-live="polite"></div>
</section>
<main class="form" id="mainForm">
<div class="form-top">Yazılım Bileşenleri ve SBOM Risk Değerlendirme Formu</div>
<div class="form-body">
<div class="subhead"><h2>1 · Genel Bilgiler</h2></div>
<div class="fields" id="generalFields"></div>
<div class="subhead"><h2>2 · SBOM Bilgileri</h2></div>
<div class="fields" id="sbomFields"></div>
<div class="subhead"><h2>3 · Yazılım Bileşenleri</h2><button type="button" id="addComponent" class="btn mini noprint">+ Bileşen Ekle</button></div>
<div class="table-wrap" id="componentsTable"></div>
<div class="subhead"><h2>4 · Zafiyet ve Risk Takibi</h2><button type="button" id="addVulnerability" class="btn mini noprint">+ Zafiyet Ekle</button></div>
<div class="table-wrap" id="vulnerabilitiesTable"></div>
<div id="summary" class="summary" aria-live="polite">Bileşen: 0 · Zafiyet: 0 · Yüksek öncelik: 0</div>
<div class="subhead"><h2>5 · Aksiyon ve Değerlendirme</h2></div>
<div class="fields" id="actionFields"></div>
<div class="help">Not: SBOM ve zafiyet bilgileri form üzerinden veya Excel dosyasından girilir. CVE, EPSS ve KEV değerleri internetten otomatik sorgulanmaz. Risk sınıflandırması ön değerlendirme amaçlıdır.</div>
</div>
</main>
<footer class="form-page-footer" aria-label="Form sayfa numarası"><span>Sayfa 1 / 1</span></footer>
</div>
</div>
<input type="file" id="excelFile" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" hidden>
<input type="file" id="logoFile" accept="image/png,image/jpeg,image/webp,image/gif,.png,.jpg,.jpeg,.webp,.gif" hidden>
<script>
'use strict';
(()=>{
const $=id=>document.getElementById(id);
const esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const s=x=>x==null?'':String(x);
const dateToday=()=>{let t=new Date();return [t.getFullYear(),String(t.getMonth()+1).padStart(2,'0'),String(t.getDate()).padStart(2,'0')].join('-')};
const mkid=()=>globalThis.crypto?.randomUUID?.()||'b'+Date.now().toString(36)+Math.random().toString(36).slice(2);
const F=(key,label,type='text',options=[],span='')=>({key,label,type,options,span});
const specs={
general:[F('recordNo','Kayıt No'),F('date','Kayıt Tarihi','date'),F('appName','Yazılım / Uygulama Adı'),F('version','Uygulama Sürümü'),F('department','Sorumlu Birim'),F('owner','Teknik Sorumlu'),F('criticality','Varlık Kritiklik','select',['Kritik','Yüksek','Orta','Düşük','Bilinmiyor']),F('environment','Ortam','select',['Üretim','Test','Geliştirme','Diğer']),F('exposure','İnternet Erişimi','select',['Açık','Kısıtlı','Kapalı','Bilinmiyor'])],
sbom:[F('format','SBOM Formatı','select',['CycloneDX','SPDX','Diğer']),F('sbomDate','SBOM Tarihi','date'),F('author','Üreten / Sağlayan'),F('filename','SBOM Dosyası'),F('coverage','Kapsam','select',['Tam','Kısmi','Bilinmiyor'])],
action:[F('riskOwner','Risk / Aksiyon Sorumlusu'),F('actionStatus','Aksiyon Durumu','select',['Planlandı','Devam Ediyor','Tamamlandı','Risk Kabul Edildi']),F('dueDate','Hedef Tarih','date'),F('reviewDate','Kontrol Tarihi','date'),F('reviewer','İnceleyen / Onaylayan'),F('decision','Değerlendirme Sonucu','select',['Bekliyor','Uygun','Şartlı Uygun','Uygun Değil']),F('actionPlan','Düzeltici Faaliyet / Alınacak Önlem','textarea',[],'span4'),F('notes','Değerlendirme / Kapanış Notu','textarea',[],'span4')],
components:[F('name','Bileşen'),F('version','Sürüm'),F('purl','PURL / Paket Kimliği'),F('license','Lisans'),F('licenseStatus','Lisans Durumu','select',['Uygun','İnceleme Gerekli','Uygun Değil','Bilinmiyor']),F('support','Destek','select',['Destekleniyor','Destek Sonu','Bilinmiyor'])],
vulnerabilities:[F('cve','CVE / Bulgu'),F('component','Bileşen'),F('cvss','CVSS','number'),F('epss','EPSS (0–1)','number'),F('kev','KEV','select',['Evet','Hayır','Bilinmiyor']),F('vex','Etkilenme','select',['Etkileniyor','Etkilenmiyor','İnceleniyor','Giderildi','Bilinmiyor']),F('status','Durum','select',['Açık','İnceleniyor','Giderildi','Risk Kabul Edildi'])]
};
const blank=()=>({general:{recordNo:'',date:dateToday()},sbom:{},action:{},components:[],vulnerabilities:[]});
let data=blank();
function status(msg,kind=''){$('status').textContent=msg;$('status').className='status noprint '+kind}
function fieldHTML(f,value,prefix){let id=prefix+'-'+f.key,attrs='id="'+id+'" data-field="'+esc(f.key)+'"';let control=f.type==='select'?'<select '+attrs+'><option value="">Seçiniz</option>'+f.options.map(o=>'<option value="'+esc(o)+'"'+(o===value?' selected':'')+'>'+esc(o)+'</option>').join('')+'</select>':f.type==='textarea'?'<textarea '+attrs+' rows="2">'+esc(value)+'</textarea>':'<input '+attrs+' type="'+f.type+'" '+(f.type==='number'?'min="0" max="10" step="0.1" ':'')+'value="'+esc(value)+'">';return '<div class="field '+f.span+'"><label for="'+id+'">'+esc(f.label)+'</label>'+control+'</div>'}
function renderFields(group){let el=$(group+'Fields');el.innerHTML=specs[group].map(f=>fieldHTML(f,data[group][f.key]??'',group)).join('')}
for(const group of ['general','sbom','action']){$(group+'Fields').addEventListener('input',e=>{let k=e.target.dataset.field;if(!k)return;data[group][k]=e.target.value;if(group==='general'&&k==='criticality')refreshRisk()});$(group+'Fields').addEventListener('change',e=>{let k=e.target.dataset.field;if(!k)return;data[group][k]=e.target.value;if(group==='general')refreshRisk()})}
const cols={components:[17,10,25,12,17,15],vulnerabilities:[14,20,9,10,9,16,15]};
function cellInput(type,row,f){let id=row.id,attr='data-col="'+esc(f.key)+'" data-id="'+esc(id)+'" aria-label="'+esc(f.label)+'"',v=s(row[f.key]);if(f.type==='select')return '<select '+attr+'><option value="">Seçiniz</option>'+f.options.map(o=>'<option'+(o===v?' selected':'')+'>'+esc(o)+'</option>').join('')+'</select>';return '<input '+attr+' type="'+f.type+'" value="'+esc(v)+'" '+(f.type==='number'?'min="0" max="'+(f.key==='epss'?'1':'10')+'" step="0.01" ':'')+'>'}
function renderTable(type){let list=data[type],fields=specs[type];let colsHTML=fields.map((f,i)=>'<col style="width:'+cols[type][i]+'%">').join('')+(type==='vulnerabilities'?'<col style="width:11%">':'')+'<col style="width:44px">';let th=fields.map(f=>'<th>'+esc(f.label)+'</th>').join('')+(type==='vulnerabilities'?'<th>Öncelik</th>':'')+'<th class="noprint">Sil</th>';
let rows=list.map(row=>'<tr>'+fields.map(f=>'<td>'+cellInput(type,row,f)+'</td>').join('')+(type==='vulnerabilities'?'<td class="risk-cell" data-risk="'+esc(row.id)+'"></td>':'')+'<td><button type="button" class="btn mini delete noprint" data-delete="'+esc(row.id)+'" aria-label="Satırı sil">−</button></td></tr>').join('');
$(type+'Table').innerHTML=list.length?'<table class="tbl"><colgroup>'+colsHTML+'</colgroup><thead><tr>'+th+'</tr></thead><tbody>'+rows+'</tbody></table>':'<div class="empty">Henüz kayıt eklenmedi.</div>';refreshRisk()}
for(const type of ['components','vulnerabilities']){$(type+'Table').addEventListener('input',e=>{const col=e.target.dataset.col,id=e.target.dataset.id;if(!col||!id)return;const row=data[type].find(x=>x.id===id);if(!row)return;row[col]=e.target.value;refreshRisk()});$(type+'Table').addEventListener('change',e=>{const col=e.target.dataset.col,id=e.target.dataset.id;if(!col||!id)return;const row=data[type].find(x=>x.id===id);if(!row)return;row[col]=e.target.value;refreshRisk()});$(type+'Table').addEventListener('click',e=>{const btn=e.target.closest('[data-delete]');if(!btn)return;data[type]=data[type].filter(x=>x.id!==btn.dataset.delete);renderTable(type)})}
$('addComponent').addEventListener('click',()=>{data.components.push({id:mkid(),licenseStatus:'Bilinmiyor',support:'Bilinmiyor'});renderTable('components');$('componentsTable tbody tr:last-child input')?.focus()});
$('addVulnerability').addEventListener('click',()=>{data.vulnerabilities.push({id:mkid(),kev:'Bilinmiyor',vex:'Bilinmiyor',status:'Açık'});renderTable('vulnerabilities');$('vulnerabilitiesTable tbody tr:last-child input')?.focus()});
function risk(v){if(v.status==='Giderildi'||v.vex==='Giderildi')return 'Giderildi';if(v.vex==='Etkilenmiyor')return 'Etkilenmiyor';let cv=Number(v.cvss),ep=Number(v.epss),hasCV=s(v.cvss).trim()!==''&&Number.isFinite(cv),hasEP=s(v.epss).trim()!==''&&Number.isFinite(ep);if(!hasCV&&!hasEP&&v.kev!=='Evet')return 'Belirsiz';if(v.kev==='Evet'&&v.vex==='Etkileniyor')return 'P1';if(hasCV&&cv>=9&&['Kritik','Yüksek'].includes(data.general.criticality))return 'P1';if(v.kev==='Evet'||(hasCV&&cv>=7)||(hasEP&&ep>=.5))return 'P2';if((hasCV&&cv>=4)||(hasEP&&ep>=.1))return 'P3';return 'P4'}
function refreshRisk(){for(const row of data.vulnerabilities){let el=document.querySelector('[data-risk="'+CSS.escape(row.id)+'"]');if(el){let r=risk(row);el.textContent=r;el.className='risk-cell '+(r==='P1'?'risk-high':r==='P2'||r==='P3'?'risk-med':r==='P4'||r==='Giderildi'||r==='Etkilenmiyor'?'risk-low':'')}}let high=data.vulnerabilities.filter(v=>['P1','P2'].includes(risk(v))).length,kev=data.vulnerabilities.filter(v=>v.kev==='Evet').length,licenses=data.components.filter(v=>v.licenseStatus==='Uygun Değil').length;
$('summary').textContent='Toplam Bileşen: '+data.components.length+' | Zafiyet Kaydı: '+data.vulnerabilities.length+' | P1/P2: '+high+' | KEV: '+kev+' | Lisans Uyumsuzluğu: '+licenses;}
function renderAll(){['general','sbom','action'].forEach(renderFields);['components','vulnerabilities'].forEach(renderTable);refreshRisk()}
function saveBlob(blob,name){let url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2500)}
const filename=t=>(s(t||'SBOM_Risk_Takip').replace(/[^A-Za-z0-9_-]/g,'_').slice(0,55)||'SBOM_Risk_Takip');
// Excel .xlsx: tamamen tarayıcıda, CDN olmadan ZIP/XML okuma-yazma.
const utf=new TextEncoder(),utfd=new TextDecoder();const ub=s=>utf.encode(s);
const join=(arrays)=>{let n=arrays.reduce((a,b)=>a+b.length,0),out=new Uint8Array(n),p=0;for(let a of arrays){out.set(a,p);p+=a.length}return out};
const u16=(a,i,v)=>{a[i]=v&255;a[i+1]=v>>>8&255},u32=(a,i,v)=>{u16(a,i,v);u16(a,i+2,v>>>16)};const g16=(a,i)=>a[i]|a[i+1]<<8;const g32=(a,i)=>(a[i]|a[i+1]<<8|a[i+2]<<16|a[i+3]<<24)>>>0;
const crct=(()=>{let t=[];for(let i=0;i<256;i++){let c=i;for(let j=0;j<8;j++)c=c&1?0xedb88320^(c>>>1):c>>>1;t.push(c>>>0)}return t})();
const crc=b=>{let c=0xffffffff;for(let x of b)c=crct[(c^x)&255]^(c>>>8);return(c^0xffffffff)>>>0};
function zip(files){let local=[],central=[],offset=0;for(let [name,body] of Object.entries(files)){let n=ub(name),b=ub(body),check=crc(b),l=new Uint8Array(30+n.length),c=new Uint8Array(46+n.length);u32(l,0,0x04034b50);u16(l,4,20);u16(l,8,0);u32(l,14,check);u32(l,18,b.length);u32(l,22,b.length);u16(l,26,n.length);l.set(n,30);u32(c,0,0x02014b50);u16(c,4,20);u16(c,6,20);u32(c,16,check);u32(c,20,b.length);u32(c,24,b.length);u16(c,28,n.length);u32(c,42,offset);c.set(n,46);local.push(l,b);central.push(c);offset+=l.length+b.length}let ct=join(central),end=new Uint8Array(22);u32(end,0,0x06054b50);u16(end,8,central.length);u16(end,10,central.length);u32(end,12,ct.length);u32(end,16,offset);return new Blob([...local,ct,end],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})}
async function unzip(file){let b=new Uint8Array(await file.arrayBuffer()),end=-1;for(let i=b.length-22;i>=Math.max(0,b.length-65557);i--)if(g32(b,i)===0x06054b50){end=i;break}if(end<0)throw Error('Geçerli XLSX ZIP dosyası değil.');let count=g16(b,end+10),p=g32(b,end+16),out={};if(count>2000)throw Error('XLSX çok fazla dosya içeriyor.');for(let i=0;i<count;i++){if(g32(b,p)!==0x02014b50)throw Error('Bozuk ZIP dizini.');let method=g16(b,p+10),len=g32(b,p+20),nameLen=g16(b,p+28),extra=g16(b,p+30),comment=g16(b,p+32),offset=g32(b,p+42),name=utfd.decode(b.slice(p+46,p+46+nameLen));p+=46+nameLen+extra+comment;if(!/^(xl\/|\[Content_Types\]|_rels\/)/.test(name)||len>35*1024*1024)continue;let start=offset+30+g16(b,offset+26)+g16(b,offset+28),compressed=b.slice(start,start+len),raw;if(method===0)raw=compressed;else if(method===8){if(!('DecompressionStream' in window))throw Error('Bu tarayıcı XLSX sıkıştırma açmayı desteklemiyor.');let stream=new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'));raw=new Uint8Array(await new Response(stream).arrayBuffer())}else throw Error('Desteklenmeyen XLSX ZIP sıkıştırması.');if(raw.length>75*1024*1024)throw Error('Açılan XLSX içeriği çok büyük.');out[name]=utfd.decode(raw)}return out}
const xmlEsc=s=>String(s??'').replace(/[&<>"']/g,x=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[x]));
const sheet=(title,fields,rows)=>({title,heads:fields.map(f=>f.label),keys:fields.map(f=>f.key),rows});
function excelSheets(){return [sheet('Genel',specs.general,[data.general]),sheet('SBOM',specs.sbom,[data.sbom]),sheet('Degerlendirme',specs.action,[data.action]),sheet('Bilesenler',specs.components,data.components),sheet('Zafiyetler',specs.vulnerabilities,data.vulnerabilities)]}
// Alanlar ve örnek kayıtlar Excel'de dikey satırlarda gösterilir.
function verticalSheets(){
const simple=(title,fields,record)=>({title,rows:[['Alan','Değer'],...fields.map(f=>[f.label,s(record[f.key])])]});
const many=(title,fields,records,prefix)=>({title,rows:[['Kayıt No','Alan','Değer'],...records.flatMap((record,i)=>fields.map(f=>[prefix+'-'+String(i+1).padStart(2,'0'),f.label,s(record[f.key])]))]});
return [simple('Genel',specs.general,data.general),simple('SBOM',specs.sbom,data.sbom),simple('Degerlendirme',specs.action,data.action),many('Bilesenler',specs.components,data.components,'B'),many('Zafiyetler',specs.vulnerabilities,data.vulnerabilities,'Z')];
}
function excelFile(){let sheets=verticalSheets(),files={},contentTypes=['<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'];let ws=[];for(let i=0;i<sheets.length;i++){let sh=sheets[i],rows=sh.rows,all=rows.map((row,ri)=>'<row r="'+(ri+1)+'">'+row.map((v,ci)=>{let col='',k=ci+1;while(k){k--;col=String.fromCharCode(65+k%26)+col;k=Math.floor(k/26)}return '<c r="'+col+(ri+1)+'" t="inlineStr"><is><t xml:space="preserve">'+xmlEsc(v)+'</t></is></c>'}).join('')+'</row>').join('');files['xl/worksheets/sheet'+(i+1)+'.xml']='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>'+all+'</sheetData></worksheet>';contentTypes.push('<Override PartName="/xl/worksheets/sheet'+(i+1)+'.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>');ws.push('<sheet name="'+xmlEsc(sh.title)+'" sheetId="'+(i+1)+'" r:id="rId'+(i+1)+'"/>')}
files['xl/workbook.xml']='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>'+ws.join('')+'</sheets></workbook>';
files['xl/_rels/workbook.xml.rels']='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'+sheets.map((_,i)=>'<Relationship Id="rId'+(i+1)+'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet'+(i+1)+'.xml"/>').join('')+'</Relationships>';
files['_rels/.rels']='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>';
files['[Content_Types].xml']=contentTypes.join('')+'</Types>';return zip(files)}
function parseXML(s){let d=new DOMParser().parseFromString(s,'application/xml');if(d.getElementsByTagName('parsererror').length)throw Error('XLSX XML okunamadı.');return d}
function excelReadSheet(xml,shared){let root=parseXML(xml),out=[];for(let r of root.getElementsByTagNameNS('*','row')){let cells={};for(let c of r.getElementsByTagNameNS('*','c')){let ref=c.getAttribute('r')||'',col=(ref.match(/^[A-Z]+/)||['A'])[0].split('').reduce((a,ch)=>a*26+ch.charCodeAt(0)-64,0)-1;let val='';if(c.getAttribute('t')==='inlineStr'){val=Array.from(c.getElementsByTagNameNS('*','t')).map(x=>x.textContent).join('')}else{val=c.getElementsByTagNameNS('*','v')[0]?.textContent||'';if(c.getAttribute('t')==='s')val=shared[Number(val)]??''}cells[col]=val}out.push(cells)}if(!out.length)return[];let names=out[0];return out.slice(1).map(row=>Object.keys(names).map(i=>row[i]??''))}
async function importExcel(file){
if(file.size>35*1024*1024)throw Error('Excel dosyası 35 MB sınırını aşıyor.');
const files=await unzip(file),work=parseXML(files['xl/workbook.xml']||''),rels=parseXML(files['xl/_rels/workbook.xml.rels']||''),relsMap={};
for(let r of rels.getElementsByTagNameNS('*','Relationship'))relsMap[r.getAttribute('Id')]=r.getAttribute('Target');
let shared=[];if(files['xl/sharedStrings.xml']){const doc=parseXML(files['xl/sharedStrings.xml']);shared=Array.from(doc.getElementsByTagNameNS('*','si')).map(si=>Array.from(si.getElementsByTagNameNS('*','t')).map(x=>x.textContent).join(''))}
const named={};for(let sh of work.getElementsByTagNameNS('*','sheet')){let name=sh.getAttribute('name'),rid=sh.getAttribute('r:id')||sh.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships','id'),target=relsMap[rid];if(!target)continue;let path=target.startsWith('/')?target.slice(1):'xl/'+target.replace(/^\.\.\//,'');if(files[path])named[name]=excelReadSheet(files[path],shared)}
const next=blank(),mapping={Genel:'general',SBOM:'sbom',Degerlendirme:'action',Bilesenler:'components',Zafiyetler:'vulnerabilities'};
let matched=0;
for(const sh of excelSheets()){
const rows=named[sh.title];if(!rows)continue;matched++;
const key=mapping[sh.title],fields=specs[key],byLabel=new Map(fields.map(f=>[f.label,f.key]));
if(key==='components'||key==='vulnerabilities'){
// Yeni dikey biçim: B-01 | Bileşen | Örnek Bileşen
const vertical=rows.some(r=>byLabel.has(s(r[1])));
if(vertical){const grouping=new Map();for(const r of rows){const id=s(r[0]).trim(),field=byLabel.get(s(r[1]));if(!id||!field)continue;if(!grouping.has(id))grouping.set(id,{});grouping.get(id)[field]=s(r[2])}
next[key]=[...grouping.values()].filter(row=>Object.values(row).some(v=>v.trim())).map(row=>({...row,id:mkid()}));
}else{
const arr=rows.map(r=>Object.fromEntries(sh.keys.map((col,i)=>[col,s(r[i])]))).filter(row=>Object.values(row).some(v=>v.trim()));next[key]=arr.map(row=>({...row,id:mkid()}));
}
}else{
// Yeni dikey biçim: Alan | Değer. Eski yatay aktarım da okunabilir.
const vertical=rows.some(r=>byLabel.has(s(r[0])));
if(vertical){for(const r of rows){const field=byLabel.get(s(r[0]));if(field)next[key][field]=s(r[1])}}
else{const arr=rows.map(r=>Object.fromEntries(sh.keys.map((col,i)=>[col,s(r[i])]))).filter(row=>Object.values(row).some(v=>v.trim()));if(arr.length)Object.assign(next[key],arr[0])}
}
}
if(!matched)throw Error('Beklenen sayfalar yok. Bu formun Excel şablonunu kullanın.');return next;
}
$('excelExportBtn').onclick=()=>{try{saveBlob(excelFile(),filename(data.general.appName)+'_SBOM_Risk.xlsx');status('Excel raporu oluşturuldu.','ok')}catch(e){status('Excel dışarı aktarılamadı: '+e.message,'error')}};
$('excelImportBtn').onclick=()=>$('excelFile').click();
$('excelFile').onchange=async e=>{const file=e.target.files[0];e.target.value='';if(!file)return;try{const next=await importExcel(file);if(!confirm('Excel içindeki kayıtlar mevcut form verilerinin yerine yüklensin mi?'))return;data=next;renderAll();status('Excel içeri aktarıldı.','ok')}catch(ex){status('Excel içeri aktarılamadı: '+ex.message,'error')}};
$('printBtn').onclick=()=>window.print();
// Kalıcı Olgun UI2 başlığı: hem logo hem kurum adı (tarayıcı depolama izin veriyorsa).
const nameStorageKey='olgun-ui2:kurum-adi';
const logoStorageKey='olgun-ui2:kurum-logo';
const tryGet=(key)=>{try{return localStorage.getItem(key)}catch{return null}};
const trySet=(key,value)=>{try{localStorage.setItem(key,value);return true}catch{return false}};
const logoArea=$('logoArea'),logoImage=$('logoImage'),logoFile=$('logoFile');
function syncLogo(){
// cache'den senkron yüklenmiş resimler için de naturalWidth zorunlu.
logoArea.classList.toggle('has-logo',Boolean(logoImage.complete&&logoImage.naturalWidth>0));
}
logoImage.addEventListener('load',syncLogo);
logoImage.addEventListener('error',syncLogo);
const savedLogo=tryGet(logoStorageKey);
if(savedLogo&&/^data:image\/(png|jpeg|webp|gif);base64,/i.test(savedLogo))logoImage.src=savedLogo;
syncLogo();
logoArea.addEventListener('click',()=>logoFile.click());
logoFile.addEventListener('change',()=>{
const file=logoFile.files?.[0];logoFile.value='';if(!file)return;
const extensionOk=/\.(png|jpe?g|webp|gif)$/i.test(file.name);
if(!/^image\/(png|jpeg|webp|gif)$/i.test(file.type)&&!extensionOk){status('PNG, JPG, WebP veya GIF seçin.','error');return}
if(file.size>10*1024*1024){status('Logo dosyası 10 MB sınırını aşıyor.','error');return}
const reader=new FileReader();
reader.onerror=()=>status('Logo dosyası okunamadı.','error');
reader.onload=()=>{
const src=String(reader.result||'');
const validator=new Image();
validator.onerror=()=>status('Seçilen dosya geçerli bir resim değil.','error');
validator.onload=()=>{
// Büyük logoları küçült: localStorage kotasını ve PDF baskı yükünü sınırlar.
let result=src;
if(file.type!=='image/gif'){
try{
const max=500,scale=Math.min(1,max/Math.max(validator.naturalWidth,validator.naturalHeight));
const canvas=document.createElement('canvas');
canvas.width=Math.max(1,Math.round(validator.naturalWidth*scale));
canvas.height=Math.max(1,Math.round(validator.naturalHeight*scale));
canvas.getContext('2d').drawImage(validator,0,0,canvas.width,canvas.height);
result=canvas.toDataURL(file.type==='image/jpeg'?'image/jpeg':'image/png',.88);
}catch{result=src}
}
logoImage.src=result;
syncLogo();
const stored=trySet(logoStorageKey,result);
status(stored?'Logo değiştirildi ve tarayıcıda saklandı.':'Logo değiştirildi; tarayıcı kalıcı kaydetmeye izin vermedi.','ok');
};
validator.src=src;
};
reader.readAsDataURL(file);
});
const agency=$('agency');
const savedName=tryGet(nameStorageKey);
if(savedName&&savedName.trim())agency.textContent=savedName.trim().slice(0,140);
let clickCount=0,lastClick=0,originalName='';
const resetCounter=()=>{clickCount=0;lastClick=0;agency.title='Kurum adını düzenlemek için 5 kez art arda tıklayın veya dokunun'};
function enableAgencyEdit(){
originalName=agency.textContent;
resetCounter();agency.contentEditable='true';agency.classList.add('editable');
agency.setAttribute('role','textbox');agency.setAttribute('aria-label','Kurum adını düzenleyin');
agency.focus();
const range=document.createRange();range.selectNodeContents(agency);
const selection=window.getSelection();selection.removeAllRanges();selection.addRange(range);
}
function countActivation(){
if(agency.isContentEditable)return;
const now=Date.now();
// Normal, çift tık ve mobil dokunmaları say; yavaş tıklayanlar için 4 sn tolerans.
clickCount=now-lastClick<4000?clickCount+1:1;
lastClick=now;
if(clickCount>=5){enableAgencyEdit();return}
agency.title='Düzenlemek için '+(5-clickCount)+' kez daha tıklayın';
}
agency.addEventListener('click',countActivation);
agency.addEventListener('keydown',e=>{
if(agency.isContentEditable){
if(e.key==='Enter'){e.preventDefault();agency.blur()}
else if(e.key==='Escape'){e.preventDefault();agency.textContent=originalName;agency.blur()}
}else if(e.key==='Enter'||e.key===' '){e.preventDefault();countActivation()}
});
agency.addEventListener('blur',()=>{
if(!agency.isContentEditable){resetCounter();return}
agency.textContent=agency.textContent.replace(/\s+/g,' ').trim().slice(0,140)||'OLGUN BAŞKANLIĞI';
agency.contentEditable='false';agency.classList.remove('editable');
agency.setAttribute('role','button');agency.setAttribute('aria-label','Kurum adını değiştirmek için beş kez tıklayın');
const saved=trySet(nameStorageKey,agency.textContent);
if(!saved)status('Kurum adı değiştirildi ancak tarayıcı kalıcı kaydetmeye izin vermedi.','error');
resetCounter();
});
document.addEventListener('click',e=>{if(!agency.contains(e.target)&&!agency.isContentEditable)resetCounter()});
renderAll();status('Boş form hazır. Alanları doldurabilir veya Excel dosyası içeri alabilirsiniz.');
})();
</script>
</body>
</html>












