Kurumların dijital sertifika, kriptografik anahtar ve makine kimliklerini merkezi olarak izlemek ve yönetmek amacıyla geliştirilmiştir. Geçerlilik sürelerini otomatik hesaplayarak yaklaşan yenilemeleri ve kritik durumları belirler; hizmet kesintilerini ve güvenlik risklerini önlemeyi, operasyonel sürekliliği ve denetlenebilirliği güçlendirmeyi hedefler.
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: "Makine Kimliği, Sertifika ve Anahtar Süre Takip Aracı.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">
<title>Makine Kimliği, Sertifika ve Anahtar Süre Takip Aracı</title>
<style>
/* Olgun UI2 — Ekteki SBOM formunun başlık yapısıyla; 11pt okunabilirlik */
:root{--bg:#f8fafc;--paper:#fff;--ink:#172439;--muted:#64748b;--line:#cbd5e1;--blue:#0284c7;--shade:#edf5fb;--base:11pt}
*{box-sizing:border-box}
html,body{width:100%;min-height:100%;margin:0}
body{background:var(--bg);color:var(--ink);font:var(--base)/1.26 'Segoe UI',Arial,sans-serif}
input,select,textarea,button{font:inherit}
button{cursor:pointer}button:disabled{opacity:.5;cursor:not-allowed}
.app{width:100%;min-height:100vh;display:flex;flex-direction:column;gap:7px;padding:8px 11px 7px;background:var(--bg);border:2px solid #b5c2ce;box-shadow:inset 0 0 0 3px #eef2f7}
.header{display:flex;align-items:center;gap:13px;min-height:79px;padding:9px 15px;border:1px solid var(--line);border-radius:6px;background:#fff;box-shadow:0 2px 10px #0f172a08}
.logo{display:flex;position:relative;align-items:center;justify-content:center;flex:0 0 68px;width:68px;height:60px;padding:3px;background:#fff;color:#64748b;border:1px dashed #afc2d4;border-radius:5px;overflow:hidden}
.logo img{display:none;max-width:100%;max-height:100%;width:100%;height:100%;object-fit:contain}
.logo img.is-ready{display:block}.logo-fallback{display:grid;place-items:center;width:100%;height:100%;font-size:10pt;letter-spacing:.07em;font-weight:700}.logo-fallback.is-hidden{display:none}
.logo:hover{background:#eff8fe;border-color:var(--blue)}
.header-titles{flex:1 1 auto;min-width:0}.header h1{font-size:20px;line-height:1.2;font-weight:700;margin:0 0 4px;color:#172439}.header p{font-size:11pt;color:var(--muted);margin:0;line-height:1.25}
.org{margin-left:auto;flex:0 1 auto;max-width:43%;min-width:0;text-align:right;font-size:19px;font-weight:700;color:#000;line-height:1.22;overflow-wrap:anywhere;white-space:normal;cursor:pointer;user-select:none}
.org[contenteditable=true]{background:#fffbeb;outline:2px solid #f59e0b;outline-offset:2px;padding:3px 5px;user-select:text}
.org:focus-visible,.logo:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
.app>.tools{display:inline-flex;align-self:flex-start;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:8px;padding:7px 9px;margin:0;max-width:100%;background:#fff;border:1px solid var(--line);border-radius:6px;box-shadow:0 1px 7px #0f172a08}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:34px;padding:5px 11px;border:1px solid var(--line);border-radius:5px;background:#fff;color:#334155;font-size:11pt;font-weight:600;white-space:nowrap}
.btn:hover{filter:brightness(.97)}.btn:focus-visible{outline:2px solid var(--blue);outline-offset:1px}
.excel-in{background:#dbeeff;border-color:#93c5fd;color:#075985}.excel-out{background:#dcfce7;border-color:#86efac;color:#166534}.print-action{background:#e9edf2;border-color:#cbd5e1;color:#334155}
main{width:100%;display:flex;flex:1;flex-direction:column;min-width:0;gap:6px}
.panel{background:#fff;border:1px solid var(--line);border-radius:6px;box-shadow:0 1px 8px #0f172a06;min-width:0;margin:0;overflow:visible}
.panel-head{display:flex;align-items:center;gap:9px;padding:7px 12px;min-height:35px;background:#edf5fb;border-bottom:1px solid #dbe5ee;flex-wrap:wrap}
.panel-head h2{font-size:12pt;color:#075985;margin:0;font-weight:700}.meta{color:var(--muted);font-size:10pt}
.statusline{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-left:auto}
.small{font-size:9.5pt;color:#64748b}.chip{display:inline-block;min-width:0;padding:2px 6px;border-radius:4px;font-size:10pt;font-weight:700;white-space:nowrap}
.chip.ok{background:#dcfce7;color:#166534}.chip.info{background:#dbeafe;color:#075985}.chip.warn{background:#fef3c7;color:#92400e}.chip.bad{background:#fee2e2;color:#991b1b}.chip.off{background:#e2e8f0;color:#475569}
.form-wrap{display:grid;grid-template-columns:1.08fr .97fr 1.14fr;gap:8px;padding:8px}
.form-group{min-width:0;padding:0 7px 6px;border:1px solid #dbe3eb;border-radius:5px;background:#fff}
.group-title{display:flex;align-items:center;min-height:28px;margin:0 -7px 6px;padding:6px 8px;background:#edf6fc;border-bottom:1px solid #d6e5f1;color:#075985;font-size:11pt;font-weight:700}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:7px;row-gap:4px}
.form-group:last-child .grid{grid-template-columns:repeat(3,minmax(0,1fr))}.form-group:last-child .field.span4{grid-column:1/-1}
.field{display:flex;flex-direction:column;gap:1px;min-width:0}.field.span2,.field.span4{grid-column:auto}
.field label{display:flex;align-items:flex-end;min-height:18px;font-size:11pt;font-weight:600;color:#334155;line-height:1.05;overflow-wrap:anywhere}
.field input,.field select,.field textarea{display:block;width:100%;min-width:0;height:33px;min-height:33px;padding:5px 6px;color:#111827;background:#fff;border:1px solid #bdcbd8;border-radius:4px;font-size:11pt;line-height:1.15;outline:0}
.field textarea{height:33px;min-height:33px;resize:vertical;overflow:hidden;white-space:pre-wrap}
.field :is(input,select,textarea):focus{border-color:#0284c7;box-shadow:0 0 0 2px #0284c71a}
.field input[readonly]{background:#f1f5f9;color:#475569}.required::after{content:' *';color:#be123c}
.form-footer{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;min-height:31px;padding:5px 11px;border-top:1px solid #e2e8f0}
.form-footer .small{margin-left:auto}.btn.danger{min-height:30px;background:#fff1f2;color:#9f1239;border-color:#fecdd3}
.stat{display:flex;gap:10px;align-items:center;flex-wrap:wrap;color:#64748b;font-size:10pt}.stat strong{color:#075985}.stat span+span::before{content:'·';margin-right:9px;color:#94a3b8}
.search{width:230px;max-width:100%;height:33px;margin-left:auto;padding:5px 8px;font-size:11pt;border:1px solid var(--line);border-radius:4px}
.tablewrap{width:100%;overflow:visible}.tablewrap table{width:100%;border-collapse:collapse;table-layout:fixed}
.tablewrap th,.tablewrap td{padding:6px 8px;font-size:11pt;line-height:1.1;text-align:left;vertical-align:middle;border-bottom:1px solid #e9eef3;overflow-wrap:anywhere;white-space:normal}
.tablewrap th{background:#f1f6fb;color:#334155;font-weight:700}.tablewrap tr.active{background:#f0f9ff}
.tablewrap th:nth-child(1){width:13%}.tablewrap th:nth-child(2){width:25%}.tablewrap th:nth-child(3){width:12%}.tablewrap th:nth-child(4){width:17%}.tablewrap th:nth-child(5){width:12%}.tablewrap th:nth-child(6){width:10%}.tablewrap th:nth-child(7){width:11%}.tablewrap th:nth-child(8){width:72px}
.tablewrap .empty{padding:12px;text-align:center;color:var(--muted)}.tablebtn{min-height:27px;padding:4px 7px;font-size:10pt}
.list-nav{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:32px;padding:4px 10px;background:#fff;font-size:10pt;color:#64748b;border-top:1px solid #e2e8f0}
/* Sayfalama kaldırıldı: tüm kayıtlar tek tabloda, iç dikey kaydırma olmadan listelenir. */.nav-btn{padding:4px 8px;border-radius:4px;border:1px solid var(--line);background:#f8fafc;font-size:10pt;color:#334155}
.screen-pagination{display:flex;align-items:center;justify-content:flex-end;margin-top:auto;padding:3px;font-size:10pt;color:#64748b}
.toast{position:fixed;z-index:99;bottom:15px;right:15px;padding:10px 14px;background:#1e4057;color:#fff;border-radius:6px;max-width:420px}.toast.err{background:#9f1239}
.hidden,.file{display:none!important}.print-only{display:none}
@media(min-width:901px) and (max-width:1100px){.form-group:last-child .grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media(min-width:701px) and (max-width:1100px){.header{padding:6px 12px}.app>.tools{padding:5px 8px}.list-head .search{width:165px}.list-head{min-height:32px;padding:5px 9px}}
@media(max-width:900px){.form-wrap{grid-template-columns:1fr 1fr}.form-group:last-child{grid-column:1/-1}.form-group:last-child .grid{grid-template-columns:repeat(4,minmax(0,1fr))}.form-group:last-child .field.span4{grid-column:1/-1}.app{gap:7px}}
@media(max-width:700px){.app{padding:6px}.header{gap:8px;padding:8px;align-items:center}.logo{flex-basis:46px;width:46px;height:46px}.header h1{font-size:14pt}.header p{font-size:9pt}.org{font-size:12pt;max-width:38%}.form-wrap{grid-template-columns:1fr}.form-group:last-child{grid-column:auto}.form-group:last-child .grid{grid-template-columns:repeat(2,minmax(0,1fr))}.panel-head{padding:7px}.tablewrap{overflow-x:auto}.tablewrap table{min-width:900px}.search{margin-left:0}.field label{min-height:24px}}
@media(max-width:430px){.form-group .grid,.form-group:last-child .grid{grid-template-columns:1fr}.field label{min-height:0}.header-titles h1{font-size:12pt}.org{font-size:10pt;max-width:33%}.app>.tools .btn{font-size:10pt}}
@page{size:A4 landscape;margin:9mm 9mm 15mm;@bottom-right{content:'Sayfa ' counter(page) ' / ' counter(pages);font-family:'Segoe UI',Arial,sans-serif;font-weight:600;font-size:9pt;color:#475569}}
@media print{
html,body{width:auto!important;height:auto!important;min-height:0!important;overflow:visible!important;background:#fff!important}
body{font:11pt/1.26 'Segoe UI',Arial,sans-serif!important;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.app{display:block!important;min-height:0!important;height:auto!important;overflow:visible!important;border:1px solid #b5c2ce!important;padding:2mm!important;background:#fff!important;box-shadow:none!important}
.header{display:flex!important;min-height:0!important;gap:3mm!important;padding:2mm 3mm!important;margin:0 0 2mm!important;border:1px solid #cbd5e1!important;border-radius:1.2mm!important;break-inside:avoid!important;box-shadow:none!important}
.header h1{font-size:15pt!important}.header p{font-size:11pt!important}.org{font-size:14pt!important;max-width:44%!important}.logo{width:16mm!important;height:14mm!important;flex-basis:16mm!important}
main,#printContent{display:block!important;width:100%!important;height:auto!important;overflow:visible!important;min-height:0!important;margin:0!important;padding:0!important}
.tools,.panel,.toast,.no-print,.form-footer,.file,.screen-pagination{display:none!important}
.print-only{display:block!important}
.print-summary{display:flex;gap:3mm;align-items:center;justify-content:space-between;flex-wrap:wrap;padding:2mm 2.4mm;border:1px solid #bfd0de;background:#edf5fb;color:#075985;font-size:11pt;font-weight:700;break-inside:avoid}
.print-section{border:1px solid #cbd5e1;background:#fff;margin-top:2.5mm;break-inside:auto;overflow:visible!important}
.print-group-title{font-size:11pt;font-weight:700;color:#075985;background:#edf5fb;padding:1.6mm 2.4mm;border-bottom:1px solid #cbd5e1;break-after:avoid}
.print-fields{width:100%;border-collapse:collapse;table-layout:fixed;border-spacing:0}.print-fields tr{break-inside:avoid-page}.print-fields td{background:#fff;padding:1.6mm 2mm;border:1px solid #e2e8f0;vertical-align:top;overflow-wrap:anywhere}
.print-fields td b,.print-note b{display:block;font-size:11pt;color:#475569;font-weight:700;margin-bottom:.8mm}
.print-value{display:block;font-size:11pt;color:#111827;line-height:1.3;overflow-wrap:anywhere;white-space:pre-wrap}.print-value.is-empty{color:#94a3b8}
.print-note{padding:2mm;border-top:1px solid #e2e8f0;overflow-wrap:anywhere;break-inside:auto}
.print-footer{padding:2mm 0 0;font-size:9pt;color:#64748b;break-inside:avoid}
.print-register{width:100%;table-layout:fixed;border-collapse:collapse;margin-top:1.5mm;font-size:11pt}
.print-register thead{display:table-header-group}.print-register tr{break-inside:avoid-page}
.print-register th,.print-register td{border:1px solid #cbd5e1;padding:1.6mm 1.3mm;text-align:left;white-space:normal;overflow-wrap:anywhere;vertical-align:top;font-size:11pt}
.print-register th{background:#edf5fb;color:#075985}
.print-register th:nth-child(1){width:13%}.print-register th:nth-child(2){width:24%}.print-register th:nth-child(3){width:14%}.print-register th:nth-child(4){width:18%}.print-register th:nth-child(5){width:12%}.print-register th:nth-child(6){width:7%}.print-register th:nth-child(7){width:12%}
}
</style>
</head>
<body>
<div class="app" data-design="OLGUN-UI2-11PT-BOS-BASLANGIC-SURE-POLITIKASI">
<header class="header">
<button id="logoButton" class="logo" type="button" title="Logo seçmek veya değiştirmek için tıklayın. (PNG, JPG, WebP, GIF)" aria-label="Kurum logosunu seç veya değiştir">
<img id="logoImage" src="logo.jpg" alt="Kurum logosu" decoding="async"><span id="logoFallback" class="logo-fallback" aria-hidden="true">LOGO</span>
</button><input id="logoFile" class="file" type="file" accept="image/png,image/jpeg,image/webp,image/gif,.png,.jpg,.jpeg,.webp,.gif">
<div class="header-titles"><h1>Makine Kimliği, Sertifika ve Anahtar Süre Takip Aracı</h1><p>(Makine kimlikleri ve kriptografik bileşenlerin geçerlilik ve yenileme takibi)</p></div>
<div id="organization" class="org" title="Kurum adını değiştirmek için art arda 5 kez tıklayın veya dokunun; klavyede Enter da kullanılabilir" aria-label="Kurum adını değiştirmek için art arda beş kez tıklayın" role="button" tabindex="0">OLGUN BAŞKANLIĞI</div>
</header>
<div class="tools no-print" role="toolbar" aria-label="Form işlem butonları">
<button type="button" id="excelInBtn" class="btn excel-in">↥ Excel İçeri Aktar</button>
<button type="button" id="excelOutBtn" class="btn excel-out">↧ Excel Dışarı Aktar</button>
<button type="button" id="printBtn" class="btn print-action">🖨 PDF / Yazdır</button>
<input id="excelFile" class="file" type="file" accept=".xlsx">
</div>
<main>
<section class="panel" id="editorPanel">
<div class="panel-head"><h2>Kimlik, Sertifika ve Anahtar Takip Formu</h2><span id="mode" class="meta" aria-live="polite">Boş form · Kayıtlar bu sekmede tutulur</span>
<div class="statusline"><b class="small">Süre Durumu:</b><span id="liveStatus" class="chip off">Tarih Girilmedi</span><span id="daysLabel" class="small">Son kullanma tarihi bekleniyor</span></div></div>
<form id="recordForm" autocomplete="off" novalidate>
<div class="form-wrap">
<section class="form-group"><div class="group-title">Kayıt ve Sorumluluk Bilgileri</div>
<div class="grid">
<div class="field"><label for="recordNo">Kayıt No</label><input id="recordNo" name="recordNo" type="text" readonly placeholder="Otomatik atanır"></div>
<div class="field"><label for="recordDate">Kayıt Tarihi</label><input id="recordDate" name="recordDate" type="date"></div>
<div class="field"><label class="required" for="recordType">Kayıt Türü</label><select id="recordType" name="recordType"><option value="">Seçiniz</option><option>Makine Kimliği</option><option>Dijital Sertifika</option><option>Kriptografik Anahtar</option><option>API Anahtarı / Token</option></select></div>
<div class="field"><label for="environment">Ortam</label><select id="environment" name="environment"><option value="">Seçiniz</option><option>Üretim</option><option>Test</option><option>Geliştirme</option><option>Felaket Kurtarma</option><option>Diğer</option></select></div>
<div class="field span2"><label class="required" for="recordName">Kimlik / Sertifika / Anahtar Adı</label><input id="recordName" name="recordName" placeholder="Örn. Kurum web SSL sertifikası"></div>
<div class="field span2"><label class="required" for="system">İlgili Sistem / Uygulama</label><input id="system" name="system" placeholder="Sistem veya servis adı"></div>
<div class="field span2"><label for="department">Sorumlu Birim</label><input id="department" name="department" placeholder="Departman / Birim"></div>
<div class="field span2"><label class="required" for="owner">Teknik Sorumlu</label><input id="owner" name="owner" placeholder="Adı Soyadı / Görev"></div>
</div>
</section>
<section class="form-group"><div class="group-title">Teknik Tanım</div>
<div class="grid">
<div class="field span2"><label for="resourceAddress">Hostname / FQDN / Servis Adresi</label><input id="resourceAddress" name="resourceAddress" placeholder="Sunucu, servis veya uç nokta"></div>
<div class="field span2"><label for="identifierRef">Kimlik / Seri No / Güvenli Depo Referansı</label><input id="identifierRef" name="identifierRef" placeholder="Sadece kimlik veya referans; gizli anahtar değil"></div>
<div class="field span2"><label for="issuerSource">Sertifika Otoritesi / KMS / Kimlik Sağlayıcı</label><input id="issuerSource" name="issuerSource" placeholder="CA, KMS, Vault, IdP vb."></div>
<div class="field span2"><label for="usagePurpose">Kullanım Amacı</label><input id="usagePurpose" name="usagePurpose" placeholder="TLS, imzalama, kimlik doğrulama vb."></div>
</div>
</section>
<section class="form-group"><div class="group-title">Süre, Yenileme ve Durum</div>
<div class="grid">
<div class="field"><label for="validFrom">Geçerlilik Başlangıcı</label><input id="validFrom" name="validFrom" type="date"></div>
<div class="field"><label for="expiresAt">Son Kullanma Tarihi</label><input id="expiresAt" name="expiresAt" type="date"></div>
<div class="field"><label for="plannedRenewal">Planlanan Yenileme</label><input id="plannedRenewal" name="plannedRenewal" type="date"></div>
<div class="field"><label for="lastRenewal">Son Yenileme Tarihi</label><input id="lastRenewal" name="lastRenewal" type="date"></div>
<div class="field"><label for="autoRenew">Otomatik Yenileme</label><select id="autoRenew" name="autoRenew"><option value="">Seçiniz</option><option>Aktif</option><option>Pasif</option><option>Bilinmiyor</option></select></div>
<div class="field"><label for="renewalState">Yenileme Durumu</label><select id="renewalState" name="renewalState"><option value="">Seçiniz</option><option>Planlanmadı</option><option>Planlandı</option><option>Devam Ediyor</option><option>Tamamlandı</option><option>Başarısız</option></select></div>
<div class="field"><label for="riskLevel">Kritiklik Seviyesi</label><select id="riskLevel" name="riskLevel"><option value="">Seçiniz</option><option>Düşük</option><option>Orta</option><option>Yüksek</option><option>Kritik</option></select></div>
<div class="field"><label for="recordState">Kayıt Durumu</label><select id="recordState" name="recordState"><option value="">Seçiniz</option><option>Aktif</option><option>Pasif</option><option>İptal</option></select></div>
<div class="field span4"><label for="notes">Açıklamalar / Alınacak Önlemler</label><textarea id="notes" name="notes" rows="2" placeholder="Kısa açıklama, yenileme işlemi ve izlenecek aksiyonlar"></textarea></div>
</div>
</section>
</div>
<div class="form-footer no-print"><button id="deleteBtn" type="button" class="btn danger" disabled>Kaydı Sil</button><span class="small">Doldurulan kayıtlar açık sekmede izlenir. Kapatmadan önce Excel Dışarı Aktar kullanın. * Zorunlu alan.</span></div>
</form>
</section>
<section class="panel no-print" aria-label="Kayıt Listesi">
<div class="panel-head list-head"><h2>Kayıt Listesi</h2><div class="stat"><span>Toplam <strong id="totalCount">0</strong></span><span>30 gün içinde <strong id="soonCount">0</strong></span><span>Süresi dolmuş <strong id="expiredCount">0</strong></span></div><input id="search" class="search" type="search" placeholder="Kayıt, sistem veya sorumlu ara" aria-label="Kayıtlarda ara"></div>
<div class="tablewrap"><table><thead><tr><th>Kayıt No</th><th>Kayıt Adı</th><th>Tür</th><th>Sistem</th><th>Son Kullanma</th><th>Kalan Gün</th><th>Süre Durumu</th><th>İşlem</th></tr></thead><tbody id="recordsBody"><tr><td colspan="8" class="empty">Henüz kayıt bulunmuyor.</td></tr></tbody></table></div>
<div class="list-nav no-print"><button id="emptyFormBtn" class="nav-btn" type="button">Boş Form</button><span id="pageInfo" aria-live="polite">Tüm kayıtlar gösteriliyor · 0 kayıt</span></div>
</section>
<div id="printContent" class="print-only"></div>
</main>
<div class="screen-pagination no-print"><span id="screenPageNumber">Kayıt listesi · 0 kayıt</span></div>
</div>
<div id="toast" class="toast hidden" role="status"></div>
<script>
(()=>{'use strict';
const STORE='olgun-machine-identity-v1';
const ORG_STORE='olgun-machine-identity-org-v1';
const LOGO_STORE='olgun-ui2-machine-logo-v1';
const DRAFT_STORE='olgun-machine-identity-autodraft-v1';
const byId=id=>document.getElementById(id);
const escapeHTML=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const today=()=>{const d=new Date();return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-')};
const dateTR=d=>/^\d{4}-\d{2}-\d{2}$/.test(d||'')?d.split('-').reverse().join('.'):('—');
const columns=[
['recordNo','Kayıt No'],['recordDate','Kayıt Tarihi'],['recordType','Kayıt Türü'],['environment','Ortam'],['recordName','Kayıt Adı'],
['system','İlgili Sistem / Uygulama'],['department','Sorumlu Birim'],['owner','Teknik Sorumlu'],['resourceAddress','Hostname / FQDN / Servis Adresi'],
['identifierRef','Kimlik / Seri / Depo Referansı'],['issuerSource','Sertifika Otoritesi / KMS / IdP'],['usagePurpose','Kullanım Amacı'],
['validFrom','Geçerlilik Başlangıcı'],['expiresAt','Son Kullanma Tarihi'],['plannedRenewal','Planlanan Yenileme'],['lastRenewal','Son Yenileme Tarihi'],
['autoRenew','Otomatik Yenileme'],['renewalState','Yenileme Durumu'],['riskLevel','Kritiklik Seviyesi'],['recordState','Kayıt Durumu'],['notes','Açıklamalar']
].map(([id,label])=>({id,label}));
const dates=new Set(['recordDate','validFrom','expiresAt','plannedRenewal','lastRenewal']);
document.addEventListener('input',e=>{if(e.target.matches?.('.field input, .field textarea'))e.target.title=e.target.value;});
let records=[],selectedId=null,toastTimer;
// Tüm kayıtlar sayfalama yapılmadan aynı tabloda görüntülenir.
const names=columns.map(x=>x.id);
function migrate(r){return {...r, recordNo:r.recordNo||'', recordName:r.recordName||'', recordType:r.recordType||'', system:r.system||'',
environment:r.environment==='Production'?'Üretim':r.environment==='Development'?'Geliştirme':r.environment==='DR'?'Felaket Kurtarma':r.environment||'',
resourceAddress:r.resourceAddress||r.hostname||r.scanTarget||r.ipAddress||'',identifierRef:r.identifierRef||r.certSerial||r.keyReference||r.identityId||r.certFingerprint||'',
issuerSource:r.issuerSource||r.caProvider||r.keyManager||r.idProvider||'',usagePurpose:r.usagePurpose||r.keyPurpose||r.certType||r.identityType||'',
validFrom:r.validFrom||r.certStart||r.keyStart||'',expiresAt:r.expiresAt||r.certEnd||r.keyEnd||r.identityEnd||'',
notes:r.notes||r.generalNote||r.renewalComment||''};}
function getForm(){const data={};for(const id of names)data[id]=byId(id).value.trim();return data;}
function setForm(d){for(const id of names){byId(id).value=d[id]??'';byId(id).title=byId(id).value;}autoGrow();updateLive();}
// Kayıtlar geçmiş sürümlerden veya tarayıcı depolamasından yüklenmez.
// HTML her açılışta sıfır kayıtla başlar. Aktarılan veriler yalnızca aktif sayfada tutulur.
function load(){records=[];}
function persist(){return true;} // Geçici oturum: depolamaya yazma yok.
function showToast(text,error=false){const e=byId('toast');e.textContent=text;e.className='toast'+(error?' err':'');clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.add('hidden'),4500)}
function autoGrow(){const t=byId('notes');t.style.height='auto';t.style.height=Math.max(33,t.scrollHeight+2)+'px';t.style.overflowY='hidden'}
function daysUntil(date){if(!/^\d{4}-\d{2}-\d{2}$/.test(date||''))return null;const [y,m,d]=date.split('-').map(Number);const dt=new Date(Date.UTC(y,m-1,d));if(dt.getUTCFullYear()!==y||dt.getUTCMonth()!==m-1||dt.getUTCDate()!==d)return null;const n=new Date();return Math.round((Date.UTC(y,m-1,d)-Date.UTC(n.getFullYear(),n.getMonth(),n.getDate()))/86400000)}
// Süre durumları, kayıt/yenileme tarihi veya manuel risk düzeyinden değil, yalnızca Son Kullanma Tarihi'nden hesaplanır.
function evaluate(r){const d=daysUntil(r.expiresAt);
if(r.recordState==='İptal')return {text:'İptal',css:'off',days:d};
if(r.recordState==='Pasif')return {text:'Pasif',css:'off',days:d};
if(d===null)return {text:'Tarih Girilmedi',css:'off',days:null};
if(d<0)return {text:'Süresi Dolmuş',css:'bad',days:d};
if(d<=7)return {text:'Kritik',css:'bad',days:d};
if(d<=30)return {text:'Uyarı',css:'warn',days:d};
if(d<=90)return {text:'Bilgilendirme',css:'info',days:d};
return {text:'Geçerli',css:'ok',days:d};}
function updateLive(){const e=evaluate(getForm());byId('liveStatus').textContent=e.text;byId('liveStatus').className='chip '+e.css;byId('daysLabel').textContent=e.days===null?'Son kullanma tarihi bekleniyor':(e.days<0?Math.abs(e.days)+' gün geçti':e.days+' gün kaldı');}
function generateId(){let id;do{id='MK-'+today().replaceAll('-','')+'-'+Math.random().toString(36).slice(2,7).toUpperCase()}while(records.some(r=>r.recordNo===id));return id}
function clearDraft(){} // Taslak saklama kapalı; her açılışta temiz başlangıç.
function storeDraft(data){return true;} // Kullanıcının o anda yazdığı değerler formda kalır.
function resetForm(clear=true){selectedId=null;byId('recordForm').reset();setForm({});byId('mode').textContent='Boş form · Yalnızca açık sekmede';byId('deleteBtn').disabled=true;if(clear)clearDraft();renderTable()}
function hasUserInput(r){return names.some(id=>!['recordNo','recordDate'].includes(id)&&String(r[id]||'').trim())}
function datesValid(r){return !(r.validFrom&&r.expiresAt&&r.validFrom>r.expiresAt)&&!(r.plannedRenewal&&r.validFrom&&r.plannedRenewal<r.validFrom)}
function autoSave(){const data=getForm();
if(selectedId){const index=records.findIndex(r=>r.recordNo===selectedId);if(index>=0){const old=records[index];records[index]={...old,...data,recordNo:selectedId,updatedAt:new Date().toISOString()};
if(!persist())records[index]=old;else{byId('mode').textContent='Bu sekmede güncellendi · '+selectedId;renderCounts();updateLive();updateSelectedRow(records[index])}return;}}
if(!hasUserInput(data)){clearDraft();byId('mode').textContent='Boş form · Yalnızca açık sekmede';return;}
if(!['recordType','recordName','system','owner'].every(id=>data[id])||!datesValid(data)){
storeDraft(data);byId('mode').textContent=datesValid(data)?'Taslak · zorunlu alanlar bekleniyor':'Taslak · tarihleri kontrol edin';return;}
const recordNo=generateId(),now=new Date().toISOString(),next={...data,recordNo,recordDate:data.recordDate||today(),createdAt:now,updatedAt:now};
records.unshift(next);if(!persist()){records.shift();storeDraft(data);return;}
selectedId=recordNo;clearDraft();byId('recordNo').value=recordNo;byId('deleteBtn').disabled=false;byId('mode').textContent='Bu sekmede kaydedildi · '+recordNo;renderAll();showToast('Kayıt bu sekmede oluşturuldu. Kapatmadan önce Excel ile dışarı aktarın.');}
function restoreDraft(){} // Eski taslak hiçbir koşulda geri getirilmez.
function openRecord(id){const record=records.find(r=>r.recordNo===id);if(!record)return;selectedId=id;setForm(record);byId('mode').textContent='Düzenleniyor: '+id;byId('deleteBtn').disabled=false;renderTable();if(window.innerWidth<850)byId('editorPanel').scrollIntoView({behavior:'smooth',block:'start'})}
function deleteRecord(){if(!selectedId)return;const r=records.find(x=>x.recordNo===selectedId);if(!r)return;if(!confirm('“'+r.recordName+'” kaydı silinsin mi?'))return;
const old=records.slice();records=records.filter(x=>x.recordNo!==selectedId);if(!persist()){records=old;return}resetForm();renderAll();showToast('Kayıt silindi.')}
function updateSelectedRow(r){const b=Array.from(byId('recordsBody').querySelectorAll('[data-edit]')).find(n=>n.dataset.edit===r.recordNo);if(!b)return;
const row=b.closest('tr'),cells=row?.querySelectorAll('td');if(!cells||cells.length<8)return;const e=evaluate(r);
[r.recordNo,r.recordName,r.recordType,r.system,dateTR(r.expiresAt),e.days===null?'—':e.days].forEach((v,i)=>cells[i].textContent=v||'');
cells[6].innerHTML=`<span class="chip ${e.css}">${escapeHTML(e.text)}</span>`;}
function renderCounts(){const active=records.map(evaluate);byId('totalCount').textContent=records.length;byId('soonCount').textContent=active.filter(x=>x.text==='Kritik'||x.text==='Uyarı').length;byId('expiredCount').textContent=active.filter(x=>x.text==='Süresi Dolmuş').length}
function renderTable(){
const q=byId('search').value.toLocaleLowerCase('tr-TR');
const subset=records.filter(r=>[r.recordNo,r.recordName,r.recordType,r.system,r.owner].some(v=>String(v||'').toLocaleLowerCase('tr-TR').includes(q)));
const rows=subset.length?subset.map(r=>{const e=evaluate(r);return `<tr class="${r.recordNo===selectedId?'active':''}"><td>${escapeHTML(r.recordNo)}</td><td>${escapeHTML(r.recordName)}</td><td>${escapeHTML(r.recordType)}</td><td>${escapeHTML(r.system)}</td><td>${dateTR(r.expiresAt)}</td><td>${e.days===null?'—':e.days}</td><td><span class="chip ${e.css}">${escapeHTML(e.text)}</span></td><td><button class="btn tablebtn" type="button" data-edit="${escapeHTML(r.recordNo)}">Düzenle</button></td></tr>`}).join(''):'<tr><td class="empty" colspan="8">Kayıt bulunamadı.</td></tr>';
byId('recordsBody').innerHTML=rows;
byId('pageInfo').textContent=`Tüm kayıtlar gösteriliyor · ${subset.length} kayıt`;
byId('screenPageNumber').textContent=`Kayıt listesi · ${subset.length} kayıt`;
}
function renderAll(){renderCounts();renderTable();updateLive()}
function download(blob,name){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000)}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const encode = text => encoder.encode(String(text));
const xmlSafe = text => String(text ?? '').replace(/[&<>"']/g, ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]))
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g,'');
function crc32(bytes){
let crc=0xffffffff;for(const byte of bytes){crc^=byte;for(let j=0;j<8;j++)crc=(crc>>>1)^((crc&1)?0xedb88320:0);}
return (crc^0xffffffff)>>>0;
}
function zipFiles(files){
const local=[],central=[];let offset=0;
for(const [name,content] of Object.entries(files)){
const path=encode(name),bytes=encode(content),crc=crc32(bytes);
const h=new Uint8Array(30+path.length),v=new DataView(h.buffer);
v.setUint32(0,0x04034b50,true);v.setUint16(4,20,true);v.setUint16(6,0x0800,true);
v.setUint16(8,0,true);v.setUint32(14,crc,true);v.setUint32(18,bytes.length,true);v.setUint32(22,bytes.length,true);
v.setUint16(26,path.length,true);h.set(path,30);local.push(h,bytes);
const c=new Uint8Array(46+path.length),cv=new DataView(c.buffer);
cv.setUint32(0,0x02014b50,true);cv.setUint16(4,20,true);cv.setUint16(6,20,true);cv.setUint16(8,0x0800,true);
cv.setUint32(16,crc,true);cv.setUint32(20,bytes.length,true);cv.setUint32(24,bytes.length,true);
cv.setUint16(28,path.length,true);cv.setUint32(42,offset,true);c.set(path,46);central.push(c);
offset+=h.length+bytes.length;
}
const centralLength=central.reduce((n,x)=>n+x.length,0),end=new Uint8Array(22),ev=new DataView(end.buffer);
ev.setUint32(0,0x06054b50,true);ev.setUint16(8,central.length,true);ev.setUint16(10,central.length,true);
ev.setUint32(12,centralLength,true);ev.setUint32(16,offset,true);
const all=[...local,...central,end],out=new Uint8Array(all.reduce((n,x)=>n+x.length,0));
let pos=0;for(const part of all){out.set(part,pos);pos+=part.length;}return out;
}
function excelColumn(index){let s='';for(index++;index;index=Math.floor((index-1)/26))s=String.fromCharCode(65+(index-1)%26)+s;return s;}
function worksheetXML(rows){
const sheet=rows.map((row,ri)=>`<row r="${ri+1}">${row.map((value,ci)=>{
if(value===null||value===undefined||value==='')return '';
return `<c r="${excelColumn(ci)}${ri+1}" t="inlineStr"><is><t xml:space="preserve">${xmlSafe(value)}</t></is></c>`;
}).join('')}</row>`).join('');
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${sheet}</sheetData></worksheet>`;
}
function createWorkbook(sheets){
const names=Object.keys(sheets),base='http://schemas.openxmlformats.org/officeDocument/2006/relationships',files={};
files['[Content_Types].xml']=`<?xml version="1.0" encoding="UTF-8"?><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"/>${names.map((_,i)=>`<Override PartName="/xl/worksheets/sheet${i+1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('')}</Types>`;
files['_rels/.rels']=`<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="${base}/officeDocument" Target="xl/workbook.xml"/></Relationships>`;
files['xl/workbook.xml']=`<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="${base}"><sheets>${names.map((name,i)=>`<sheet name="${xmlSafe(name)}" sheetId="${i+1}" r:id="rId${i+1}"/>`).join('')}</sheets></workbook>`;
files['xl/_rels/workbook.xml.rels']=`<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${names.map((_,i)=>`<Relationship Id="rId${i+1}" Type="${base}/worksheet" Target="worksheets/sheet${i+1}.xml"/>`).join('')}</Relationships>`;
names.forEach((name,i)=>files[`xl/worksheets/sheet${i+1}.xml`]=worksheetXML(sheets[name]));
return zipFiles(files);
}
async function unzipExcel(arrayBuffer){
const bytes=new Uint8Array(arrayBuffer),view=new DataView(arrayBuffer),files={};
let end=-1;for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--){if(view.getUint32(i,true)===0x06054b50){end=i;break;}}
if(end<0)throw Error('Geçerli bir XLSX / ZIP dosyası değil.');
const count=view.getUint16(end+10,true);let pos=view.getUint32(end+16,true);
for(let n=0;n<count;n++){
if(view.getUint32(pos,true)!==0x02014b50)throw Error('Excel ZIP dizini okunamadı.');
const method=view.getUint16(pos+10,true),length=view.getUint32(pos+20,true),nameLen=view.getUint16(pos+28,true);
const extraLen=view.getUint16(pos+30,true),commentLen=view.getUint16(pos+32,true),localOffset=view.getUint32(pos+42,true);
const name=decoder.decode(bytes.slice(pos+46,pos+46+nameLen));
const nameLengthLocal=view.getUint16(localOffset+26,true),extraLengthLocal=view.getUint16(localOffset+28,true);
const start=localOffset+30+nameLengthLocal+extraLengthLocal;let data=bytes.slice(start,start+length);
if(method===8){
if(typeof DecompressionStream==='undefined')throw Error('Bu tarayıcı XLSX sıkıştırmasını desteklemiyor.');
try { data=new Uint8Array(await new Response(new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer()); }
catch {throw Error('Sıkıştırılmış Excel verisi açılamadı. Güncel Chrome/Edge deneyin.');}
} else if(method!==0)throw Error('Desteklenmeyen Excel sıkıştırma yöntemi.');
files[name]=decoder.decode(data);pos+=46+nameLen+extraLen+commentLen;
}
return files;
}
function parseXml(xml){const doc=new DOMParser().parseFromString(xml,'application/xml');if(doc.querySelector('parsererror'))throw Error('Excel XML içeriği hatalı.');return doc;}
function xmlNodes(parent,name){return Array.from(parent.getElementsByTagNameNS('*',name));}
function readWorksheet(text,sharedStrings){
const doc=parseXml(text),rows=[];
for(const row of xmlNodes(doc,'row')){
const number=Math.max(0,Number(row.getAttribute('r')||rows.length+1)-1),values=[];
for(const cell of xmlNodes(row,'c')){
const ref=cell.getAttribute('r')||'A1',match=/^[A-Z]+/.exec(ref);if(!match)continue;
const index=match[0].split('').reduce((acc,ch)=>acc*26+ch.charCodeAt(0)-64,0)-1;
const type=cell.getAttribute('t'),v=xmlNodes(cell,'v')[0],inline=xmlNodes(cell,'is')[0];
values[index]=type==='s'?sharedStrings[Number(v?.textContent||0)]||'':
type==='inlineStr'?(inline?.textContent||''):(v?.textContent||'');
}
rows[number]=values;
}
return rows;
}
async function readWorkbook(file){
const files=await unzipExcel(await file.arrayBuffer());
const book=files['xl/workbook.xml'],rels=files['xl/_rels/workbook.xml.rels'];
if(!book||!rels)throw Error('Excel çalışma kitabı bulunamadı.');
const relMap={};for(const rel of xmlNodes(parseXml(rels),'Relationship'))relMap[rel.getAttribute('Id')]=rel.getAttribute('Target');
const shared=files['xl/sharedStrings.xml']?xmlNodes(parseXml(files['xl/sharedStrings.xml']),'si').map(si=>si.textContent):[];
const result={};for(const sh of xmlNodes(parseXml(book),'sheet')){
const id=sh.getAttribute('r:id')||sh.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships','id');
const target=relMap[id];if(!target)continue;
const path=target.startsWith('/')?target.slice(1):'xl/'+target.replace(/^\.\//,'');
const normalized=path.split('/').reduce((acc,p)=>{if(p==='..')acc.pop();else if(p!=='.')acc.push(p);return acc;},[]).join('/');
if(files[normalized])result[sh.getAttribute('name')]=readWorksheet(files[normalized],shared);
}
return result;
}
// Dikey Excel: her form alani alt alta; her kayit ayri bir ornek sutunundadir.
function exportExcel(){
const rows=[['Form Alanı',...records.map((r,i)=>r.recordNo||'Kayıt '+(i+1))],
...columns.map(c=>[c.label,...records.map(r=>r[c.id]||'')])];
const bin=createWorkbook({Dikey_Kayit:rows});
download(new Blob([bin],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),'Makine_Kimligi_Sertifika_Anahtar_Dikey.xlsx');
showToast(records.length+' kayıt dikey Excel dosyasına aktarıldı.');
}
function excelDate(value){const s=String(value||'').trim();if(!s)return '';if(/^\d{4}-\d{2}-\d{2}/.test(s))return s.slice(0,10);if(/^\d{2}\.\d{2}\.\d{4}$/.test(s)){const [d,m,y]=s.split('.');return `${y}-${m}-${d}`};if(/^\d{4,5}(?:\.\d+)?$/.test(s)){const d=new Date(Date.UTC(1899,11,30)+Number(s)*86400000);return d.toISOString().slice(0,10)}return s}
function excelAliases(){return {recordName:['Kayıt Adı / Konusu','Kimlik / Sertifika / Anahtar Adı'],department:['Sorumlu Departman'],resourceAddress:['Hostname / FQDN','Tarama Hedefi (FQDN / IP / Port)'],
identifierRef:['Seri Numarası','Anahtar Saklama Referans ID','Makine Kimliği ID'],issuerSource:['Sertifika Sağlayıcısı (CA)','Anahtar Yönetim Sistemi','Kimlik Sağlayıcısı'],
usagePurpose:['Kullanım Amacı'],environment:['Sistem Ortamı'],notes:['Açıklama','Ek Rapor / Denetim Açıklamaları']};}
function matchExcelColumns(heads){const legacy=excelAliases();return columns.map(c=>heads.findIndex(h=>[c.label,...(legacy[c.id]||[])].some(a=>h===a||h.endsWith(' / '+a))))}
function recordFromExcel(valueFor){const r={};for(const c of columns)r[c.id]=String(valueFor(c.id)??'').trim();
if(!r.recordName||!r.recordType||!r.system||!r.owner)return null;
for(const id of dates)r[id]=excelDate(r[id]);r.recordNo=r.recordNo||generateId();r.createdAt=new Date().toISOString();r.updatedAt=r.createdAt;
return r;
}
function readVerticalRecords(rows){
if(!rows||rows.length<2)throw Error('Dikey Excel sayfasında kayıt bulunamadı.');
const labels=rows.slice(1).map(r=>String(r?.[0]??'').trim()),indexes=matchExcelColumns(labels);
if(indexes.filter(i=>i>=0).length<4)throw Error('Dikey Excel alan adları bu forma uygun değil.');
const totalCols=Math.max(...rows.map(r=>r?.length||0)),incoming=[];
for(let col=1;col<totalCols;col++){
const record=recordFromExcel(id=>{const i=columns.findIndex(c=>c.id===id);return indexes[i]<0?'':(rows[indexes[i]+1]||[])[col];});
if(record)incoming.push(record);
}
return incoming;
}
function readHorizontalRecords(rows){
if(!rows||rows.length<2)throw Error('Excel dosyasında kayıt bulunamadı.');
const heads=(rows[0]||[]).map(x=>String(x||'').trim()),indexes=matchExcelColumns(heads);
if(indexes.filter(i=>i>=0).length<4)throw Error('Excel sütunları bu forma uygun değil.');
return rows.slice(1).map(row=>recordFromExcel(id=>{const i=columns.findIndex(c=>c.id===id);return indexes[i]<0?'':row?.[indexes[i]];})).filter(Boolean);
}
async function importExcel(file){try{
const book=await readWorkbook(file),first=Object.values(book)[0];
const vertical=book.Dikey_Kayit||book.DikeyKayit||((String(first?.[0]?.[0]||'').trim()==='Form Alanı')?first:null);
const incoming=vertical?readVerticalRecords(vertical):readHorizontalRecords(book.Kayitlar||first);
if(!incoming.length)throw Error('Zorunlu alanları dolu kayıt bulunamadı.');
const prior=records.slice();let updated=0;for(const r of incoming){const i=records.findIndex(x=>x.recordNo===r.recordNo);if(i<0)records.push(migrate(r));else {records[i]={...records[i],...migrate(r)};updated++}}
if(!persist()){records=prior;return}resetForm();renderAll();showToast(incoming.length+' kayıt içeri alındı ('+updated+' güncellendi).');
}catch(e){showToast('Excel içeri aktarılamadı: '+e.message,true)}
}
function buildPrint(){
const r=getForm(),status=evaluate(r);
const sections=[
['Kayıt ve Sorumluluk Bilgileri',['recordNo','recordDate','recordType','environment','recordName','system','department','owner']],
['Teknik Tanım',['resourceAddress','identifierRef','issuerSource','usagePurpose']],
['Süre, Yenileme ve Durum',['validFrom','expiresAt','plannedRenewal','lastRenewal','autoRenew','renewalState','riskLevel','recordState','notes']]
];
const labels=Object.fromEntries(columns.map(c=>[c.id,c.label]));
const spanFor=id=>['recordName','system','department','owner','resourceAddress','identifierRef','issuerSource','usagePurpose'].includes(id)?2:1;
function cell(id,span){
const raw=String(r[id]??'');
const value=dates.has(id)&&raw?dateTR(raw):raw;
return `<td colspan="${span}"><b>${escapeHTML(labels[id])}</b><span class="print-value${!value?' is-empty':''}">${escapeHTML(value||'—')}</span></td>`;
}
function makeRows(ids){
const rows=[];let line=[],used=0;
function flush(){if(line.length){if(used<4)line.push(`<td colspan="${4-used}"></td>`);rows.push(`<tr>${line.join('')}</tr>`);line=[];used=0;}}
for(const id of ids){const span=spanFor(id);if(used+span>4)flush();line.push(cell(id,span));used+=span;if(used===4)flush();}
flush();return rows.join('');
}
const parts=[`<div class="print-summary"><span>Kayıt No: ${escapeHTML(r.recordNo||'Yeni Kayıt')}</span><span>Süre Durumu: ${escapeHTML(status.text)} · ${status.days===null?'Tarih girilmedi':status.days<0?Math.abs(status.days)+' gün geçti':status.days+' gün kaldı'}</span></div>`];
for(const [title,ids] of sections){
const short=ids.filter(id=>id!=='notes');
parts.push(`<section class="print-section"><div class="print-group-title">${escapeHTML(title)}</div><table class="print-fields"><colgroup><col><col><col><col></colgroup><tbody>${makeRows(short)}</tbody></table>`);
if(ids.includes('notes')){
const val=String(r.notes||'');
parts.push(`<div class="print-note"><b>${escapeHTML(labels.notes)}</b><span class="print-value${!val?' is-empty':''}">${escapeHTML(val||'—')}</span></div>`);
}
parts.push('</section>');
}
if(records.length){
const list=records.map(item=>{
const st=evaluate(item),date=dateTR(item.expiresAt);
const cells=[item.recordNo,item.recordName,item.recordType,item.system,date,st.days===null?'—':String(st.days),st.text];
return '<tr>'+cells.map(v=>'<td>'+escapeHTML(v||'—')+'</td>').join('')+'</tr>';
}).join('');
parts.push('<section class="print-section"><div class="print-group-title">Kayıt Listesi · Tüm Kayıtlar ('+records.length+')</div><table class="print-register"><thead><tr>'+['Kayıt No','Kayıt Adı','Tür','İlgili Sistem','Son Kullanma','Kalan Gün','Süre Durumu'].map(t=>'<th>'+t+'</th>').join('')+'</tr></thead><tbody>'+list+'</tbody></table></section>');
} else {
parts.push('<section class="print-section"><div class="print-group-title">Kayıt Listesi</div><div class="print-note"><span class="print-value">Henüz kayıt bulunmuyor.</span></div></section>');
}
parts.push(`<div class="print-footer">Makine Kimliği, Sertifika ve Anahtar Süre Takip Aracı · ${escapeHTML(new Date().toLocaleDateString('tr-TR'))}</div>`);
byId('printContent').innerHTML=parts.join('');
}
function setupLogo(){
const button=byId('logoButton'),img=byId('logoImage'),fallback=byId('logoFallback'),file=byId('logoFile');
function showLogo(){img.classList.add('is-ready');fallback.classList.add('is-hidden');}
function showFallback(){img.classList.remove('is-ready');fallback.classList.remove('is-hidden');}
img.addEventListener('load',()=>{if(img.naturalWidth>0&&img.naturalHeight>0)showLogo();else showFallback()});
img.addEventListener('error',showFallback);
// Complete+naturalWidth control covers images already loaded from the browser cache.
let stored='';try{stored=localStorage.getItem(LOGO_STORE)||''}catch{}
if(stored)img.src=stored;
if(img.complete){if(img.naturalWidth>0&&img.naturalHeight>0)showLogo();else showFallback()}
button.addEventListener('click',()=>file.click());
button.addEventListener('keydown',e=>{
// Optional reset to the file next to the HTML (logo.jpg).
if(e.key==='Delete'&&e.altKey){e.preventDefault();try{localStorage.removeItem(LOGO_STORE)}catch{}img.src='logo.jpg';if(img.complete){img.naturalWidth?showLogo():showFallback()}showToast('Logo seçimi sıfırlandı. logo.jpg dosyası kullanılacak.');}
});
file.addEventListener('change',async()=>{
const picked=file.files&&file.files[0];file.value='';if(!picked)return;
if(!/^image\/(png|jpeg|webp|gif)$/.test(picked.type)){showToast('PNG, JPG, WebP veya GIF seçin.',true);return;}
if(picked.size>12*1024*1024){showToast('Logo dosyası 12 MB sınırını aşmamalı.',true);return;}
let objURL;
try{
objURL=URL.createObjectURL(picked);
const temp=new Image();const loaded=new Promise((resolve,reject)=>{temp.onload=resolve;temp.onerror=()=>reject(Error('Seçilen logo açılamadı.'))});
temp.src=objURL;await loaded;
// Resize to avoid localStorage quota errors on high-resolution images.
const maxSide=640,ratio=Math.min(1,maxSide/Math.max(temp.naturalWidth,temp.naturalHeight));
const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(temp.naturalWidth*ratio));canvas.height=Math.max(1,Math.round(temp.naturalHeight*ratio));
canvas.getContext('2d').drawImage(temp,0,0,canvas.width,canvas.height);
const data=canvas.toDataURL('image/png');
img.src=data;if(img.complete&&img.naturalWidth)showLogo();
try{localStorage.setItem(LOGO_STORE,data);showToast('Kurum logosu güncellendi ve kaydedildi.');}
catch{showToast('Logo gösteriliyor ancak tarayıcı depolama alanına kaydedilemedi.',true)}
}catch(error){showToast('Logo yüklenemedi: '+error.message,true)}
finally{if(objURL)URL.revokeObjectURL(objURL)}
});
}
function setupOrganization(){
const el=byId('organization');
try{el.textContent=localStorage.getItem(ORG_STORE)||'OLGUN BAŞKANLIĞI'}catch{}
let clicks=0,lastClick=0,previous=el.textContent;
function editOrganization(){
if(el.isContentEditable)return;
clicks=0;previous=el.textContent;
el.contentEditable='true';el.setAttribute('role','textbox');el.setAttribute('aria-label','Kurum adı');el.focus();
const range=document.createRange(),sel=window.getSelection();range.selectNodeContents(el);sel.removeAllRanges();sel.addRange(range);
showToast('Kurum adını yazın; Enter veya alan dışına tıklayarak kaydedin.');
}
function activate(){
if(el.isContentEditable)return;
const now=Date.now();clicks=(now-lastClick<=4000)?clicks+1:1;lastClick=now;
if(clicks>=5)editOrganization();
}
function finish(cancel=false){
if(!el.isContentEditable)return;
el.contentEditable='false';el.setAttribute('role','button');el.setAttribute('aria-label','Kurum adını değiştirmek için art arda beş kez tıklayın');
const name=cancel?previous:(el.textContent.replace(/\s+/g,' ').trim()||'OLGUN BAŞKANLIĞI');
el.textContent=name;
if(!cancel){try{localStorage.setItem(ORG_STORE,name)}catch{showToast('Kurum adı değişti ancak kalıcı saklanamadı.',true)}}
clicks=0;lastClick=0;
}
// A normal mouse click, touch tap and double-click each produce click events.
// One single listener prevents mobile click + touchend from being counted twice.
el.addEventListener('click',activate);
el.addEventListener('keydown',ev=>{
if(el.isContentEditable){if(ev.key==='Enter'){ev.preventDefault();el.blur()}else if(ev.key==='Escape'){ev.preventDefault();finish(true);el.blur()}return;}
if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();activate()}
});
el.addEventListener('blur',()=>finish());
}
load();setupLogo();setupOrganization();resetForm(true);renderAll();
byId('recordForm').addEventListener('submit',ev=>ev.preventDefault());
byId('deleteBtn').addEventListener('click',deleteRecord);
byId('recordForm').addEventListener('input',ev=>{if(ev.target.id==='notes')autoGrow();updateLive();autoSave()});
byId('recordForm').addEventListener('change',()=>{updateLive();autoSave()});
byId('recordsBody').addEventListener('click',ev=>{const blank=ev.target.closest('[data-empty]');if(blank){resetForm();if(window.innerWidth<850)byId('editorPanel').scrollIntoView({behavior:'smooth'});return;}
const b=ev.target.closest('[data-edit]');if(b)openRecord(b.dataset.edit)});
byId('search').addEventListener('input',renderTable);
byId('emptyFormBtn').addEventListener('click',()=>{resetForm();if(window.innerWidth<850)byId('editorPanel').scrollIntoView({behavior:'smooth'})});
byId('excelOutBtn').addEventListener('click',exportExcel);byId('excelInBtn').addEventListener('click',()=>byId('excelFile').click());
byId('excelFile').addEventListener('change',ev=>{if(ev.target.files[0])importExcel(ev.target.files[0]);ev.target.value=''})
byId('printBtn').addEventListener('click',()=>{buildPrint();window.print()});window.addEventListener('beforeprint',buildPrint);
setInterval(()=>{renderAll()},60*1000);
})();
</script>
</body>
</html>




