Passionate network & system professional with hands-on expertise in enterprise networking, MikroTik, Cisco, Red Hat Enterprise Linux, and Windows Server infrastructure — ready to build, secure, and scale your IT operations.
Pick a photo from your device. It is resized, saved to your downloads as photo-name.jpg, and the path /images/photo-name.jpg is filled in above. Upload that file into your site's images folder.
⇅ Reorder Posts
Drag rows or use the arrows to set the display order. The first ones shown here appear on the homepage.
Pick a photo from your device. It is resized, saved to your downloads as project-name.jpg, and the path /projects/project-name.jpg is filled in above. Upload that file into your site's projects folder.
⇅ Reorder Projects
Drag rows or use the arrows to set the display order. The first ones shown here appear on the homepage.
Pick a photo from your device. It is resized, saved to your downloads as certificate-name.jpg, and the path /certificates/certificate-name.jpg is filled in above. Upload that file into your site's certificates folder.
⇅ Reorder Certifications
Drag rows or use the arrows to set the display order. The first ones shown here appear on the homepage.
// Let's connect
Ready to work together?
I'm ready to contribute my expertise in network administration and IT infrastructure to meaningful projects. Feel free to reach out — I'd love to collaborate with you.
Authentication -> Users.\n allow write: if request.auth != null\n && request.auth.uid == 'PASTE_ADMIN_UID_HERE'\n && docId in ['mr_projects_v1', 'mr_certs_v1', 'mr_blog_posts', 'mr_skills_v1']\n && request.resource.data.keys().hasOnly(['items', 'updatedAt'])\n && request.resource.data.items is list\n && request.resource.data.items.size() <= 500;\n }\n\n // Everything else is locked.\n match /{document=**} {\n allow read, write: if false;\n }\n }\n}\n",
"robots.txt": "User-agent: *\nAllow: /\n\nSitemap: https://mahfuj.bd/sitemap.xml\n",
"sitemap.xml": "\n\n \n https://mahfuj.bd/<\/loc>\n 2026-10-23<\/lastmod>\n weekly<\/changefreq>\n 1.0<\/priority>\n <\/url>\n \n https://mahfuj.bd/blog/<\/loc>\n 2026-10-23<\/lastmod>\n weekly<\/changefreq>\n 0.8<\/priority>\n <\/url>\n \n https://mahfuj.bd/projects/<\/loc>\n 2026-10-23<\/lastmod>\n monthly<\/changefreq>\n 0.8<\/priority>\n <\/url>\n \n https://mahfuj.bd/certificates/<\/loc>\n 2026-10-23<\/lastmod>\n monthly<\/changefreq>\n 0.7<\/priority>\n <\/url>\n \n https://mahfuj.bd/tools<\/loc>\n 2026-10-26<\/lastmod>\n monthly<\/changefreq>\n 0.7<\/priority>\n <\/url>\n \n https://mahfuj.bd/speed<\/loc>\n 2026-10-26<\/lastmod>\n monthly<\/changefreq>\n 0.7<\/priority>\n <\/url>\n \n https://mahfuj.bd/subnet<\/loc>\n 2026-10-26<\/lastmod>\n monthly<\/changefreq>\n 0.7<\/priority>\n <\/url>\n<\/urlset>\n",
"desktop-fit.css": "/* desktop-fit.css — load LAST, before <\/head>. Mobile/tablet (<=1100px) untouched.\n\n One consistent desktop look: the design width is 1440px. Every screen from\n 1101px up (14\" laptops, desktops, 4K) shows the SAME layout, scaled like\n browser zoom to fill the screen: zoom = screen width / 1440.\n e.g. 1366px -> 0.94, 1536px -> 1.06, 1920px -> 1.33, 2560px -> 1.77 */\n\n@media (min-width:1101px){\n /* same layout rules the site applies on >=1440px, so 14\" matches big screens */\n nav,.hero-inner,.section-header,.section-header-row,.skills-grid,\n .projects-grid,.certs-grid,.blog-grid,.see-more-wrap,.blog-empty,\n .contact-inner,.footer-inner,.list-page-head,.list-foot{max-width:100%}\n .hero-inner{grid-template-columns:1fr 620px;gap:64px}\n .hero-desc,.hero-title{max-width:100%}\n .hero-visual{margin-top:28px!important}\n\n html{zoom:var(--z)}\n /* viewport units are not zoomed, so divide by the zoom to still fill the screen */\n .hero{min-height:calc(100vh / var(--z));min-height:calc(100svh / var(--z))}\n body{min-height:calc(100vh / var(--z))} /* tools page: keeps footer at the bottom */\n #list-page{min-height:calc(100vh / var(--z))}\n .card-read-overlay{height:calc(100dvh / var(--z))}\n /* admin add/edit modals: vh is multiplied by the zoom too, so a 90vh box grew taller than the\n screen at zoom>1 (cut off, no scroll). Divide by the zoom so it always fits and scrolls. */\n .modal-overlay{overflow-y:auto;overscroll-behavior:contain}\n .modal-box{max-height:calc(90vh / var(--z));max-height:calc(90dvh / var(--z));overscroll-behavior:contain}\n /* custom cursor works in real screen pixels, so cancel the zoom for it */\n #cms-toast{max-width:calc((100vw - 40px) / var(--z))}\n #cursor,#cursor-ring{zoom:calc(1 / var(--z))}\n}\n\n:root{--z:1}\n@media (min-width:1101px){:root{--z:0.76}}\n@media (min-width:1120px){:root{--z:0.77}}\n@media (min-width:1160px){:root{--z:0.8}}\n@media (min-width:1200px){:root{--z:0.83}}\n@media (min-width:1240px){:root{--z:0.86}}\n@media (min-width:1280px){:root{--z:0.88}}\n@media (min-width:1320px){:root{--z:0.91}}\n@media (min-width:1360px){:root{--z:0.94}}\n@media (min-width:1400px){:root{--z:0.97}}\n@media (min-width:1440px){:root{--z:1.0}}\n@media (min-width:1480px){:root{--z:1.02}}\n@media (min-width:1520px){:root{--z:1.05}}\n@media (min-width:1560px){:root{--z:1.08}}\n@media (min-width:1600px){:root{--z:1.11}}\n@media (min-width:1640px){:root{--z:1.13}}\n@media (min-width:1680px){:root{--z:1.16}}\n@media (min-width:1720px){:root{--z:1.19}}\n@media (min-width:1760px){:root{--z:1.22}}\n@media (min-width:1800px){:root{--z:1.25}}\n@media (min-width:1840px){:root{--z:1.27}}\n@media (min-width:1880px){:root{--z:1.3}}\n@media (min-width:1920px){:root{--z:1.33}}\n@media (min-width:1960px){:root{--z:1.36}}\n@media (min-width:2000px){:root{--z:1.38}}\n@media (min-width:2040px){:root{--z:1.41}}\n@media (min-width:2080px){:root{--z:1.44}}\n@media (min-width:2120px){:root{--z:1.47}}\n@media (min-width:2160px){:root{--z:1.5}}\n@media (min-width:2200px){:root{--z:1.52}}\n@media (min-width:2240px){:root{--z:1.55}}\n@media (min-width:2280px){:root{--z:1.58}}\n@media (min-width:2320px){:root{--z:1.61}}\n@media (min-width:2360px){:root{--z:1.63}}\n@media (min-width:2400px){:root{--z:1.66}}\n@media (min-width:2440px){:root{--z:1.69}}\n@media (min-width:2480px){:root{--z:1.72}}\n@media (min-width:2520px){:root{--z:1.75}}\n@media (min-width:2560px){:root{--z:1.77}}\n@media (min-width:2600px){:root{--z:1.8}}\n@media (min-width:2640px){:root{--z:1.83}}\n@media (min-width:2680px){:root{--z:1.86}}\n@media (min-width:2720px){:root{--z:1.88}}\n@media (min-width:2760px){:root{--z:1.91}}\n@media (min-width:2800px){:root{--z:1.94}}\n@media (min-width:2840px){:root{--z:1.97}}\n@media (min-width:2880px){:root{--z:2.0}}\n@media (min-width:2920px){:root{--z:2.02}}\n@media (min-width:2960px){:root{--z:2.05}}\n@media (min-width:3000px){:root{--z:2.08}}\n@media (min-width:3040px){:root{--z:2.11}}\n@media (min-width:3080px){:root{--z:2.13}}\n@media (min-width:3120px){:root{--z:2.16}}\n@media (min-width:3160px){:root{--z:2.19}}\n@media (min-width:3200px){:root{--z:2.22}}\n@media (min-width:3240px){:root{--z:2.25}}\n@media (min-width:3280px){:root{--z:2.27}}\n@media (min-width:3320px){:root{--z:2.3}}\n@media (min-width:3360px){:root{--z:2.33}}\n@media (min-width:3400px){:root{--z:2.36}}\n@media (min-width:3440px){:root{--z:2.38}}\n@media (min-width:3480px){:root{--z:2.41}}\n@media (min-width:3520px){:root{--z:2.44}}\n@media (min-width:3560px){:root{--z:2.47}}\n@media (min-width:3600px){:root{--z:2.5}}\n@media (min-width:3640px){:root{--z:2.52}}\n@media (min-width:3680px){:root{--z:2.55}}\n@media (min-width:3720px){:root{--z:2.58}}\n@media (min-width:3760px){:root{--z:2.61}}\n@media (min-width:3800px){:root{--z:2.63}}\n@media (min-width:3840px){:root{--z:2.66}}\n@media (min-width:3880px){:root{--z:2.69}}\n@media (min-width:3920px){:root{--z:2.72}}\n@media (min-width:3960px){:root{--z:2.75}}\n@media (min-width:4000px){:root{--z:2.77}}\n\n/* images never overflow their container (max-width only, so sizes set elsewhere are untouched) */\nimg{max-width:100%}\n",
"desktop-fit.js": "/* desktop-fit.js — sets the smooth desktop zoom (--z) for screens >= 1101px.\n Same 1440px-wide design everywhere: zoom = width/1440, but never so large\n that the page gets shorter than ~690px (protects ultrawide / short windows).\n Without JS, the stepped values in desktop-fit.css are used. */\n(function(){\n var root=document.documentElement;\n function fit(){\n var w=window.innerWidth,h=window.innerHeight;\n if(w<1101){root.style.removeProperty('--z');return;}\n var z=Math.min(w/1440,h/690);\n z=Math.max(0.76,Math.round(z*100)/100);\n root.style.setProperty('--z',z);\n }\n fit();\n window.addEventListener('resize',fit);\n})();\n",
"btn-fx.css": "/* btn-fx.css — shared button motion for every page. Load LAST (after desktop-fit.css).\n Motion only: sizes, colours and layout are untouched.\n Modelled on the Network Tools buttons: smooth hover lift, press-in, click ripple (see btn-fx.js).\n\n It also settles older per-page rules that disagreed with each other:\n - transition:all / transition:transform only (shadow and colour snapped) -> one shared transition list\n - hover lifts of -1px, -2px and -3px, and scale(1.03) -> one -2px lift\n - hover glows that were weaker than the resting shadow -> one stronger glow\n - no press feedback on most buttons -> scale(.97) (.92 for icon buttons)\n transform / box-shadow / transition use !important on purpose so those older rules cannot win. */\n\n:is(\n button:not(.copy),\n input[type=button],\n input[type=submit],\n [role=button],\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .res-toggle,\n .list-back,\n .modal-close,\n .sc-x,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn,\n .reorder-move-btn,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .hamburger\n){\n -webkit-tap-highlight-color:transparent;\n transition:transform .22s cubic-bezier(.2,.8,.2,1),box-shadow .25s ease,background-color .2s ease,color .2s ease,border-color .2s ease,filter .2s ease,opacity .2s ease!important;\n}\n.fx-rel{position:relative}\n\n/* hover: lift (pointer devices only, so touch screens never get a stuck hover) */\n@media (hover:hover){\n :is(\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn\n ):not(:disabled):not([aria-disabled=\"true\"]):hover{transform:translateY(-2px)!important}\n :is(\n .fx-btn.p,\n .btn-primary,\n .btn-send,\n .btn-modal-primary,\n .btn-add-item,\n .nav-cta,\n .pwa-install-btn\n ):not(:disabled):not([aria-disabled=\"true\"]):hover{box-shadow:0 14px 32px rgba(29,78,216,.38)!important;filter:brightness(1.06)}\n}\n\n/* press */\n:is(\n button:not(.copy),\n input[type=button],\n input[type=submit],\n [role=button],\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .res-toggle,\n .list-back,\n .modal-close,\n .sc-x,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn,\n .reorder-move-btn,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .hamburger\n):not(:disabled):not([aria-disabled=\"true\"]):active{transform:scale(.97)!important}\n:is(\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .hamburger,\n .modal-close,\n .sc-x,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .reorder-move-btn\n):not(:disabled):not([aria-disabled=\"true\"]):active{transform:scale(.92)!important}\n\n/* ripple: a clipped layer inside the button, so the button's own overflow and layout never change */\n.fx-rw{position:absolute;inset:0;display:block;border-radius:inherit;overflow:hidden;pointer-events:none}\n.fx-rip{position:absolute;display:block;border-radius:50%;background:currentColor;opacity:.28;transform:scale(0);animation:fxrip .6s ease-out forwards}\n@keyframes fxrip{to{transform:scale(1);opacity:0}}\n\n@media (prefers-reduced-motion:reduce){\n :is(\n button:not(.copy),\n input[type=button],\n input[type=submit],\n [role=button],\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .res-toggle,\n .list-back,\n .modal-close,\n .sc-x,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn,\n .reorder-move-btn,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .hamburger\n ){transition-duration:.01ms!important}\n :is(\n button:not(.copy),\n input[type=button],\n input[type=submit],\n [role=button],\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .res-toggle,\n .list-back,\n .modal-close,\n .sc-x,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn,\n .reorder-move-btn,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .hamburger\n ):hover,:is(\n button:not(.copy),\n input[type=button],\n input[type=submit],\n [role=button],\n .fx-btn,\n .btn-primary,\n .btn-outline,\n .btn-see-more,\n .btn-share,\n .btn-send,\n .btn-modal-primary,\n .btn-modal-secondary,\n .btn-add-item,\n .btn-edit,\n .btn-danger,\n .btn-publish,\n .btn-skill-edit,\n .btn-skill-delete,\n .btn-skill-publish,\n .nav-cta,\n .nav-cta-mobile,\n .nav-social-icon,\n .footer-social,\n .social-btn,\n .mob-social,\n .speed-btn,\n .panel-btn,\n .mini-btn,\n .res-toggle,\n .list-back,\n .modal-close,\n .sc-x,\n .pwa-install-btn,\n .pwa-dismiss-btn,\n .img-pick-btn,\n .img-clear-btn,\n .reorder-move-btn,\n #theme-toggle,\n #blog-admin-fab,\n #export-data-fab,\n #admin-logout-fab,\n #seo-check-fab,\n .hamburger\n ):active{transform:none!important}\n .fx-rw{display:none}\n}\n",
"btn-fx.js": "/* btn-fx.js — click ripple for every button-like element (styles in btn-fx.css).\n Buttons that already run their own ripple (.panel-btn, .mini-btn, the speed-test buttons) are left alone,\n so nothing ripples twice. Works with the desktop zoom (--z) the same way those buttons do. */\n(function () {\n if (window.__btnFx) return; window.__btnFx = 1;\n var SEL = 'button:not(.copy), [role=button], .fx-btn, .btn-primary, .btn-outline, .btn-see-more, .btn-share, .btn-send, .btn-modal-primary, .btn-modal-secondary, .btn-add-item, .btn-edit, .btn-danger, .btn-publish, .btn-skill-edit, .btn-skill-delete, .btn-skill-publish, .nav-cta, .nav-cta-mobile, .nav-social-icon, .footer-social, .social-btn, .mob-social, .speed-btn, .panel-btn, .mini-btn, .res-toggle, .list-back, .modal-close, .sc-x, .pwa-install-btn, .pwa-dismiss-btn, .img-pick-btn, .img-clear-btn, .reorder-move-btn, #theme-toggle, #blog-admin-fab, #export-data-fab, #admin-logout-fab, #seo-check-fab, .hamburger';\n var OWN = '.panel-btn, .mini-btn, .sc-acts .speed-btn, #speed-share, #speed-start';\n var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;\n\n function usable(b) {\n return b && !b.disabled && b.getAttribute('aria-disabled') !== 'true' && !b.matches(OWN) && b.offsetWidth > 0;\n }\n function ripple(b, x, y) {\n var r = b.getBoundingClientRect(), z = b.offsetWidth ? r.width / b.offsetWidth : 1;\n var w = r.width / z, h = r.height / z, sz = Math.max(w, h) * 1.6;\n var cx = x == null ? w / 2 : (x - r.left) / z, cy = y == null ? h / 2 : (y - r.top) / z;\n if (getComputedStyle(b).position === 'static') b.classList.add('fx-rel');\n var wrap = document.createElement('fx-r'); wrap.className = 'fx-rw';\n var rip = document.createElement('fx-r'); rip.className = 'fx-rip';\n rip.style.cssText = 'width:' + sz + 'px;height:' + sz + 'px;left:' + (cx - sz / 2) + 'px;top:' + (cy - sz / 2) + 'px';\n wrap.appendChild(rip); b.appendChild(wrap);\n setTimeout(function () { wrap.remove(); }, 700);\n }\n\n document.addEventListener('pointerdown', function (e) {\n if (e.button > 0 || (mq && mq.matches)) return;\n var b = e.target.closest && e.target.closest(SEL);\n if (!usable(b)) return;\n var x = e.clientX, y = e.clientY;\n // wait one tick: if the page's own handler already added a ripple, don't add a second one\n setTimeout(function () { if (!b.querySelector(':scope > .rip')) ripple(b, x, y); }, 0);\n });\n\n // keyboard (Enter / Space) activation: ripple from the centre\n document.addEventListener('click', function (e) {\n if (!e.isTrusted || e.detail !== 0 || (mq && mq.matches)) return;\n var b = e.target.closest && e.target.closest(SEL);\n if (usable(b)) ripple(b);\n });\n})();\n"
};
const SITE_ASSET_EXT = 'jpe?g|png|gif|webp|avif|svg|ico|bmp|pdf|mp4|webm|woff2?|css|js';
function _loadJSZip() {
if (window.JSZip) return Promise.resolve(window.JSZip);
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
s.onload = () => window.JSZip ? resolve(window.JSZip) : reject(new Error('ZIP library failed to start'));
s.onerror = () => reject(new Error('Could not load the ZIP library — check your internet connection'));
document.head.appendChild(s);
});
}
function _pageZipPath(p) {
p = p.split('#')[0].split('?')[0];
if (p.endsWith('/')) return p.slice(1) + 'index.html';
const last = p.split('/').pop();
return last.indexOf('.') === -1 ? p.slice(1) + '/index.html' : p.slice(1);
}
function _normPath(p) {
p = (p || '').split('#')[0].split('?')[0];
if (!p.startsWith('/')) p = '/' + p;
return p;
}
async function _fetchSiteText(path, allow404) {
const res = await fetch(path + (path.indexOf('?') === -1 ? '?' : '&') + '_export=' + Date.now(), { cache: 'no-store' });
if (!res.ok && !(allow404 && res.status === 404)) throw new Error('HTTP ' + res.status);
return await res.text();
}
async function _fetchSiteBinary(path) {
const res = await fetch(path, { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const type = (res.headers.get('content-type') || '').toLowerCase();
if (type.indexOf('text/html') !== -1) throw new Error('not a real file'); // host fallback page, not the asset
return await res.arrayBuffer();
}
// The 3 list pages (blog/projects/certificates) differ from the home page only in a few
// head/nav lines. These stored edits (base64) let the export rebuild them from the home
// page whenever the server answers those URLs with the wrong page.
const SITE_REBUILD = "eyJibG9nIjogW1siPGh0bWwgbGFuZz1cImVuXCI+IiwgIjxodG1sIGxhbmc9XCJlblwiIGRhdGEtcGFnZT1cImJsb2dcIj4iXSwgWyIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJNYWhmdWp1ciBSYWhtYW4gLSBDQ05BLCBNVENOQSwgTVRDUkUsIE1UQ1NFIGNlcnRpZmllZCBOZXR3b3JrICZhbXA7IFN5c3RlbSBBZG1pbmlzdHJhdG9yIGluIERoYWthLCBCYW5nbGFkZXNoLiBDaXNjbywgTWlrcm9UaWssIFJIRUwsIFdpbmRvd3MgU2VydmVyIGV4cGVydC5cIiAvPiIsICIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJBbGwgYmxvZyBwb3N0cyBieSBNYWhmdWp1ciBSYWhtYW4gb24gbmV0d29ya2luZywgTWlrcm9UaWssIENpc2NvLCBMaW51eCBhbmQgV2luZG93cyBTZXJ2ZXIuXCIgLz4iXSwgWyIgIDxsaW5rIHJlbD1cImNhbm9uaWNhbFwiIGhyZWY9XCJodHRwczovL21haGZ1ai5iZC9cIiAvPlxuICA8bWV0YSBwcm9wZXJ0eT1cIm9nOnRpdGxlXCIgY29udGVudD1cIk1haGZ1anVyIFJhaG1hbiB8IE5ldHdvcmsgJmFtcDsgU3lzdGVtIEFkbWluaXN0cmF0aW9uXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzpkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJQYXNzaW9uYXRlIG5ldHdvcmsgJmFtcDsgc3lzdGVtIHByb2Zlc3Npb25hbCB3aXRoIGhhbmRzLW9uIGV4cGVydGlzZSBpbiBlbnRlcnByaXNlIG5ldHdvcmtpbmcsIE1pa3JvVGlrLCBDaXNjbywgUkhFTCwgYW5kIFdpbmRvd3MgU2VydmVyLlwiIC8+IiwgIiAgPGxpbmsgcmVsPVwiY2Fub25pY2FsXCIgaHJlZj1cImh0dHBzOi8vbWFoZnVqLmJkL2Jsb2cvXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzp0aXRsZVwiIGNvbnRlbnQ9XCJCbG9nIHwgTWFoZnVqdXIgUmFobWFuXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzpkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJBbGwgYmxvZyBwb3N0cyBieSBNYWhmdWp1ciBSYWhtYW4gb24gbmV0d29ya2luZywgTWlrcm9UaWssIENpc2NvLCBMaW51eCBhbmQgV2luZG93cyBTZXJ2ZXIuXCIgLz4iXSwgWyIgIDxtZXRhIHByb3BlcnR5PVwib2c6dXJsXCIgY29udGVudD1cImh0dHBzOi8vbWFoZnVqLmJkL1wiIC8+IiwgIiAgPG1ldGEgcHJvcGVydHk9XCJvZzp1cmxcIiBjb250ZW50PVwiaHR0cHM6Ly9tYWhmdWouYmQvYmxvZy9cIiAvPiJdLCBbIiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6dGl0bGVcIiBjb250ZW50PVwiTWFoZnVqdXIgUmFobWFuIHwgTmV0d29yayAmYW1wOyBTeXN0ZW0gQWRtaW5pc3RyYXRpb25cIiAvPlxuICA8bWV0YSBuYW1lPVwidHdpdHRlcjpkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJQYXNzaW9uYXRlIG5ldHdvcmsgJmFtcDsgc3lzdGVtIHByb2Zlc3Npb25hbCB3aXRoIGhhbmRzLW9uIGV4cGVydGlzZSBpbiBlbnRlcnByaXNlIG5ldHdvcmtpbmcsIE1pa3JvVGlrLCBDaXNjbywgUkhFTCwgYW5kIFdpbmRvd3MgU2VydmVyLlwiIC8+IiwgIiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6dGl0bGVcIiBjb250ZW50PVwiQmxvZyB8IE1haGZ1anVyIFJhaG1hblwiIC8+XG4gIDxtZXRhIG5hbWU9XCJ0d2l0dGVyOmRlc2NyaXB0aW9uXCIgY29udGVudD1cIkFsbCBibG9nIHBvc3RzIGJ5IE1haGZ1anVyIFJhaG1hbiBvbiBuZXR3b3JraW5nLCBNaWtyb1RpaywgQ2lzY28sIExpbnV4IGFuZCBXaW5kb3dzIFNlcnZlci5cIiAvPiJdLCBbIiAgPHRpdGxlPk1haGZ1anVyIFJhaG1hbiB8IE5ldHdvcmsgJmFtcDsgU3lzdGVtIEFkbWluaXN0cmF0aW9uPC90aXRsZT4iLCAiICA8dGl0bGU+QmxvZyB8IE1haGZ1anVyIFJhaG1hbjwvdGl0bGU+Il0sIFsiICAgIFwicHVibGlzaGVyXCI6IHsgXCJAdHlwZVwiOiBcIlBlcnNvblwiLCBcIm5hbWVcIjogXCJNYWhmdWp1ciBSYWhtYW5cIiwgXCJ1cmxcIjogXCJodHRwczovL21haGZ1ai5iZC9cIiB9IiwgIiAgICBcInB1Ymxpc2hlclwiOiB7IFwiQHR5cGVcIjogXCJQZXJzb25cIiwgXCJuYW1lXCI6IFwiTWFoZnVqdXIgUmFobWFuXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgfVxuICB9XG4gIDwvc2NyaXB0PlxuICA8c2NyaXB0IHR5cGU9XCJhcHBsaWNhdGlvbi9sZCtqc29uXCI+XG4gIHtcbiAgICBcIkBjb250ZXh0XCI6IFwiaHR0cHM6Ly9zY2hlbWEub3JnXCIsXG4gICAgXCJAZ3JhcGhcIjogW1xuICAgICAgeyBcIkB0eXBlXCI6IFwiQnJlYWRjcnVtYkxpc3RcIiwgXCJpdGVtTGlzdEVsZW1lbnRcIjogW1xuICAgICAgICB7IFwiQHR5cGVcIjogXCJMaXN0SXRlbVwiLCBcInBvc2l0aW9uXCI6IDEsIFwibmFtZVwiOiBcIkhvbWVcIiwgXCJpdGVtXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgfSxcbiAgICAgICAgeyBcIkB0eXBlXCI6IFwiTGlzdEl0ZW1cIiwgXCJwb3NpdGlvblwiOiAyLCBcIm5hbWVcIjogXCJCbG9nXCIsIFwiaXRlbVwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL2Jsb2cvXCIgfSBdIH0sXG4gICAgICB7IFwiQHR5cGVcIjogXCJDb2xsZWN0aW9uUGFnZVwiLCBcIm5hbWVcIjogXCJCbG9nIHwgTWFoZnVqdXIgUmFobWFuXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvYmxvZy9cIiwgXCJkZXNjcmlwdGlvblwiOiBcIkFsbCBibG9nIHBvc3RzIGJ5IE1haGZ1anVyIFJhaG1hbiBvbiBuZXR3b3JraW5nLCBNaWtyb1RpaywgQ2lzY28sIExpbnV4IGFuZCBXaW5kb3dzIFNlcnZlci5cIixcbiAgICAgICAgXCJpc1BhcnRPZlwiOiB7IFwiQHR5cGVcIjogXCJXZWJTaXRlXCIsIFwibmFtZVwiOiBcIk1haGZ1anVyIFJhaG1hblwiLCBcInVybFwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH0sXG4gICAgICAgIFwiYXV0aG9yXCI6IHsgXCJAdHlwZVwiOiBcIlBlcnNvblwiLCBcIm5hbWVcIjogXCJNYWhmdWp1ciBSYWhtYW5cIiwgXCJ1cmxcIjogXCJodHRwczovL21haGZ1ai5iZC9cIiB9IH1cbiAgICBdIl0sIFsiPC9oZWFkPlxuPGJvZHk+IiwgIjwvaGVhZD5cbjxib2R5IGNsYXNzPVwibGlzdC1tb2RlXCI+Il0sIFsiICA8YSBocmVmPVwiI2hvbWVcIiBjbGFzcz1cInNraXAtbGlua1wiPlNraXAgdG8gY29udGVudDwvYT4iLCAiICA8YSBocmVmPVwiLyNob21lXCIgY2xhc3M9XCJza2lwLWxpbmtcIj5Ta2lwIHRvIGNvbnRlbnQ8L2E+Il0sIFsiICAgICAgPGEgaHJlZj1cIiNob21lXCIgY2xhc3M9XCJsb2dvXCIgYXJpYS1sYWJlbD1cIk1haGZ1anVyIFJhaG1hbiDigJQgSG9tZVwiPiIsICIgICAgICA8YSBocmVmPVwiLyNob21lXCIgY2xhc3M9XCJsb2dvXCIgYXJpYS1sYWJlbD1cIk1haGZ1anVyIFJhaG1hbiDigJQgSG9tZVwiPiJdLCBbIiAgICAgICAgPGxpPjxhIGhyZWY9XCIjaG9tZVwiPkhvbWU8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjc2tpbGxzXCI+U2tpbGxzPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI3Byb2plY3RzXCI+UHJvamVjdHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjY2VydGlmaWNhdGlvbnNcIj5DZXJ0aWZpY2F0aW9uczwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiNibG9nXCI+QmxvZzwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiN0b29sc1wiPlRvb2xzPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI2NvbnRhY3RcIj5Db250YWN0PC9hPjwvbGk+IiwgIiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI2hvbWVcIj5Ib21lPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiLyNza2lsbHNcIj5Ta2lsbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI3Byb2plY3RzXCI+UHJvamVjdHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI2NlcnRpZmljYXRpb25zXCI+Q2VydGlmaWNhdGlvbnM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI2Jsb2dcIj5CbG9nPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiLyN0b29sc1wiPlRvb2xzPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiLyNjb250YWN0XCI+Q29udGFjdDwvYT48L2xpPiJdLCBbIiAgICAgICAgPGxpPjxhIGhyZWY9XCIjY29udGFjdFwiIGNsYXNzPVwibmF2LWN0YVwiPkhJUkUgTUU8L2E+PC9saT4iLCAiICAgICAgICA8bGk+PGEgaHJlZj1cIi8jY29udGFjdFwiIGNsYXNzPVwibmF2LWN0YVwiPkhJUkUgTUU8L2E+PC9saT4iXSwgWyIgICAgICA8bGk+PGEgaHJlZj1cIiNob21lXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfj6A8L3NwYW4+IEhvbWUgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI3NraWxsc1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7imqE8L3NwYW4+IFNraWxscyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjcHJvamVjdHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+Upzwvc3Bhbj4gUHJvamVjdHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI2NlcnRpZmljYXRpb25zXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfjpM8L3NwYW4+IENlcnRpZmljYXRpb25zIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIiNibG9nXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfk508L3NwYW4+IEJsb2cgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI3Rvb2xzXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfm6A8L3NwYW4+IFRvb2xzIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIiNjb250YWN0XCIgY2xhc3M9XCJtb2ItbGluayBuYXYtY3RhLW1vYmlsZVwiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKciTwvc3Bhbj4gSElSRSBNRTwvYT48L2xpPiIsICIgICAgICA8bGk+PGEgaHJlZj1cIi8jaG9tZVwiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn4+gPC9zcGFuPiBIb21lIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jc2tpbGxzXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKaoTwvc3Bhbj4gU2tpbGxzIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jcHJvamVjdHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+Upzwvc3Bhbj4gUHJvamVjdHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNjZXJ0aWZpY2F0aW9uc1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn46TPC9zcGFuPiBDZXJ0aWZpY2F0aW9ucyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIvI2Jsb2dcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+TnTwvc3Bhbj4gQmxvZyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIvI3Rvb2xzXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfm6A8L3NwYW4+IFRvb2xzIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jY29udGFjdFwiIGNsYXNzPVwibW9iLWxpbmsgbmF2LWN0YS1tb2JpbGVcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7inIk8L3NwYW4+IEhJUkUgTUU8L2E+PC9saT4iXSwgWyIgICAgICAgIHsgaWQ6ICcxNzgyMTU1MDQ5NTQ3cGJibmYnLCBvcmRlcjogMywgaWNvbjogJ/Cfqp8nLCB0aXRsZTogJ1dpbmRvd3MgU2VydmVyIEFkbWluaXN0cmF0aW9uJywgZGVzYzogYOKAoiBXaW5kb3dzIFNlcnZlciBJbnN0YWxsYXRpb24sIERlcGxveW1lbnQsIFVwZ3JhZGUsIGFuZCBJbml0aWFsIENvbmZpZ3VyYXRpb24iLCAiICAgICAgICB7IGlkOiAnMTc4MjE1NTA0OTU0N3BiYm5mJywgb3JkZXI6IDMsIGljb246ICfwn5al77iPJywgdGl0bGU6ICdXaW5kb3dzIFNlcnZlciBBZG1pbmlzdHJhdGlvbicsIGRlc2M6IGDigKIgV2luZG93cyBTZXJ2ZXIgSW5zdGFsbGF0aW9uLCBEZXBsb3ltZW50LCBVcGdyYWRlLCBhbmQgSW5pdGlhbCBDb25maWd1cmF0aW9uIl0sIFsiICAgIGZ1bmN0aW9uIGN1cnJlbnRMaXN0UGFnZSgpIHsiLCAiICAgIGZ1bmN0aW9uIGN1cnJlbnRMaXN0UGFnZSgpIHtcbiAgICAgIGNvbnN0IGZvcmNlZCA9IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5nZXRBdHRyaWJ1dGUoJ2RhdGEtcGFnZScpOyAgIC8vIHNldCBvbiBibG9nLyBhbmQgY2VydGlmaWNhdGVzLyBwYWdlc1xuICAgICAgaWYgKGZvcmNlZCAmJiBMSVNUX1BBR0VTW2ZvcmNlZF0pIHJldHVybiBmb3JjZWQ7Il0sIFsiICAgICAgY29uc3QgdXJsID0gY2ZnID8gX2hvbWVNZXRhLmNhbm9uaWNhbCArICc/cGFnZT0nICsga2V5IDogX2hvbWVNZXRhLmNhbm9uaWNhbDsiLCAiICAgICAgY29uc3QgdXJsID0gY2ZnID8gKFBBR0VfUEFUSFNba2V5XSA/IG5ldyBVUkwoX2hvbWVNZXRhLmNhbm9uaWNhbCwgbG9jYXRpb24uaHJlZikub3JpZ2luICsgUEFHRV9QQVRIU1trZXldIDogX2hvbWVNZXRhLmNhbm9uaWNhbCArICc/cGFnZT0nICsga2V5KSA6IF9ob21lTWV0YS5jYW5vbmljYWw7Il1dLCAicHJvamVjdHMiOiBbWyI8aHRtbCBsYW5nPVwiZW5cIj4iLCAiPGh0bWwgbGFuZz1cImVuXCIgZGF0YS1wYWdlPVwicHJvamVjdHNcIj4iXSwgWyIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJNYWhmdWp1ciBSYWhtYW4gLSBDQ05BLCBNVENOQSwgTVRDUkUsIE1UQ1NFIGNlcnRpZmllZCBOZXR3b3JrICZhbXA7IFN5c3RlbSBBZG1pbmlzdHJhdG9yIGluIERoYWthLCBCYW5nbGFkZXNoLiBDaXNjbywgTWlrcm9UaWssIFJIRUwsIFdpbmRvd3MgU2VydmVyIGV4cGVydC5cIiAvPiIsICIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJBbGwgcHJvamVjdHMgYnkgTWFoZnVqdXIgUmFobWFuIOKAlCBuZXR3b3JrIGFuZCBzeXN0ZW0gYWRtaW5pc3RyYXRpb24gd29yayBhY3Jvc3MgTWlrcm9UaWssIENpc2NvLCBSSEVMIGFuZCBXaW5kb3dzIFNlcnZlci5cIiAvPiJdLCBbIiAgPGxpbmsgcmVsPVwiY2Fub25pY2FsXCIgaHJlZj1cImh0dHBzOi8vbWFoZnVqLmJkL1wiIC8+XG4gIDxtZXRhIHByb3BlcnR5PVwib2c6dGl0bGVcIiBjb250ZW50PVwiTWFoZnVqdXIgUmFobWFuIHwgTmV0d29yayAmYW1wOyBTeXN0ZW0gQWRtaW5pc3RyYXRpb25cIiAvPlxuICA8bWV0YSBwcm9wZXJ0eT1cIm9nOmRlc2NyaXB0aW9uXCIgY29udGVudD1cIlBhc3Npb25hdGUgbmV0d29yayAmYW1wOyBzeXN0ZW0gcHJvZmVzc2lvbmFsIHdpdGggaGFuZHMtb24gZXhwZXJ0aXNlIGluIGVudGVycHJpc2UgbmV0d29ya2luZywgTWlrcm9UaWssIENpc2NvLCBSSEVMLCBhbmQgV2luZG93cyBTZXJ2ZXIuXCIgLz4iLCAiICA8bGluayByZWw9XCJjYW5vbmljYWxcIiBocmVmPVwiaHR0cHM6Ly9tYWhmdWouYmQvcHJvamVjdHMvXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzp0aXRsZVwiIGNvbnRlbnQ9XCJQcm9qZWN0cyB8IE1haGZ1anVyIFJhaG1hblwiIC8+XG4gIDxtZXRhIHByb3BlcnR5PVwib2c6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiQWxsIHByb2plY3RzIGJ5IE1haGZ1anVyIFJhaG1hbiDigJQgbmV0d29yayBhbmQgc3lzdGVtIGFkbWluaXN0cmF0aW9uIHdvcmsgYWNyb3NzIE1pa3JvVGlrLCBDaXNjbywgUkhFTCBhbmQgV2luZG93cyBTZXJ2ZXIuXCIgLz4iXSwgWyIgIDxtZXRhIHByb3BlcnR5PVwib2c6dXJsXCIgY29udGVudD1cImh0dHBzOi8vbWFoZnVqLmJkL1wiIC8+IiwgIiAgPG1ldGEgcHJvcGVydHk9XCJvZzp1cmxcIiBjb250ZW50PVwiaHR0cHM6Ly9tYWhmdWouYmQvcHJvamVjdHMvXCIgLz4iXSwgWyIgIDxtZXRhIG5hbWU9XCJ0d2l0dGVyOnRpdGxlXCIgY29udGVudD1cIk1haGZ1anVyIFJhaG1hbiB8IE5ldHdvcmsgJmFtcDsgU3lzdGVtIEFkbWluaXN0cmF0aW9uXCIgLz5cbiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiUGFzc2lvbmF0ZSBuZXR3b3JrICZhbXA7IHN5c3RlbSBwcm9mZXNzaW9uYWwgd2l0aCBoYW5kcy1vbiBleHBlcnRpc2UgaW4gZW50ZXJwcmlzZSBuZXR3b3JraW5nLCBNaWtyb1RpaywgQ2lzY28sIFJIRUwsIGFuZCBXaW5kb3dzIFNlcnZlci5cIiAvPiIsICIgIDxtZXRhIG5hbWU9XCJ0d2l0dGVyOnRpdGxlXCIgY29udGVudD1cIlByb2plY3RzIHwgTWFoZnVqdXIgUmFobWFuXCIgLz5cbiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiQWxsIHByb2plY3RzIGJ5IE1haGZ1anVyIFJhaG1hbiDigJQgbmV0d29yayBhbmQgc3lzdGVtIGFkbWluaXN0cmF0aW9uIHdvcmsgYWNyb3NzIE1pa3JvVGlrLCBDaXNjbywgUkhFTCBhbmQgV2luZG93cyBTZXJ2ZXIuXCIgLz4iXSwgWyIgIDx0aXRsZT5NYWhmdWp1ciBSYWhtYW4gfCBOZXR3b3JrICZhbXA7IFN5c3RlbSBBZG1pbmlzdHJhdGlvbjwvdGl0bGU+IiwgIiAgPHRpdGxlPlByb2plY3RzIHwgTWFoZnVqdXIgUmFobWFuPC90aXRsZT4iXSwgWyIgICAgXCJwdWJsaXNoZXJcIjogeyBcIkB0eXBlXCI6IFwiUGVyc29uXCIsIFwibmFtZVwiOiBcIk1haGZ1anVyIFJhaG1hblwiLCBcInVybFwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH0iLCAiICAgIFwicHVibGlzaGVyXCI6IHsgXCJAdHlwZVwiOiBcIlBlcnNvblwiLCBcIm5hbWVcIjogXCJNYWhmdWp1ciBSYWhtYW5cIiwgXCJ1cmxcIjogXCJodHRwczovL21haGZ1ai5iZC9cIiB9XG4gIH1cbiAgPC9zY3JpcHQ+XG4gIDxzY3JpcHQgdHlwZT1cImFwcGxpY2F0aW9uL2xkK2pzb25cIj5cbiAge1xuICAgIFwiQGNvbnRleHRcIjogXCJodHRwczovL3NjaGVtYS5vcmdcIixcbiAgICBcIkBncmFwaFwiOiBbXG4gICAgICB7IFwiQHR5cGVcIjogXCJCcmVhZGNydW1iTGlzdFwiLCBcIml0ZW1MaXN0RWxlbWVudFwiOiBbXG4gICAgICAgIHsgXCJAdHlwZVwiOiBcIkxpc3RJdGVtXCIsIFwicG9zaXRpb25cIjogMSwgXCJuYW1lXCI6IFwiSG9tZVwiLCBcIml0ZW1cIjogXCJodHRwczovL21haGZ1ai5iZC9cIiB9LFxuICAgICAgICB7IFwiQHR5cGVcIjogXCJMaXN0SXRlbVwiLCBcInBvc2l0aW9uXCI6IDIsIFwibmFtZVwiOiBcIlByb2plY3RzXCIsIFwiaXRlbVwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL3Byb2plY3RzL1wiIH0gXSB9LFxuICAgICAgeyBcIkB0eXBlXCI6IFwiQ29sbGVjdGlvblBhZ2VcIiwgXCJuYW1lXCI6IFwiUHJvamVjdHMgfCBNYWhmdWp1ciBSYWhtYW5cIiwgXCJ1cmxcIjogXCJodHRwczovL21haGZ1ai5iZC9wcm9qZWN0cy9cIiwgXCJkZXNjcmlwdGlvblwiOiBcIkFsbCBwcm9qZWN0cyBieSBNYWhmdWp1ciBSYWhtYW4g4oCUIG5ldHdvcmsgYW5kIHN5c3RlbSBhZG1pbmlzdHJhdGlvbiB3b3JrIGFjcm9zcyBNaWtyb1RpaywgQ2lzY28sIFJIRUwgYW5kIFdpbmRvd3MgU2VydmVyLlwiLFxuICAgICAgICBcImlzUGFydE9mXCI6IHsgXCJAdHlwZVwiOiBcIldlYlNpdGVcIiwgXCJuYW1lXCI6IFwiTWFoZnVqdXIgUmFobWFuXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgfSxcbiAgICAgICAgXCJhdXRob3JcIjogeyBcIkB0eXBlXCI6IFwiUGVyc29uXCIsIFwibmFtZVwiOiBcIk1haGZ1anVyIFJhaG1hblwiLCBcInVybFwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH0gfVxuICAgIF0iXSwgWyI8L2hlYWQ+XG48Ym9keT4iLCAiPC9oZWFkPlxuPGJvZHkgY2xhc3M9XCJsaXN0LW1vZGVcIj4iXSwgWyIgIDxhIGhyZWY9XCIjaG9tZVwiIGNsYXNzPVwic2tpcC1saW5rXCI+U2tpcCB0byBjb250ZW50PC9hPiIsICIgIDxhIGhyZWY9XCIvI2hvbWVcIiBjbGFzcz1cInNraXAtbGlua1wiPlNraXAgdG8gY29udGVudDwvYT4iXSwgWyIgICAgICA8YSBocmVmPVwiI2hvbWVcIiBjbGFzcz1cImxvZ29cIiBhcmlhLWxhYmVsPVwiTWFoZnVqdXIgUmFobWFuIOKAlCBIb21lXCI+IiwgIiAgICAgIDxhIGhyZWY9XCIvI2hvbWVcIiBjbGFzcz1cImxvZ29cIiBhcmlhLWxhYmVsPVwiTWFoZnVqdXIgUmFobWFuIOKAlCBIb21lXCI+Il0sIFsiICAgICAgICA8bGk+PGEgaHJlZj1cIiNob21lXCI+SG9tZTwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiNza2lsbHNcIj5Ta2lsbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjcHJvamVjdHNcIj5Qcm9qZWN0czwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiNjZXJ0aWZpY2F0aW9uc1wiPkNlcnRpZmljYXRpb25zPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI2Jsb2dcIj5CbG9nPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI3Rvb2xzXCI+VG9vbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjY29udGFjdFwiPkNvbnRhY3Q8L2E+PC9saT4iLCAiICAgICAgICA8bGk+PGEgaHJlZj1cIi8jaG9tZVwiPkhvbWU8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI3NraWxsc1wiPlNraWxsczwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jcHJvamVjdHNcIj5Qcm9qZWN0czwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jY2VydGlmaWNhdGlvbnNcIj5DZXJ0aWZpY2F0aW9uczwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jYmxvZ1wiPkJsb2c8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI3Rvb2xzXCI+VG9vbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI2NvbnRhY3RcIj5Db250YWN0PC9hPjwvbGk+Il0sIFsiICAgICAgICA8bGk+PGEgaHJlZj1cIiNjb250YWN0XCIgY2xhc3M9XCJuYXYtY3RhXCI+SElSRSBNRTwvYT48L2xpPiIsICIgICAgICAgIDxsaT48YSBocmVmPVwiLyNjb250YWN0XCIgY2xhc3M9XCJuYXYtY3RhXCI+SElSRSBNRTwvYT48L2xpPiJdLCBbIiAgICAgIDxsaT48YSBocmVmPVwiI2hvbWVcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+PoDwvc3Bhbj4gSG9tZSA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjc2tpbGxzXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKaoTwvc3Bhbj4gU2tpbGxzIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIiNwcm9qZWN0c1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5SnPC9zcGFuPiBQcm9qZWN0cyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjY2VydGlmaWNhdGlvbnNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+Okzwvc3Bhbj4gQ2VydGlmaWNhdGlvbnMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI2Jsb2dcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+TnTwvc3Bhbj4gQmxvZyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjdG9vbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+boDwvc3Bhbj4gVG9vbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI2NvbnRhY3RcIiBjbGFzcz1cIm1vYi1saW5rIG5hdi1jdGEtbW9iaWxlXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+4pyJPC9zcGFuPiBISVJFIE1FPC9hPjwvbGk+IiwgIiAgICAgIDxsaT48YSBocmVmPVwiLyNob21lXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfj6A8L3NwYW4+IEhvbWUgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNza2lsbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+4pqhPC9zcGFuPiBTa2lsbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNwcm9qZWN0c1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5SnPC9zcGFuPiBQcm9qZWN0cyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIvI2NlcnRpZmljYXRpb25zXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfjpM8L3NwYW4+IENlcnRpZmljYXRpb25zIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jYmxvZ1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5OdPC9zcGFuPiBCbG9nIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jdG9vbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+boDwvc3Bhbj4gVG9vbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNjb250YWN0XCIgY2xhc3M9XCJtb2ItbGluayBuYXYtY3RhLW1vYmlsZVwiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKciTwvc3Bhbj4gSElSRSBNRTwvYT48L2xpPiJdLCBbIiAgICAgICAgeyBpZDogJzE3ODIxNTUwNDk1NDdwYmJuZicsIG9yZGVyOiAzLCBpY29uOiAn8J+qnycsIHRpdGxlOiAnV2luZG93cyBTZXJ2ZXIgQWRtaW5pc3RyYXRpb24nLCBkZXNjOiBg4oCiIFdpbmRvd3MgU2VydmVyIEluc3RhbGxhdGlvbiwgRGVwbG95bWVudCwgVXBncmFkZSwgYW5kIEluaXRpYWwgQ29uZmlndXJhdGlvbiIsICIgICAgICAgIHsgaWQ6ICcxNzgyMTU1MDQ5NTQ3cGJibmYnLCBvcmRlcjogMywgaWNvbjogJ/CflqXvuI8nLCB0aXRsZTogJ1dpbmRvd3MgU2VydmVyIEFkbWluaXN0cmF0aW9uJywgZGVzYzogYOKAoiBXaW5kb3dzIFNlcnZlciBJbnN0YWxsYXRpb24sIERlcGxveW1lbnQsIFVwZ3JhZGUsIGFuZCBJbml0aWFsIENvbmZpZ3VyYXRpb24iXSwgWyIgICAgZnVuY3Rpb24gY3VycmVudExpc3RQYWdlKCkgeyIsICIgICAgZnVuY3Rpb24gY3VycmVudExpc3RQYWdlKCkge1xuICAgICAgY29uc3QgZm9yY2VkID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmdldEF0dHJpYnV0ZSgnZGF0YS1wYWdlJyk7ICAgLy8gc2V0IG9uIGJsb2cvIGFuZCBjZXJ0aWZpY2F0ZXMvIHBhZ2VzXG4gICAgICBpZiAoZm9yY2VkICYmIExJU1RfUEFHRVNbZm9yY2VkXSkgcmV0dXJuIGZvcmNlZDsiXSwgWyIgICAgICBjb25zdCB1cmwgPSBjZmcgPyBfaG9tZU1ldGEuY2Fub25pY2FsICsgJz9wYWdlPScgKyBrZXkgOiBfaG9tZU1ldGEuY2Fub25pY2FsOyIsICIgICAgICBjb25zdCB1cmwgPSBjZmcgPyAoUEFHRV9QQVRIU1trZXldID8gbmV3IFVSTChfaG9tZU1ldGEuY2Fub25pY2FsLCBsb2NhdGlvbi5ocmVmKS5vcmlnaW4gKyBQQUdFX1BBVEhTW2tleV0gOiBfaG9tZU1ldGEuY2Fub25pY2FsICsgJz9wYWdlPScgKyBrZXkpIDogX2hvbWVNZXRhLmNhbm9uaWNhbDsiXV0sICJjZXJ0aWZpY2F0ZXMiOiBbWyI8aHRtbCBsYW5nPVwiZW5cIj4iLCAiPGh0bWwgbGFuZz1cImVuXCIgZGF0YS1wYWdlPVwiY2VydGlmaWNhdGlvbnNcIj4iXSwgWyIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJNYWhmdWp1ciBSYWhtYW4gLSBDQ05BLCBNVENOQSwgTVRDUkUsIE1UQ1NFIGNlcnRpZmllZCBOZXR3b3JrICZhbXA7IFN5c3RlbSBBZG1pbmlzdHJhdG9yIGluIERoYWthLCBCYW5nbGFkZXNoLiBDaXNjbywgTWlrcm9UaWssIFJIRUwsIFdpbmRvd3MgU2VydmVyIGV4cGVydC5cIiAvPiIsICIgIDxtZXRhIG5hbWU9XCJkZXNjcmlwdGlvblwiIGNvbnRlbnQ9XCJBbGwgY2VydGlmaWNhdGlvbnMgaGVsZCBieSBNYWhmdWp1ciBSYWhtYW4sIGluY2x1ZGluZyBDQ05BLCBNVENOQSwgTVRDUkUgYW5kIE1UQ1NFLlwiIC8+Il0sIFsiICA8bGluayByZWw9XCJjYW5vbmljYWxcIiBocmVmPVwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzp0aXRsZVwiIGNvbnRlbnQ9XCJNYWhmdWp1ciBSYWhtYW4gfCBOZXR3b3JrICZhbXA7IFN5c3RlbSBBZG1pbmlzdHJhdGlvblwiIC8+XG4gIDxtZXRhIHByb3BlcnR5PVwib2c6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiUGFzc2lvbmF0ZSBuZXR3b3JrICZhbXA7IHN5c3RlbSBwcm9mZXNzaW9uYWwgd2l0aCBoYW5kcy1vbiBleHBlcnRpc2UgaW4gZW50ZXJwcmlzZSBuZXR3b3JraW5nLCBNaWtyb1RpaywgQ2lzY28sIFJIRUwsIGFuZCBXaW5kb3dzIFNlcnZlci5cIiAvPiIsICIgIDxsaW5rIHJlbD1cImNhbm9uaWNhbFwiIGhyZWY9XCJodHRwczovL21haGZ1ai5iZC9jZXJ0aWZpY2F0ZXMvXCIgLz5cbiAgPG1ldGEgcHJvcGVydHk9XCJvZzp0aXRsZVwiIGNvbnRlbnQ9XCJDZXJ0aWZpY2F0aW9ucyB8IE1haGZ1anVyIFJhaG1hblwiIC8+XG4gIDxtZXRhIHByb3BlcnR5PVwib2c6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiQWxsIGNlcnRpZmljYXRpb25zIGhlbGQgYnkgTWFoZnVqdXIgUmFobWFuLCBpbmNsdWRpbmcgQ0NOQSwgTVRDTkEsIE1UQ1JFIGFuZCBNVENTRS5cIiAvPiJdLCBbIiAgPG1ldGEgcHJvcGVydHk9XCJvZzp1cmxcIiBjb250ZW50PVwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgLz4iLCAiICA8bWV0YSBwcm9wZXJ0eT1cIm9nOnVybFwiIGNvbnRlbnQ9XCJodHRwczovL21haGZ1ai5iZC9jZXJ0aWZpY2F0ZXMvXCIgLz4iXSwgWyIgIDxtZXRhIG5hbWU9XCJ0d2l0dGVyOnRpdGxlXCIgY29udGVudD1cIk1haGZ1anVyIFJhaG1hbiB8IE5ldHdvcmsgJmFtcDsgU3lzdGVtIEFkbWluaXN0cmF0aW9uXCIgLz5cbiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiUGFzc2lvbmF0ZSBuZXR3b3JrICZhbXA7IHN5c3RlbSBwcm9mZXNzaW9uYWwgd2l0aCBoYW5kcy1vbiBleHBlcnRpc2UgaW4gZW50ZXJwcmlzZSBuZXR3b3JraW5nLCBNaWtyb1RpaywgQ2lzY28sIFJIRUwsIGFuZCBXaW5kb3dzIFNlcnZlci5cIiAvPiIsICIgIDxtZXRhIG5hbWU9XCJ0d2l0dGVyOnRpdGxlXCIgY29udGVudD1cIkNlcnRpZmljYXRpb25zIHwgTWFoZnVqdXIgUmFobWFuXCIgLz5cbiAgPG1ldGEgbmFtZT1cInR3aXR0ZXI6ZGVzY3JpcHRpb25cIiBjb250ZW50PVwiQWxsIGNlcnRpZmljYXRpb25zIGhlbGQgYnkgTWFoZnVqdXIgUmFobWFuLCBpbmNsdWRpbmcgQ0NOQSwgTVRDTkEsIE1UQ1JFIGFuZCBNVENTRS5cIiAvPiJdLCBbIiAgPHRpdGxlPk1haGZ1anVyIFJhaG1hbiB8IE5ldHdvcmsgJmFtcDsgU3lzdGVtIEFkbWluaXN0cmF0aW9uPC90aXRsZT4iLCAiICA8dGl0bGU+Q2VydGlmaWNhdGlvbnMgfCBNYWhmdWp1ciBSYWhtYW48L3RpdGxlPiJdLCBbIiAgICBcInB1Ymxpc2hlclwiOiB7IFwiQHR5cGVcIjogXCJQZXJzb25cIiwgXCJuYW1lXCI6IFwiTWFoZnVqdXIgUmFobWFuXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgfSIsICIgICAgXCJwdWJsaXNoZXJcIjogeyBcIkB0eXBlXCI6IFwiUGVyc29uXCIsIFwibmFtZVwiOiBcIk1haGZ1anVyIFJhaG1hblwiLCBcInVybFwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH1cbiAgfVxuICA8L3NjcmlwdD5cbiAgPHNjcmlwdCB0eXBlPVwiYXBwbGljYXRpb24vbGQranNvblwiPlxuICB7XG4gICAgXCJAY29udGV4dFwiOiBcImh0dHBzOi8vc2NoZW1hLm9yZ1wiLFxuICAgIFwiQGdyYXBoXCI6IFtcbiAgICAgIHsgXCJAdHlwZVwiOiBcIkJyZWFkY3J1bWJMaXN0XCIsIFwiaXRlbUxpc3RFbGVtZW50XCI6IFtcbiAgICAgICAgeyBcIkB0eXBlXCI6IFwiTGlzdEl0ZW1cIiwgXCJwb3NpdGlvblwiOiAxLCBcIm5hbWVcIjogXCJIb21lXCIsIFwiaXRlbVwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH0sXG4gICAgICAgIHsgXCJAdHlwZVwiOiBcIkxpc3RJdGVtXCIsIFwicG9zaXRpb25cIjogMiwgXCJuYW1lXCI6IFwiQ2VydGlmaWNhdGlvbnNcIiwgXCJpdGVtXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvY2VydGlmaWNhdGVzL1wiIH0gXSB9LFxuICAgICAgeyBcIkB0eXBlXCI6IFwiQ29sbGVjdGlvblBhZ2VcIiwgXCJuYW1lXCI6IFwiQ2VydGlmaWNhdGlvbnMgfCBNYWhmdWp1ciBSYWhtYW5cIiwgXCJ1cmxcIjogXCJodHRwczovL21haGZ1ai5iZC9jZXJ0aWZpY2F0ZXMvXCIsIFwiZGVzY3JpcHRpb25cIjogXCJBbGwgY2VydGlmaWNhdGlvbnMgaGVsZCBieSBNYWhmdWp1ciBSYWhtYW4sIGluY2x1ZGluZyBDQ05BLCBNVENOQSwgTVRDUkUgYW5kIE1UQ1NFLlwiLFxuICAgICAgICBcImlzUGFydE9mXCI6IHsgXCJAdHlwZVwiOiBcIldlYlNpdGVcIiwgXCJuYW1lXCI6IFwiTWFoZnVqdXIgUmFobWFuXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9tYWhmdWouYmQvXCIgfSxcbiAgICAgICAgXCJhdXRob3JcIjogeyBcIkB0eXBlXCI6IFwiUGVyc29uXCIsIFwibmFtZVwiOiBcIk1haGZ1anVyIFJhaG1hblwiLCBcInVybFwiOiBcImh0dHBzOi8vbWFoZnVqLmJkL1wiIH0gfVxuICAgIF0iXSwgWyI8L2hlYWQ+XG48Ym9keT4iLCAiPC9oZWFkPlxuPGJvZHkgY2xhc3M9XCJsaXN0LW1vZGVcIj4iXSwgWyIgIDxhIGhyZWY9XCIjaG9tZVwiIGNsYXNzPVwic2tpcC1saW5rXCI+U2tpcCB0byBjb250ZW50PC9hPiIsICIgIDxhIGhyZWY9XCIvI2hvbWVcIiBjbGFzcz1cInNraXAtbGlua1wiPlNraXAgdG8gY29udGVudDwvYT4iXSwgWyIgICAgICA8YSBocmVmPVwiI2hvbWVcIiBjbGFzcz1cImxvZ29cIiBhcmlhLWxhYmVsPVwiTWFoZnVqdXIgUmFobWFuIOKAlCBIb21lXCI+IiwgIiAgICAgIDxhIGhyZWY9XCIvI2hvbWVcIiBjbGFzcz1cImxvZ29cIiBhcmlhLWxhYmVsPVwiTWFoZnVqdXIgUmFobWFuIOKAlCBIb21lXCI+Il0sIFsiICAgICAgICA8bGk+PGEgaHJlZj1cIiNob21lXCI+SG9tZTwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiNza2lsbHNcIj5Ta2lsbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjcHJvamVjdHNcIj5Qcm9qZWN0czwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIiNjZXJ0aWZpY2F0aW9uc1wiPkNlcnRpZmljYXRpb25zPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI2Jsb2dcIj5CbG9nPC9hPjwvbGk+XG4gICAgICAgIDxsaT48YSBocmVmPVwiI3Rvb2xzXCI+VG9vbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIjY29udGFjdFwiPkNvbnRhY3Q8L2E+PC9saT4iLCAiICAgICAgICA8bGk+PGEgaHJlZj1cIi8jaG9tZVwiPkhvbWU8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI3NraWxsc1wiPlNraWxsczwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jcHJvamVjdHNcIj5Qcm9qZWN0czwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jY2VydGlmaWNhdGlvbnNcIj5DZXJ0aWZpY2F0aW9uczwvYT48L2xpPlxuICAgICAgICA8bGk+PGEgaHJlZj1cIi8jYmxvZ1wiPkJsb2c8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI3Rvb2xzXCI+VG9vbHM8L2E+PC9saT5cbiAgICAgICAgPGxpPjxhIGhyZWY9XCIvI2NvbnRhY3RcIj5Db250YWN0PC9hPjwvbGk+Il0sIFsiICAgICAgICA8bGk+PGEgaHJlZj1cIiNjb250YWN0XCIgY2xhc3M9XCJuYXYtY3RhXCI+SElSRSBNRTwvYT48L2xpPiIsICIgICAgICAgIDxsaT48YSBocmVmPVwiLyNjb250YWN0XCIgY2xhc3M9XCJuYXYtY3RhXCI+SElSRSBNRTwvYT48L2xpPiJdLCBbIiAgICAgIDxsaT48YSBocmVmPVwiI2hvbWVcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+PoDwvc3Bhbj4gSG9tZSA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjc2tpbGxzXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKaoTwvc3Bhbj4gU2tpbGxzIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIiNwcm9qZWN0c1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5SnPC9zcGFuPiBQcm9qZWN0cyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjY2VydGlmaWNhdGlvbnNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+Okzwvc3Bhbj4gQ2VydGlmaWNhdGlvbnMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI2Jsb2dcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+TnTwvc3Bhbj4gQmxvZyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIjdG9vbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+boDwvc3Bhbj4gVG9vbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiI2NvbnRhY3RcIiBjbGFzcz1cIm1vYi1saW5rIG5hdi1jdGEtbW9iaWxlXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+4pyJPC9zcGFuPiBISVJFIE1FPC9hPjwvbGk+IiwgIiAgICAgIDxsaT48YSBocmVmPVwiLyNob21lXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfj6A8L3NwYW4+IEhvbWUgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNza2lsbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+4pqhPC9zcGFuPiBTa2lsbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNwcm9qZWN0c1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5SnPC9zcGFuPiBQcm9qZWN0cyA8c3BhbiBjbGFzcz1cIm1vYi1saW5rLWFycm93XCI+4oaSPC9zcGFuPjwvYT48L2xpPlxuICAgICAgPGxpPjxhIGhyZWY9XCIvI2NlcnRpZmljYXRpb25zXCIgY2xhc3M9XCJtb2ItbGlua1wiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPvCfjpM8L3NwYW4+IENlcnRpZmljYXRpb25zIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jYmxvZ1wiIGNsYXNzPVwibW9iLWxpbmtcIj48c3BhbiBjbGFzcz1cIm1vYi1saW5rLWljb25cIj7wn5OdPC9zcGFuPiBCbG9nIDxzcGFuIGNsYXNzPVwibW9iLWxpbmstYXJyb3dcIj7ihpI8L3NwYW4+PC9hPjwvbGk+XG4gICAgICA8bGk+PGEgaHJlZj1cIi8jdG9vbHNcIiBjbGFzcz1cIm1vYi1saW5rXCI+PHNwYW4gY2xhc3M9XCJtb2ItbGluay1pY29uXCI+8J+boDwvc3Bhbj4gVG9vbHMgPHNwYW4gY2xhc3M9XCJtb2ItbGluay1hcnJvd1wiPuKGkjwvc3Bhbj48L2E+PC9saT5cbiAgICAgIDxsaT48YSBocmVmPVwiLyNjb250YWN0XCIgY2xhc3M9XCJtb2ItbGluayBuYXYtY3RhLW1vYmlsZVwiPjxzcGFuIGNsYXNzPVwibW9iLWxpbmstaWNvblwiPuKciTwvc3Bhbj4gSElSRSBNRTwvYT48L2xpPiJdLCBbIiAgICAgICAgeyBpZDogJzE3ODIxNTUwNDk1NDdwYmJuZicsIG9yZGVyOiAzLCBpY29uOiAn8J+qnycsIHRpdGxlOiAnV2luZG93cyBTZXJ2ZXIgQWRtaW5pc3RyYXRpb24nLCBkZXNjOiBg4oCiIFdpbmRvd3MgU2VydmVyIEluc3RhbGxhdGlvbiwgRGVwbG95bWVudCwgVXBncmFkZSwgYW5kIEluaXRpYWwgQ29uZmlndXJhdGlvbiIsICIgICAgICAgIHsgaWQ6ICcxNzgyMTU1MDQ5NTQ3cGJibmYnLCBvcmRlcjogMywgaWNvbjogJ/CflqXvuI8nLCB0aXRsZTogJ1dpbmRvd3MgU2VydmVyIEFkbWluaXN0cmF0aW9uJywgZGVzYzogYOKAoiBXaW5kb3dzIFNlcnZlciBJbnN0YWxsYXRpb24sIERlcGxveW1lbnQsIFVwZ3JhZGUsIGFuZCBJbml0aWFsIENvbmZpZ3VyYXRpb24iXSwgWyIgICAgZnVuY3Rpb24gY3VycmVudExpc3RQYWdlKCkgeyIsICIgICAgZnVuY3Rpb24gY3VycmVudExpc3RQYWdlKCkge1xuICAgICAgY29uc3QgZm9yY2VkID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmdldEF0dHJpYnV0ZSgnZGF0YS1wYWdlJyk7ICAgLy8gc2V0IG9uIGJsb2cvIGFuZCBjZXJ0aWZpY2F0ZXMvIHBhZ2VzXG4gICAgICBpZiAoZm9yY2VkICYmIExJU1RfUEFHRVNbZm9yY2VkXSkgcmV0dXJuIGZvcmNlZDsiXSwgWyIgICAgICBjb25zdCB1cmwgPSBjZmcgPyBfaG9tZU1ldGEuY2Fub25pY2FsICsgJz9wYWdlPScgKyBrZXkgOiBfaG9tZU1ldGEuY2Fub25pY2FsOyIsICIgICAgICBjb25zdCB1cmwgPSBjZmcgPyAoUEFHRV9QQVRIU1trZXldID8gbmV3IFVSTChfaG9tZU1ldGEuY2Fub25pY2FsLCBsb2NhdGlvbi5ocmVmKS5vcmlnaW4gKyBQQUdFX1BBVEhTW2tleV0gOiBfaG9tZU1ldGEuY2Fub25pY2FsICsgJz9wYWdlPScgKyBrZXkpIDogX2hvbWVNZXRhLmNhbm9uaWNhbDsiXV19";
function _canonPath(html) {
const m = / c.charCodeAt(0));
const table = JSON.parse(new TextDecoder().decode(bytes));
const patches = table[p.replace(/^\/|\/$/g, '')];
if (!patches) return null;
let out = homeHtml;
for (const [o, n] of patches) {
const i = out.indexOf(o);
if (i === -1 || out.indexOf(o, i + 1) !== -1) return null;
out = out.slice(0, i) + n + out.slice(i + o.length);
}
return out;
} catch (e) { return null; }
}
async function exportFullSite() {
showToast('⏳ Building full site ZIP…');
const skipped = [];
const notes = [];
let homeHtml = null;
try {
const JSZip = await _loadJSZip();
const zip = new JSZip();
const added = new Set();
const addFile = (zipPath, data) => {
zipPath = decodeURIComponent(zipPath);
if (added.has(zipPath)) return;
added.add(zipPath);
zip.file(zipPath, data);
};
// live content (everything the admin sees, incl. posts added after the last deploy)
const live = {
PROJECTS: getItems(PROJECTS_KEY),
CERTS: getItems(CERTS_KEY),
BLOG: getItems(BLOG_KEY),
SKILLS: getItems(SKILLS_KEY)
};
const liveJson = JSON.stringify(live).replace(/\\[nrt]/g, ' ');
// 1) work out which pages to pack: the core list + anything in sitemap.xml
const pages = SITE_CORE_PAGES.slice();
let sitemapText = null;
try {
sitemapText = await _fetchSiteText('/sitemap.xml');
const re = /\s*([^<\s]+)\s*<\/loc>/gi; let m;
while ((m = re.exec(sitemapText))) {
try {
const p = _normPath(new URL(m[1], location.origin).pathname);
const alt = p.endsWith('/') ? p.slice(0, -1) : p + '/';
if (!pages.includes(p) && !pages.includes(alt)) pages.push(p);
} catch (e) {}
}
} catch (e) { /* sitemap optional */ }
// 2) pages, with live data spliced into every page that carries it
const assetPaths = new Set();
let n = 0;
for (const p of pages) {
n++; showToast('⏳ Packing pages ' + n + '/' + pages.length + '…');
let html = await _fetchSitePage(p, homeHtml);
if (html === null && p !== '/') {
html = _rebuildFromHome(p, homeHtml);
if (html !== null) notes.push(p + ' — rebuilt from the home page (your server returned the wrong page for this URL)');
}
if (html === null && _normPath(location.pathname).replace(/index\.html$/, '') === p) {
html = '\n' + document.documentElement.outerHTML; // last resort: the page you are on
}
if (html === null) { skipped.push(p + ' — server returned the wrong page or nothing'); continue; }
if (p === '/') homeHtml = html; // keep the untouched home page (before data is spliced in)
if (html.indexOf('/* SEED_' + 'PROJECTS_START */') !== -1) {
html = _spliceSeedBlock(html, 'PROJECTS', live.PROJECTS);
html = _spliceSeedBlock(html, 'CERTS', live.CERTS);
html = _spliceSeedBlock(html, 'BLOG', live.BLOG);
html = _spliceSeedBlock(html, 'SKILLS', live.SKILLS);
html = html.replace('', '\n ');
}
addFile(_pageZipPath(p), html);
// local files used by this page (images, css, js)
const attrRe = new RegExp('(?:src|href)=["\'](\\/[^"\'#?\\s]+\\.(?:' + SITE_ASSET_EXT + '))["\']', 'gi'); let a;
while ((a = attrRe.exec(html))) assetPaths.add(a[1]);
}
// 3) local images/files referenced from posts, projects, certs, skills
const dataRe = new RegExp('(?:^|[^\\w.:\\/\\-])(\\/(?:[\\w\\-.%]+\\/)*[\\w\\-.%]+\\.(?:' + SITE_ASSET_EXT + '))\\b', 'gi'); let d;
while ((d = dataRe.exec(liveJson))) assetPaths.add(d[1]);
// 4) static files
SITE_STATIC_FILES.forEach(f => assetPaths.add(f));
// 5) fetch every file (skip pages already added)
const list = Array.from(assetPaths).filter(f => !added.has(decodeURIComponent(f.slice(1))));
n = 0;
for (const f of list) {
n++; showToast('⏳ Packing files ' + n + '/' + list.length + '…');
const zipPath = f.slice(1);
try {
addFile(zipPath, await _fetchSiteBinary(f));
} catch (e) {
if (SITE_FALLBACK_FILES[zipPath] !== undefined) addFile(zipPath, SITE_FALLBACK_FILES[zipPath]);
else skipped.push(f + ' (not found on the server yet)');
}
}
showToast('⏳ Creating ZIP…');
const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
const stamp = new Date().toISOString().slice(0, 10);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'mahfuj-bd-site-' + stamp + '.zip';
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
showToast('✓ Full site ZIP downloaded — ' + added.size + ' files');
if (skipped.length || notes.length) {
alert('ZIP downloaded with ' + added.size + ' files.' +
(notes.length ? '\n\nRebuilt (check these after uploading):\n' + notes.join('\n') : '') +
(skipped.length ? '\n\nNOT in the ZIP (not on your server yet, e.g. images you have not uploaded, or pages the server did not return):\n' + skipped.join('\n') : ''));
}
} catch (e) {
console.error(e);
alert('Could not build the full-site ZIP: ' + e.message + '\n\nFalling back to a JSON data-only export instead.');
exportAllData();
}
}
function fmt(ts) {
return new Date(ts).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
}
function esc(s) {
return (s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');
}
// Only http(s), mailto, tel and relative links are allowed in href attributes (blocks javascript: / data: / vbscript:)
function safeUrl(u) {
const t = String(u || '').trim();
const probe = t.replace(/[\u0000-\u0020\u007f-\u009f]/g, '');
if (/^[a-z][a-z0-9+.\-]*:/i.test(probe) && !/^(https?|mailto|tel):/i.test(probe)) return '#';
return t;
}
// Converts plain-text URLs inside already-escaped HTML into real, clickable
// tags. Must be run AFTER esc() so we're not re-escaping our own markup.
function linkify(escapedHtml) {
const urlRe = /((https?:\/\/|www\.)[^\s<]+)/gi;
return escapedHtml.replace(urlRe, (match) => {
let url = match;
let trailing = '';
const trailingMatch = url.match(/[).,;:!?]+$/);
if (trailingMatch) { trailing = trailingMatch[0]; url = url.slice(0, url.length - trailing.length); }
const href = /^www\./i.test(url) ? 'https://' + url : url;
return `${url}${trailing}`;
});
}
// For the expanded read view: convert raw "• line" bullet text into real
//
markup so wrapped lines align under the text, not the bullet.
// Every bullet style people type (• - – — * · ● ▪ ◦ ✓ ➤ …), with or without a space after the symbol
const BULLET_RE = /^(?:[•·●▪◦‣✓✔➤➢►]\s*|[-–—*]\s+)/;
// Rewrite any of those markers to "• " so new posts are stored exactly like the older ones
function normalizeBullets(text) {
return (text || '').split('\n').map(l => {
const m = l.match(/^(\s*)(.*)$/);
return BULLET_RE.test(m[2]) ? '• ' + m[2].replace(BULLET_RE, '').trim() : l;
}).join('\n');
}
function formatDescriptionHtml(text) {
const raw = (text || '').trim();
if (!raw) return '';
const bulletRe = BULLET_RE;
const blocks = raw.split(/\n\s*\n/);
return blocks.map(block => {
const lines = block.split('\n').map(l => l.trim()).filter(Boolean);
let firstBulletIdx = lines.findIndex(l => bulletRe.test(l));
// A heading ending in ":" (e.g. "Key Implementations:") followed by plain lines
// is a list even if no bullet characters were typed.
let implicit = false;
if (firstBulletIdx === -1) {
const h = lines.findIndex(l => /:$/.test(l));
if (h !== -1 && lines.length - h - 1 >= 2) { firstBulletIdx = h + 1; implicit = true; }
}
if (firstBulletIdx === -1) {
return `
${bulletLines.map(l => {
let text = implicit ? l : l.replace(bulletRe, '').trim();
if (!/[.!?:]$/.test(text)) text += '.';
return `
${esc(text)}
`;
}).join('')}
`;
return headingHtml + listHtml;
}).join('');
}
// For collapsed card previews: strip bullet markers and line breaks so
// multi-line/bulleted descriptions read as plain flowing text. The full
// formatted version (with bullets) still shows in the expanded read view.
function previewText(s) {
return (s || '')
.split('\n')
.map(line => line.replace(BULLET_RE, '').trim())
.filter(Boolean)
.map(line => line.replace(/:$/, '.'))
.map(line => /[.!?]$/.test(line) ? line : line + '.')
.join(' ');
}
function uid() {
return Date.now().toString() + Math.random().toString(36).slice(2,7);
}
// Detect a YouTube URL (watch, youtu.be, shorts, or embed) and pull out the video ID.
function getYouTubeId(url) {
if (!url) return null;
const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
return m ? m[1] : null;
}
// Swap a blog card's video thumbnail for a playing embed, filling that same card area.
window.playInlineVideo = function(container, videoId) {
container.outerHTML = ``;
};
// Split items into "featured" (shown on home, by manual order) and "rest"
// (everything else, behind See more). Limit is responsive.
function splitFeatured(items) {
const max = getMaxFeatured();
const sorted = [...items].sort((a,b) => (a.order ?? 0) - (b.order ?? 0));
const featured = sorted.slice(0, max);
const rest = sorted.slice(max);
return { featured, rest };
}
function nextOrder(items) {
return items.reduce((m, it) => Math.max(m, it.order ?? -1), -1) + 1;
}
// Re-render projects + certs when crossing the 900px tablet breakpoint
// so the featured count (3 vs 4) updates without a page reload.
// Debounced so a drag-resize doesn't trigger a render on every tick.
let _lastTablet = window.innerWidth <= 900 && window.innerWidth > 600;
let _resizeTimer = null;
window.addEventListener('resize', function() {
clearTimeout(_resizeTimer);
_resizeTimer = setTimeout(() => {
const nowTablet = window.innerWidth <= 900 && window.innerWidth > 600;
if (nowTablet !== _lastTablet) {
_lastTablet = nowTablet;
renderProjects();
renderCertifications();
}
}, 150);
}, { passive: true });
// ── SEED DATA from site-data-export.json — fixed IDs, always authoritative ──
// Always write these so random-uid duplicates from old loads are cleared out.
/* SEED_PROJECTS_START */
const SEED_PROJECTS = [
{ id: "1782155049547ir0sh", ts: 1782155044547, pinned: true, pinnedAt: 1, order: 0,
title: 'Enterprise Multi-Branch Network Design and Implementation',
description: `Designed and implemented a multi-branch enterprise network with full redundancy, efficient routing, and secure connectivity. Configured CORE and branch routers to ensure high availability and seamless communication across all branches.
Key Implementation:
• Configured eBGP routing between ISP, CORE, and branch routers for dynamic and scalable connectivity
• Implemented HSRP at branch routers to provide redundant gateway access for LAN users
• Deployed NAT on CORE routers to enable secure internet access
• Managed IP addressing
• Verified network functionality using show ip bgp summary, show standby brief, show ip nat translations, and routing checks`,
tags: 'Cisco routers, Cisco Switches, BGP, HSRP, NAT/PAT, Routing, Redundancy, PC, IP addressing, Cisco Packet Tracer',
link: 'https://github.com/mahfuj-tanvir/eBGP' },
{ id: "1782155049547givxd", ts: 1782155045547, pinned: true, pinnedAt: 2, order: 1,
title: 'RHEL 8 Enterprise Infrastructure Lab',
description: `Designed and deployed an enterprise Linux infrastructure on RHEL 8 with integrated network, security, mail, web, and virtualization services to simulate a real-world enterprise environment. Configured and managed core Linux services, secure system administration, and cross-platform integration between Linux and Windows systems.
Key Implementation:
• Configured DNS server using BIND for internal domain name resolution
• Implemented DHCP server for centralized dynamic IP allocation
• Deployed Apache (HTTPD) web server with virtual host configuration
• Configured Samba file sharing for Linux–Windows integration
• Created local YUM repository for centralized and offline package management
• Implemented secure mail infrastructure using Postfix, Dovecot, and Gmail SMTP relay
• Configured KVM virtualization using QEMU, libvirt, and Virt-Manager
• Deployed and managed Windows Server 2022 virtual machine on KVM
• Applied SELinux policies, firewalld configuration, and root login security alert monitoring
• Performed system administration, troubleshooting, and service verification tasks`,
tags: 'RHEL 8, KVM, BIND DNS, DHCP, Apache HTTPD, Samba, Mail Server, SELinux/Firewalld',
link: 'https://github.com/mahfuj-tanvir/RHEL8' },
{ id: "17821550495479dvw2", ts: 1782155046547, pinned: true, pinnedAt: 3, order: 2,
title: 'Enterprise Windows Server 2022 Infrastructure Deployment',
description: `Designed and implemented a multi-department enterprise infrastructure using Windows Server 2022, Hyper-V, and pfSense Firewall, providing centralized identity management, network services, and security controls.
Key Implementation:
• Deployed Active Directory Domain Services (AD DS), DNS, and DHCP
• Configured Hyper-V virtualization and pfSense-based network segmentation
• Implemented DHCP Relay for multi-subnet IP address management
• Installed and configured AD CS as an Enterprise Root CA
• Created OUs, users, and Group Policies for centralized administration
• Enforced security policies including USB restrictions, password policies, and account lockout settings
• Deployed IPAM for centralized IP address monitoring and management`,
tags: 'Windows Server 2022, AD DS, DNS, DHCP, Hyper-V, AD CS, Group Policy (GPO), DFS',
link: 'https://github.com/mahfuj-tanvir/MS-Windows-Server' },
{ id: "1782155049547ftebv", ts: 1782155047547, pinned: false, pinnedAt: null, order: 3,
title: 'pfSense Firewall and Network Segmentation Implementation',
description: `Designed and deployed a pfSense-based firewall solution for a multi-department network environment using Hyper-V virtualization.
Key Implementation:
• Configured WAN, LAN, and multiple departmental networks
• Implemented network segmentation and inter-network routing
• Configured DHCP Relay for centralized DHCP services
• Provided secure Internet connectivity and gateway services
• Managed firewall interfaces and network traffic control`,
tags: 'pfSense, Hyper-V, DHCP Relay, IPv4 Networking, Routing, Firewall Management',
link: 'https://github.com/mahfuj-tanvir/pfSense' },
{ id: "1782155049547mktk1", ts: 1782155048547, pinned: false, pinnedAt: null, order: 4,
title: 'MikroTik Multi-Department Enterprise Network and PPPoE ISP Simulation',
description: `Designed and implemented a virtual multi-department enterprise network using MikroTik Cloud Hosted Router (CHR) on Microsoft Hyper-V. Integrated Windows Server DNS services and simulated an ISP environment using PPPoE to provide secure Internet connectivity, centralized network services, and bandwidth-managed remote access.
Key Implementation:
• Designed the network using VLSM and configured WAN connectivity, interface IP addressing, NAT Masquerade, and DHCP Relay
• Integrated Windows Server 2022 DNS services and configured DHCP Servers for departmental networks with DNS suffix distribution
• Deployed a PPPoE Server with Bronze, Silver, and Gold bandwidth profiles, and configured a MikroTik CHR-based PPPoE Client Router with a Home LAN
• Configured router identification using System Note, created router backups, and exported configurations for recovery and maintenance
• Verified end-to-end network functionality using Ping, NSLookup, IPConfig, and Traceroute, and troubleshot DHCP, DNS, NAT, and PPPoE services`,
tags: 'Microsoft Hyper-V, MikroTik RouterOS (CHR), WinBox, VLSM, DHCP, NAT Masquerade, PPPoE',
link: 'https://github.com/mahfuj-tanvir/Mikrotik' }
];
/* SEED_PROJECTS_END */
/* SEED_CERTS_START */
const SEED_CERTS = [
{ id: "1782155049547qwgnh", ts: 1782155048547, order: 0, pinned: false, pinnedAt: null,
title: 'Cisco Certified Network Associate (CCNA)',
description: 'Certified in Cisco enterprise networking, including routing, switching, IP services, network security, and infrastructure management. Issued June 2026.',
badges: 'Verify CCNA|https://cp.certmetrics.com/cisco/en/public/verify/credential/8fb33cae27834cd489812f7c8bb55a86' },
{ id: "1782155049547k72yx", ts: 1782155046547, order: 1, pinned: true, pinnedAt: 1,
title: 'MikroTik Certified Network Associate (MTCNA)',
description: 'Certified in foundational MikroTik network administration including DHCP, NAT, PPPoE, routing, and firewall configuration. Issued April 2026.',
badges: 'Verify MTCNA|https://mikrotik.com/training/certificates/c714051cb3def6dc24bb' },
{ id: "1782155049547zyy48", ts: 1782155047047, order: 2, pinned: true, pinnedAt: 2,
title: 'MikroTik Certified Routing Engineer (MTCRE)',
description: 'Certified in advanced MikroTik routing including static and dynamic routing protocols, BGP, and enterprise network design. Issued April 2026.',
badges: 'Verify MTCRE|https://mikrotik.com/training/certificates/c714071ce66726707870' },
{ id: "17821550495470vs7q", ts: 1782155047547, order: 3, pinned: true, pinnedAt: 3,
title: 'MikroTik Certified Security Engineer (MTCSE)',
description: 'Certified in MikroTik network security including firewall policies, VPN, and traffic filtering for enterprise environments. Issued April 2026.',
badges: 'Verify MTCSE|https://mikrotik.com/training/certificates/c718897c84fcd13c8af8' },
{ id: "1782218399959fofl9", ts: 1782218399959, order: 4,
title: 'Introduction to Critical Infrastructure Protection (ICIP)',
description: 'Certified in critical infrastructure protection principles, including cybersecurity, physical security, risk assessment, threat identification, and resilience strategies for essential sectors. Issued June 2026.',
badges: 'Verify ICIP|https://learn.opswatacademy.com/certificate/4w8lnPTvJQ' }
];
/* SEED_CERTS_END */
/* SEED_BLOG_START */
const SEED_BLOG = [
{ id: "1779693506059", ts: 1779693506059, order: 0, category: "Career", emoji: "📝",
title: "Learning journey with the IsDB-BISEW IT Scholarship Programme.",
summary: "Completed the IsDB-BISEW IT Scholarship Programme, earning CCNA, MTCNA, MTCRE, and MTCSE certifications.",
image: "https://lh3.googleusercontent.com/d/18G7gozkfZtwPelRv93-IhJ2cUFrrcSHa",
content: `A few weeks ago, I completed an incredible learning journey with the IsDB-BISEW IT Scholarship Programme. 🎓
During this journey, I gained hands-on experience in Networking & System Administration and earned:
✅ CCNA
✅ MTCNA
✅ MTCRE
✅ MTCSE
I also strengthened my skills in MikroTik, Cisco, Windows Server, Red Hat Enterprise Linux and virtualization.
Grateful to all my instructors, mentors, and classmates for their support throughout this journey. Now looking forward to the next step in my IT career and continuing to grow in networking and cybersecurity.
#IsDBBISEW #Networking #MikroTik #Linux #Cisco #SystemAdministration #CyberSecurity` },
{ id: "1782217308090alwrw", ts: 1782217308090, order: 1, category: "Networking", emoji: "📝",
title: "pfSense Firewall & Network Segmentation Lab",
summary: "Deployed and configured a pfSense firewall environment using Hyper-V virtualization, with full network segmentation across departments.",
image: "https://lh3.googleusercontent.com/d/16j6DnQ1rWK1O9ZhZNVZ8UFs6pHBLgp7t",
content: `As part of my hands-on infrastructure and networking practice, I recently deployed and configured a pfSense firewall environment using Hyper-V virtualization.
🔹 Key Highlights:
• Configured WAN, LAN, and multiple departmental networks
• Implemented network segmentation for Server, Management, Finance, HR, and IT departments
• Configured inter-network routing through pfSense
• Implemented DHCP Relay for centralized DHCP services
• Provided secure Internet connectivity and gateway services
• Tested end-to-end connectivity and DHCP functionality across all networks
This lab helped me strengthen my practical knowledge of firewall administration, network segmentation, routing, and enterprise network design.
Technologies Used:
#pfSense #HyperV #Networking #Firewall #DHCP #Routing #SystemAdministration #NetworkSecurity #WindowsServer #ITInfrastructure #MTCNA #CCNA
Continuous learning and hands-on practice are essential for building real-world IT infrastructure skills. Looking forward to exploring more advanced pfSense and enterprise security implementations.` },
{ id: "1782219034584dqaw2", ts: 1782219034584, order: 2, category: "General", emoji: "📝",
title: "Exploring the Beauty of Rangamati",
summary: "A look at my visit to Rangamati and why balancing travel with professional growth matters.",
image: "https://lh3.googleusercontent.com/d/1CqX9YevwOdZKxf4qvwoghhcN9JJ4-t1f",
content: `Beyond my professional career in networking and system administration, I have a passion for traveling and exploring the natural beauty of Bangladesh. Traveling allows me to refresh my mind, experience different cultures, and gain new perspectives that contribute to both my personal and professional growth.
This photograph was captured during my visit to Rangamati, a beautiful hill district known for its breathtaking landscapes, serene lakes, and rich cultural heritage. The peaceful environment and stunning scenery made the trip a memorable experience. Moments like these remind me of the importance of balancing professional development with personal interests and exploration.
Through travel, I continue to learn, adapt, and appreciate the diversity of the world around me.` }
];
/* SEED_BLOG_END */
/* SEED_SKILLS_START */
const SEED_SKILLS = [
{ id: '1782155049547ouixq', order: 0, icon: '🌐', title: 'Networking & Routing', desc: `• Understand and apply IPv4/IPv6 addressing, subnetting, VLSM, CIDR, and route summarization.
• Design and configure LAN/WAN networks with static and dynamic routing (OSPF).
• Configure inter-VLAN routing, DHCP, NAT/PAT, and essential IP services.
• Implement network segmentation, gateway redundancy, and basic network security.
• Troubleshoot Layer 2 and Layer 3 connectivity using standard networking tools and techniques.`, tags: 'IPv4, IPv6, VLSM, CIDR, Routing, Switching, OSPF, Inter-VLAN Routing, DHCP, NAT/PAT, Network Segmentation, Gateway Redundancy' },
{ id: '1782155049547qfuvl', order: 1, icon: '🔵', title: 'Cisco Infrastructure', desc: `• Cisco IOS Installation, Initial Configuration, and Device Management
• Router and Switch Configuration, VLANs, Trunking, and Inter-VLAN Routing
• Static Routing, OSPF, eBGP, RIP, Default Routing, and Route Summarization
• Layer 2 and Layer 3 Switching, STP, EtherChannel, and High Availability
• DHCP, NAT/PAT, ACLs, NTP, and IP Services Configuration
• Network Security with Port Security, DHCP Snooping, Dynamic ARP Inspection (DAI), and Device Hardening
• IPv4/IPv6 Addressing, Subnetting, VLSM, and Network Segmentation
• WAN Technologies, Router-on-a-Stick, and Enterprise Network Deployment
• Network Monitoring, Verification, Troubleshooting, and Cisco IOS Maintenance
• Cisco Packet Tracer–Based Enterprise Network Design and Implementation`, tags: 'Cisco IOS, VLANs, Inter-VLAN Routing, OSPF, eBGP, STP, EtherChannel, NAT/PAT, ACLs, Port Security, DHCP Snooping, DAI, VLSM' },
{ id: '1782155049547ckent', order: 2, icon: '🛡️', title: 'MikroTik Engineering', desc: `• MikroTik RouterOS Installation, Configuration, and System Administration
• IPv4/IPv6 Addressing, Bridging, VLANs, Routing, and Switching
• Static Routing, OSPF, BGP, Route Filtering, and Policy-Based Routing
• DHCP, DNS, NAT, Firewall, and QoS Configuration
• PPPoE Server/Client, VPN, and User Management
• Load Balancing, Failover, and Traffic Engineering
• Wireless Configuration, Hotspot, and Wireless Security
• Bandwidth Management, Queues, and Traffic Monitoring
• RouterOS Backup, Restore, Upgrade, System Monitoring, and Troubleshooting
• RouterBOARD, CHR, Virtual Router Deployment, and Network Security Administration`, tags: 'RouterOS, VLANs, OSPF, BGP, NAT, Firewall, QoS, PPPoE, VPN, Load Balancing, Hotspot, RouterBOARD, CHR' },
{ id: '1782155049547pbbnf', order: 3, icon: '🪟', title: 'Windows Server Administration', desc: `• Windows Server Installation, Deployment, Upgrade, and Initial Configuration
• Server Roles, Features, and Server Manager Administration
• Active Directory Domain Services (AD DS) Administration
• DNS, DHCP, IP Address Management (IPAM), and TCP/IP Configuration
• Domain Controller Deployment and Group Policy (GPO) Management
• Active Directory Certificate Services (AD CS)
• File, Storage, and Print Services Management
• User, Group, NTFS Permissions, Shared Resources, and Security Policies
• Windows Registry, Event Logs, Performance Monitoring, and Task Scheduler
• Remote Desktop Services (RDS), Remote Administration, and Windows Admin Center
• Windows Defender Firewall, User Account Control (UAC), and Windows Security
• PowerShell Scripting, Automation, and Desired State Configuration (DSC)
• Hyper-V Role Installation, Virtual Machine, Virtual Network, and Virtual Storage Management
• Windows Containers and Docker Fundamentals
• System Maintenance, Backup, Recovery, Updates, and Troubleshooting`, tags: 'Windows Server, Server Manager, AD DS, DNS, DHCP, IPAM, GPO, AD CS, NTFS, Registry, Event Logs, RDS, Windows Admin Center, UAC, PowerShell, DSC, Hyper-V, Docker' },
{ id: '178215504954798260', order: 4, icon: '🐧', title: 'Linux Administration', desc: `• Red Hat Enterprise Linux (RHEL) Installation, Configuration, and System Administration
• Linux Command Line, Bash Shell, and Shell Scripting
• User, Group, Sudo, and File Permission Management, ACL
• File System Administration, Partitions, LVM, Mounting, and Storage Management
• Process, Service, and Boot Management (systemd)
• RPM/DNF Package Installation, Updates, and Repository Management
• Network Configuration, Hostname Resolution, DNS, and SSH Administration
• Firewalld, SELinux, and Linux Security Administration
• System Logging, Performance Monitoring, and Troubleshooting
• Archive, File Transfer, Backup, and Recovery
• Virtual Machine Management (KVM) and Container Administration (Podman/Docker)
• Network Services: Apache HTTP Server, NFS, SMB, iSCSI, MariaDB, and DNS
• Task Automation with Cron and Bash Scripts
• Server Maintenance, Performance Tuning, and Troubleshooting`, tags: 'RHEL, Bash, ACL, LVM, systemd, RPM, DNF, SSH, Firewalld, SELinux, Cron, KVM, Podman, Docker, Apache, NFS, SMB, iSCSI, MariaDB' },
{ id: '1782155049547s5f8l', order: 5, icon: '🖥️', title: 'Hardware and Virtualization', desc: `• PC Hardware Installation, Configuration, and Troubleshooting
• Motherboards, CPUs, Memory (RAM), Storage Devices, and Power Supplies
• RAID, SSD/HDD Management, and Peripheral Devices
• Network Interface Cards (NICs) and Hardware Components
• Hyper-V Installation, Configuration, and Administration
• KVM (Kernel-based Virtual Machine) Deployment and Management
• VMware Workstation Virtual Machine Administration
• Virtual Machine Provisioning, Virtual Networking, and Virtual Storage
• Hyper-V Live Migration and Replica
• Windows Containers and Docker Fundamentals
• Knowledge of Type 1 and Type 2 Hypervisors, Virtualization, and Cloud Computing Concepts`, tags: 'PC Hardware, RAID, NIC, Hyper-V, KVM, VMware, Docker, CompTIA A+' }
];
/* SEED_SKILLS_END */
// ── populate cache with seed defaults so the page paints instantly, ────────
// then sync for real with Firestore the moment it responds.
_cache[PROJECTS_KEY] = SEED_PROJECTS;
_cache[CERTS_KEY] = SEED_CERTS;
_cache[BLOG_KEY] = SEED_BLOG;
_cache[SKILLS_KEY] = SEED_SKILLS;
// ── remember the last live data from Firestore on this device ─────────────
// Items created after the last export are not in the SEED_* arrays, so on a first
// paint they would show "Loading…" until Firebase answers. Reusing the last
// synced copy makes them appear instantly on every repeat visit; the real
// Firestore data still replaces it the moment it arrives.
(function () {
[PROJECTS_KEY, CERTS_KEY, BLOG_KEY, SKILLS_KEY].forEach(function (k) {
try {
var raw = localStorage.getItem('mr_live_cache_' + k);
var arr = raw ? JSON.parse(raw) : null;
if (Array.isArray(arr) && arr.length) _cache[k] = arr;
} catch (e) {}
window.addEventListener('fb-data-' + k, function (ev) {
try { localStorage.setItem('mr_live_cache_' + k, JSON.stringify(ev.detail.items)); } catch (e) {}
});
});
})();
// Only writes the seed data to Firestore the very first time (empty collection).
// Once real content exists there, this is a no-op forever after.
window.addEventListener('fb-auth-changed', (e) => {
if (!e.detail.loggedIn) return;
onFbReady(() => {
window._fb.seedIfEmpty(PROJECTS_KEY, SEED_PROJECTS);
window._fb.seedIfEmpty(CERTS_KEY, SEED_CERTS);
window._fb.seedIfEmpty(BLOG_KEY, SEED_BLOG);
window._fb.seedIfEmpty(SKILLS_KEY, SEED_SKILLS);
});
});
// ── keep cache in sync with Firestore in real time, for every visitor ─────
window.addEventListener('fb-data-' + PROJECTS_KEY, (e) => { _cache[PROJECTS_KEY] = e.detail.items; renderProjects(); updateHeroStats(); });
window.addEventListener('fb-data-' + CERTS_KEY, (e) => { _cache[CERTS_KEY] = e.detail.items; renderCertifications(); updateHeroStats(); });
window.addEventListener('fb-data-' + BLOG_KEY, (e) => { _cache[BLOG_KEY] = e.detail.items; renderPosts(); });
window.addEventListener('fb-data-' + SKILLS_KEY, (e) => { _cache[SKILLS_KEY] = e.detail.items; renderSkills(); });
// ── keep admin state in sync with Firebase Auth (persists across reloads) ──
window.addEventListener('fb-auth-changed', (e) => {
isAdmin = e.detail.loggedIn;
if (!isAdmin) {
document.querySelectorAll('.blog-admin-controls').forEach(el => el.classList.remove('visible'));
document.querySelectorAll('.skill-card-admin').forEach(el => el.classList.remove('visible'));
const sb = document.getElementById('skills-admin-btns'); if (sb) sb.classList.remove('visible');
const fab = document.getElementById('blog-admin-fab'); if (fab) fab.style.display = 'none';
const sf = document.getElementById('seo-check-fab'); if (sf) sf.style.display = 'none';
const ex = document.getElementById('export-data-fab'); if (ex) ex.style.display = 'none';
const lo = document.getElementById('admin-logout-fab'); if (lo) lo.style.display = 'none';
const ap = document.getElementById('add-project-btn'); if (ap) ap.style.display = 'none';
const ac = document.getElementById('add-cert-btn'); if (ac) ac.style.display = 'none';
const rp = document.getElementById('reorder-projects-btn'); if (rp) rp.style.display = 'none';
const rc = document.getElementById('reorder-certs-btn'); if (rc) rc.style.display = 'none';
const rb = document.getElementById('reorder-posts-btn'); if (rb) rb.style.display = 'none';
} else {
enableAdminUI();
}
renderProjects();
renderCertifications();
renderPosts();
if (typeof renderSkills === 'function') renderSkills();
});
// ── hero stat counters (Projects / Certifications counts update live) ────
function updateHeroStats() {
const projCount = visibleItems(getItems(PROJECTS_KEY)).length;
const certCount = visibleItems(getItems(CERTS_KEY)).length;
const pe = document.getElementById('stat-projects-num');
const pl = document.getElementById('stat-projects-label');
const ce = document.getElementById('stat-certs-num');
if (pe) pe.textContent = String(projCount);
if (pl) pl.textContent = projCount === 1 ? 'Major Project' : 'Projects';
if (ce) ce.textContent = certCount + '+';
}
updateHeroStats();
// ── share: works for projects, certifications, and blog posts ─────────────
// Builds a deep link (?project=/?cert=/?post=ID) to the specific item, then
// uses the native share sheet on mobile, or copies the link on desktop.
window.shareItem = function(type, id) {
const keyMap = { project: PROJECTS_KEY, cert: CERTS_KEY, post: BLOG_KEY };
const items = getItems(keyMap[type] || []);
const item = items.find(x => x.id === id);
const title = item ? item.title : 'Mahfujur Rahman';
const shareUrl = location.origin + detailUrl(type, id);
const shareData = { title: title, text: `${title} — Mahfujur Rahman`, url: shareUrl };
if (navigator.share) {
navigator.share(shareData).catch(() => {});
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrl)
.then(() => showToast('🔗 Link copied to clipboard'))
.catch(() => window.prompt('Copy this link:', shareUrl));
} else {
window.prompt('Copy this link:', shareUrl);
}
};
function scrollToAndHighlight(id) {
let el = document.querySelector(`[data-id="${id}"]`);
if (!el) {
// Not on the home grid (not featured): show it on its "See more" page
const key = ['projects', 'certifications'].find(k => LIST_PAGES[k].items().some(x => x.id === id));
if (!key) return;
history.replaceState({}, '', window.location.pathname + '?page=' + key);
applyRoute();
el = document.querySelector(`[data-id="${id}"]`);
if (!el) return;
}
requestAnimationFrame(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('share-highlight');
setTimeout(() => el.classList.remove('share-highlight'), 2200);
});
}
function handleDeepLink() { refreshDetail(); }
// ── admin controls markup shared by project / cert / blog cards ──────────
function adminControlsHtml(opts) {
// opts: { onEdit, onDelete, onTogglePublish, published }
const pubBtn = opts.onTogglePublish
? ``
: '';
return `
${pubBtn}
`;
}
// Listing pages that live on their own URL (folder with its own index.html)
const PAGE_PATHS = { blog: '/blog/', certifications: '/certificates/', projects: '/projects/' };
function seeMoreBtnHtml(prefix, count, noun) {
if (!count) return '';
return `
`
).join('');
}
function seoApplyScore(scoreEl, barEl, score) {
const level = score >= 80 ? 'good' : score >= 50 ? 'ok' : 'bad';
scoreEl.textContent = score + '%';
scoreEl.className = 'seo-score-pill seo-' + level;
barEl.style.width = score + '%';
barEl.className = 'seo-bar-fill seo-' + level;
}
function runBlogSeoCheck() {
const title = document.getElementById('post-title').value.trim();
const keyword = document.getElementById('post-keyword').value.trim();
const summary = document.getElementById('post-summary').value.trim();
const content = document.getElementById('post-content').value.trim();
const kw = keyword.toLowerCase();
const wordCount = content ? content.split(/\s+/).filter(Boolean).length : 0;
const checks = [];
let passCount = 0, total = 0;
const add = (status, text) => { checks.push({ status, text }); total++; if (status==='pass') passCount++; else if (status==='warn') passCount += 0.5; };
// Focus keyword present at all
if (!kw) {
add('fail', 'No focus keyword set — add one to unlock keyword-based checks.');
} else {
// Title checks
if (title.toLowerCase().includes(kw)) add('pass', 'Focus keyword found in the title.');
else add('fail', 'Focus keyword is missing from the title.');
// Summary/meta checks
if (summary.toLowerCase().includes(kw)) add('pass', 'Focus keyword found in the summary (meta description).');
else add('warn', 'Focus keyword not found in the summary — helps with search snippets.');
// Keyword in first 10% of content
const intro = content.slice(0, Math.max(50, Math.floor(content.length * 0.1))).toLowerCase();
if (intro.includes(kw)) add('pass', 'Focus keyword appears early in the content.');
else add('warn', 'Focus keyword doesn\'t appear in the opening of the content.');
// Keyword density
if (wordCount > 0) {
const kwWords = kw.split(/\s+/).length;
const occurrences = content.toLowerCase().split(kw).length - 1;
const density = (occurrences * kwWords / wordCount) * 100;
if (density >= 0.5 && density <= 2.5) add('pass', `Keyword density is healthy (${density.toFixed(1)}%).`);
else if (density < 0.5) add('warn', `Keyword density is low (${density.toFixed(1)}%) — use the keyword a bit more naturally.`);
else add('warn', `Keyword density is high (${density.toFixed(1)}%) — may look like keyword stuffing.`);
}
}
// Title length
if (title.length === 0) add('fail', 'Title is empty.');
else if (title.length >= 30 && title.length <= 65) add('pass', `Title length is good (${title.length} characters).`);
else if (title.length < 30) add('warn', `Title is a bit short (${title.length} chars) — aim for 30–65.`);
else add('warn', `Title is long (${title.length} chars) — may get truncated in search results.`);
// Summary length
if (!summary) add('warn', 'No summary/meta description set.');
else if (summary.length >= 100 && summary.length <= 160) add('pass', `Summary length is ideal (${summary.length} characters).`);
else if (summary.length < 100) add('warn', `Summary is short (${summary.length} chars) — aim for 100–160.`);
else add('warn', `Summary is long (${summary.length} chars) — may get cut off (aim for 100–160).`);
// Content length
if (wordCount === 0) add('fail', 'Content is empty.');
else if (wordCount >= 300) add('pass', `Content length is good (${wordCount} words).`);
else add('warn', `Content is thin (${wordCount} words) — aim for 300+ words.`);
// Links
if (/https?:\/\//i.test(content)) add('pass', 'Content includes at least one link.');
else add('warn', 'No links found in the content — internal/external links help SEO.');
const score = total > 0 ? Math.round((passCount / total) * 100) : 0;
seoApplyScore(document.getElementById('post-seo-score'), document.getElementById('post-seo-bar'), score);
seoRenderList(document.getElementById('post-seo-list'), checks);
}
// ── SEO analysis: overall live page (admin-only, on-demand) ─────────────
function runPageSeoCheck() {
const checks = [];
let passCount = 0, total = 0;
const add = (status, text) => { checks.push({ status, text }); total++; if (status==='pass') passCount++; else if (status==='warn') passCount += 0.5; };
const titleTag = document.title || '';
if (titleTag.length >= 30 && titleTag.length <= 65) add('pass', `Page length is good (${titleTag.length} characters).`);
else add('warn', `Page is ${titleTag.length} characters — aim for 30–65.`);
const metaDesc = document.querySelector('meta[name="description"]');
const descLen = metaDesc ? metaDesc.content.length : 0;
if (metaDesc && descLen >= 100 && descLen <= 160) add('pass', `Meta description length is ideal (${descLen} characters).`);
else if (metaDesc) add('warn', `Meta description is ${descLen} characters — aim for 100–160.`);
else add('fail', 'Meta description tag is missing.');
if (document.querySelector('link[rel="canonical"]')) add('pass', 'Canonical tag is present.');
else add('fail', 'Canonical tag is missing.');
const robotsTag = document.querySelector('meta[name="robots"]');
if (robotsTag) add('pass', `Robots meta tag present ("${robotsTag.content}").`);
else add('warn', 'No robots meta tag found (defaults to indexable, but explicit is safer).');
const h1s = document.querySelectorAll('h1');
if (h1s.length === 1) add('pass', 'Exactly one
on the page.');
else if (h1s.length === 0) add('fail', 'No
found on the page.');
else add('warn', `${h1s.length}
tags found — ideally there should be just one.`);
const imgs = document.querySelectorAll('img');
const imgsNoAlt = Array.from(imgs).filter(img => !img.getAttribute('alt'));
if (imgs.length === 0) add('warn', 'No tags detected on the page yet.');
else if (imgsNoAlt.length === 0) add('pass', `All ${imgs.length} images have alt text.`);
else add('warn', `${imgsNoAlt.length} of ${imgs.length} images are missing alt text.`);
if (document.querySelector('script[type="application/ld+json"]')) add('pass', 'Structured data (JSON-LD) is present.');
else add('warn', 'No JSON-LD structured data found.');
if (document.querySelector('meta[name="viewport"]')) add('pass', 'Viewport meta tag is present (mobile-friendly).');
else add('fail', 'Viewport meta tag is missing.');
if (document.querySelector('meta[property="og:title"]') && document.querySelector('meta[property="og:image"]')) {
add('pass', 'Open Graph tags (title & image) are present for social sharing.');
} else {
add('warn', 'Open Graph tags are incomplete — affects link previews on social media.');
}
const score = total > 0 ? Math.round((passCount / total) * 100) : 0;
seoApplyScore(document.getElementById('page-seo-score'), document.getElementById('page-seo-bar'), score);
seoRenderList(document.getElementById('page-seo-list'), checks);
}
function openPageSeoModal() {
runPageSeoCheck();
document.getElementById('page-seo-overlay').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closePageSeoModal() {
document.getElementById('page-seo-overlay').classList.remove('open');
document.body.style.overflow = '';
}
window.blogEditPost = function(id) {
const p = getItems(BLOG_KEY).find(x => x.id === id);
if (p) openWriteModal(p);
};
window.blogDeletePost = function(id) {
if (!confirm('Delete this post?')) return;
saveItems(BLOG_KEY, getItems(BLOG_KEY).filter(p => p.id !== id));
renderPosts();
};
window.blogTogglePublish = function(id) {
const posts = getItems(BLOG_KEY);
const p = posts.find(x => x.id === id);
if (!p) return;
p.published = !isPublished(p);
saveItems(BLOG_KEY, posts);
renderPosts();
showToast(p.published ? '✓ Post published' : '✓ Post unpublished');
};
// ── secret login trigger: triple-click the footer logo ───────────────────
let clickCount = 0, clickTimer;
document.addEventListener('DOMContentLoaded', () => {
const footerLogo = document.querySelector('.footer-logo');
if (footerLogo) {
footerLogo.style.cursor = 'pointer';
footerLogo.addEventListener('click', () => {
clickCount++;
clearTimeout(clickTimer);
clickTimer = setTimeout(() => { clickCount = 0; }, 600);
if (clickCount >= 3) {
clickCount = 0;
if (!isAdmin) openLoginModal();
else { alert('Already logged in as admin ✓'); }
}
});
}
// bindings — login
document.getElementById('do-login').addEventListener('click', doLogin);
document.getElementById('admin-email-input').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
document.getElementById('admin-password-input').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
document.getElementById('close-login').addEventListener('click', closeLoginModal);
document.getElementById('admin-logout-fab').addEventListener('click', doLogout);
// bindings — blog
document.getElementById('blog-admin-fab').addEventListener('click', () => openWriteModal(null));
document.getElementById('export-data-fab').addEventListener('click', (e) => {
if (e.shiftKey) { exportAllData(); } else { exportFullSite(); }
});
document.getElementById('save-post-btn').addEventListener('click', savePost);
document.getElementById('cancel-write').addEventListener('click', closeWriteModal);
document.getElementById('close-write').addEventListener('click', closeWriteModal);
// bindings — SEO
document.getElementById('seo-check-fab').addEventListener('click', openPageSeoModal);
document.getElementById('close-page-seo').addEventListener('click', closePageSeoModal);
document.getElementById('close-page-seo-2').addEventListener('click', closePageSeoModal);
['post-title','post-keyword','post-summary','post-content'].forEach(id => {
document.getElementById(id).addEventListener('input', runBlogSeoCheck);
});
// bindings — projects
document.getElementById('add-project-btn').addEventListener('click', () => openProjectModal(null));
document.getElementById('save-project-btn').addEventListener('click', saveProject);
document.getElementById('cancel-project-write').addEventListener('click', closeProjectModal);
document.getElementById('close-project-write').addEventListener('click', closeProjectModal);
document.getElementById('reorder-projects-btn').addEventListener('click', openProjectsReorder);
document.getElementById('save-reorder-projects-btn').addEventListener('click', saveGenericReorder);
document.getElementById('cancel-reorder-projects-btn').addEventListener('click', closeGenericReorder);
document.getElementById('close-reorder-projects').addEventListener('click', closeGenericReorder);
// bindings — certifications
document.getElementById('add-cert-btn').addEventListener('click', () => openCertModal(null));
document.getElementById('save-cert-btn').addEventListener('click', saveCert);
document.getElementById('cancel-cert-write').addEventListener('click', closeCertModal);
document.getElementById('close-cert-write').addEventListener('click', closeCertModal);
document.getElementById('reorder-certs-btn').addEventListener('click', openCertsReorder);
document.getElementById('save-reorder-certs-btn').addEventListener('click', saveGenericReorder);
document.getElementById('cancel-reorder-certs-btn').addEventListener('click', closeGenericReorder);
document.getElementById('close-reorder-certs').addEventListener('click', closeGenericReorder);
// bindings — blog reorder
document.getElementById('reorder-posts-btn').addEventListener('click', openPostsReorder);
document.getElementById('save-reorder-posts-btn').addEventListener('click', saveGenericReorder);
document.getElementById('cancel-reorder-posts-btn').addEventListener('click', closeGenericReorder);
document.getElementById('close-reorder-posts').addEventListener('click', closeGenericReorder);
// bindings — skills
document.getElementById('add-skill-btn').addEventListener('click', () => openSkillModal(null));
document.getElementById('save-skill-btn').addEventListener('click', saveSkill);
document.getElementById('cancel-skill-write').addEventListener('click', closeSkillModal);
document.getElementById('close-skill-write').addEventListener('click', closeSkillModal);
document.getElementById('reorder-skills-btn').addEventListener('click', openReorderModal);
document.getElementById('save-reorder-btn').addEventListener('click', saveReorder);
document.getElementById('cancel-reorder-btn').addEventListener('click', closeReorderModal);
document.getElementById('close-reorder-skills').addEventListener('click', closeReorderModal);
// close on overlay click
['admin-login-overlay','write-post-overlay','write-project-overlay','write-cert-overlay','write-skill-overlay','reorder-skills-overlay','reorder-projects-overlay','reorder-certs-overlay','reorder-posts-overlay','page-seo-overlay'].forEach(id => {
document.getElementById(id).addEventListener('click', function(e) {
if (e.target === this) {
this.classList.remove('open');
document.body.style.overflow = '';
}
});
});
renderProjects();
renderCertifications();
renderPosts();
renderSkills();
updateHeroStats();
handleDeepLink();
});
})();