";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@import 'https://fonts.googleapis.com/css2?family=Archivo+Black&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap';@font-face{font-family:krungthep;src:url(/fonts/Krungthep.ttf)format('truetype');font-weight:400;font-style:normal;font-display:swap}:root{--primary-hue:26deg;--primary-saturation:42%;--primary-lightness:49%;--rapport-bg:#f7f5f2;--rapport-surface:#ffffff;--rapport-surface2:#fbf9f5;--rapport-dark:#1a1a18;--rapport-dark2:#2d2d28;--rapport-accent:#b07848;--rapport-accent-2:#9a673b;--rapport-accent-3:#7d5430;--rapport-text:#1a1a18;--rapport-text-2:#555550;--rapport-text-3:#888880;--rapport-text-4:#aaaaaa;--rapport-border:#e8e3dc;--rapport-border-2:#d8d2ca}.dark{--primary-hue:26deg;--primary-saturation:42%;--primary-lightness:56%;--color-dark:var(--rapport-dark)}body{background:var(--rapport-bg);color:var(--rapport-text);font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif}.dark body{background:#181614;color:#ece9e3}::selection{background:rgba(176,120,72,.25);color:var(--rapport-dark)}.dark ::selection{background:rgba(176,120,72,.35);color:#fff}.hextra-toc,.content,.prose{font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif}h1,h2,h3,h4,.hextra-home h1,.hextra-home h2,.hextra-home h3,.hx\:text-2xl,.hx\:text-3xl,.hx\:text-4xl,.hx\:text-5xl,.hx\:text-6xl{font-family:playfair display,Georgia,serif!important;font-weight:700!important;letter-spacing:-.01em}.hextra-navbar-title,nav .hextra-navbar-title,nav .hextra-max-navbar-width .hx\:font-bold{font-family:Krungthep,archivo black,sans-serif!important;letter-spacing:-.02em!important;font-weight:900!important}.nav-container{background:rgba(247,245,242,.85)!important;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--rapport-border)!important}.dark .nav-container{background:rgba(24,22,20,.85)!important;border-bottom:1px solid #2d2926!important}.nav-container-blur,.dark .nav-container-blur{background:0 0!important}aside.sidebar-container,.sidebar-container{background:var(--rapport-bg)!important;border-right:1px solid var(--rapport-border)}.sidebar-container a{color:var(--rapport-text-2)}.sidebar-container a:hover{color:var(--rapport-accent);background:var(--rapport-surface)}.sidebar-active-item,.sidebar-container .sidebar-active-item{background:rgba(176,120,72,.1)!important;color:var(--rapport-accent)!important;border-color:rgba(176,120,72,.2)!important}.dark aside.sidebar-container,.dark .sidebar-container{background:#181614!important;border-right:1px solid #2d2926}a{transition:color .15s}.content a,.prose a{color:var(--rapport-accent);text-decoration:none}.content a:hover,.prose a:hover{color:var(--rapport-accent-2);text-decoration:underline}.hextra-card,.hextra-card:hover,.hextra-card:focus,.hextra-card:active,.hextra-card *,.hextra-card:hover *{text-decoration:none!important}a.hextra-card:hover,a.hextra-card:focus{outline:none;text-decoration:none!important}.rapport-hero-actions{display:flex;gap:18px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:8px;margin-bottom:8px}.rapport-btn{display:inline-flex;align-items:center;gap:9px;border-radius:999px;padding:14px 30px;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif;font-size:12px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;text-decoration:none!important;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,background .18s ease,border-color .18s ease;user-select:none;white-space:nowrap}.rapport-btn-primary{background:#fff;color:var(--rapport-text)!important;border:1px solid var(--rapport-border);box-shadow:0 1px rgba(255,255,255,.9)inset,0 2px 4px rgba(0,0,0,4%),0 8px 20px rgba(0,0,0,.1),0 16px 40px rgba(0,0,0,8%)}.rapport-btn-primary:hover{background:#fff;border-color:var(--rapport-border-2);color:var(--rapport-accent-2)!important;transform:translateY(-2px);box-shadow:0 1px rgba(255,255,255,.9)inset,0 4px 8px rgba(0,0,0,6%),0 14px 28px rgba(0,0,0,.12),0 24px 56px rgba(176,120,72,.18)}.rapport-btn-primary:active{transform:translateY(0);box-shadow:0 1px rgba(255,255,255,.9)inset,0 2px 4px rgba(0,0,0,6%),0 4px 12px rgba(0,0,0,8%)}.rapport-btn-secondary{background:0 0;color:var(--rapport-text-2)!important;border:1.5px solid var(--rapport-border-2);box-shadow:0 2px 6px rgba(0,0,0,3%)}.rapport-btn-secondary:hover{background:rgba(255,255,255,.5);border-color:var(--rapport-text-3);color:var(--rapport-text)!important;transform:translateY(-2px);box-shadow:0 6px 14px rgba(0,0,0,6%),0 12px 28px rgba(0,0,0,4%)}.rapport-btn-secondary:active{transform:translateY(0)}.dark .rapport-btn-primary{background:#2a2722;color:#ece9e3!important;border-color:#3a3530;box-shadow:0 1px rgba(255,255,255,4%)inset,0 2px 4px rgba(0,0,0,.3),0 10px 24px rgba(0,0,0,.45)}.dark .rapport-btn-primary:hover{background:#322e28;color:var(--rapport-accent)!important;box-shadow:0 1px rgba(255,255,255,4%)inset,0 4px 8px rgba(0,0,0,.4),0 16px 32px rgba(0,0,0,.5),0 24px 56px rgba(176,120,72,.22)}.dark .rapport-btn-secondary{border-color:#3a3530;color:#b8b2a8!important}.dark .rapport-btn-secondary:hover{background:rgba(255,255,255,4%);border-color:#6a6258;color:#ece9e3!important}.hextra-badge{background:var(--rapport-surface)!important;border:1px solid var(--rapport-border)!important;border-radius:20px!important;padding:5px 14px!important;font-size:10px!important;letter-spacing:.12em!important;color:var(--rapport-text-4)!important;text-transform:uppercase!important;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif!important;box-shadow:0 2px 8px rgba(0,0,0,6%),0 1px 3px rgba(0,0,0,4%)}.hextra-feature-card{background:var(--rapport-surface)!important;border:1px solid var(--rapport-border)!important;border-radius:14px!important;transition:box-shadow .2s,transform .2s,border-color .2s!important}.hextra-feature-card:hover{border-color:var(--rapport-border-2)!important;transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,0,0,9%),0 2px 8px rgba(0,0,0,5%)!important}.hextra-feature-card h3{font-family:playfair display,serif!important;color:var(--rapport-text)!important;font-weight:700!important}.hextra-feature-card p{color:var(--rapport-text-2)!important;font-size:12px!important;line-height:1.8!important}.dark .hextra-feature-card{background:#211e1a!important;border-color:#2d2926!important}.dark .hextra-feature-card h3{color:#ece9e3!important}.dark .hextra-feature-card p{color:#b8b2a8!important}.hextra-card{background:var(--rapport-surface);border:1px solid var(--rapport-border);border-radius:12px;transition:border-color .2s,transform .2s}.hextra-card:hover{border-color:var(--rapport-accent-2);transform:translateY(-1px)}.hextra-code-block{background:#fff!important;border:1px solid var(--rapport-border)!important;border-radius:12px!important;overflow:hidden}.hextra-code-block pre,.hextra-code-block .highlight,.hextra-code-block .chroma{background:0 0!important;border:none!important;border-radius:0!important;margin:0!important}pre{background:var(--rapport-surface2);border:none}code,pre,pre code,kbd,samp,tt{font-family:ui-monospace,sf mono,Menlo,Monaco,Consolas,liberation mono,monospace!important}code{color:var(--rapport-accent-3)!important;background:rgba(176,120,72,.1)!important;padding:2px 6px;border-radius:4px;font-size:.9em}pre code{color:var(--rapport-text)!important;background:0 0!important;padding:0;font-size:.9em}.dark .hextra-code-block{background:#1f1c19!important;border-color:#2d2926!important}.dark .hextra-code-block pre,.dark .hextra-code-block .highlight,.dark .hextra-code-block .chroma{background:0 0!important;border:none!important}.dark code{color:var(--rapport-accent)!important;background:rgba(176,120,72,.15)!important}.dark pre code{color:#ece9e3!important}.hextra-callout{background:var(--rapport-surface)!important;border-color:var(--rapport-border-2)!important}.hextra-footer,footer{background:var(--rapport-surface2);border-top:1px solid var(--rapport-border);color:var(--rapport-text-3)}.hextra-footer a{color:var(--rapport-text-2)}.hextra-footer a:hover{color:var(--rapport-accent)}.dark .hextra-footer,.dark footer{background:#15130f;border-top:1px solid #2d2926;color:#888880}.hextra-toc a{color:var(--rapport-text-3)}.hextra-toc a:hover,.hextra-toc .active{color:var(--rapport-accent)!important}.hextra-toc div:has(>#backToTop),[data-toggle-animation]{background:var(--rapport-bg)!important;box-shadow:none!important;border-top-color:var(--rapport-border)!important}.dark .hextra-toc div:has(>#backToTop),.dark [data-toggle-animation]{background:#181614!important;border-top-color:#2d2926!important;box-shadow:none!important}.hextra-search-wrapper input{background:var(--rapport-surface)!important;border:1px solid var(--rapport-border)!important;color:var(--rapport-text)!important}.hextra-search-wrapper input:focus{border-color:var(--rapport-accent-2)!important}table{border-color:var(--rapport-border)!important}thead{background:var(--rapport-surface)!important}th,td{border-color:var(--rapport-border)!important}.rapport-logo-card{background:var(--rapport-dark);border:1px solid var(--rapport-dark2);border-radius:999px;padding:28px 64px 26px;display:inline-block;box-shadow:6px 0 20px rgba(0,0,0,.18),0 6px 16px rgba(0,0,0,.12);text-align:center;margin:0 auto 32px}.rapport-logo-text{font-family:krungthep,archivo black,sans-serif!important;font-size:48px;letter-spacing:-.02em;color:#f0ede8;line-height:.95;font-weight:400}.rapport-logo-sub{font-size:9px;letter-spacing:.15em;color:#f0ede8;text-transform:uppercase;margin-top:8px;font-weight:500}.rapport-meta{display:flex;gap:6px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:32px;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif}.rapport-meta-item{font-size:10px;letter-spacing:.08em;color:var(--rapport-text-4);text-transform:uppercase;padding:0 10px;border-right:1px solid var(--rapport-border)}.rapport-meta-item:last-child{border-right:none}.rapport-status,.rapport-status.active,.rapport-status.planned,.rapport-status.stable,.rapport-status.new{display:inline-block;font-size:10px;letter-spacing:.12em;text-transform:uppercase;padding:5px 14px;border-radius:999px;margin-bottom:12px;font-weight:600;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif;background:#fff;color:var(--rapport-text);border:1px solid var(--rapport-border);box-shadow:0 1px rgba(255,255,255,.9)inset,0 1px 2px rgba(0,0,0,4%),0 3px 8px rgba(0,0,0,6%)}.dark .rapport-status,.dark .rapport-status.active,.dark .rapport-status.planned,.dark .rapport-status.stable,.dark .rapport-status.new{background:#2a2722;color:#ece9e3;border-color:#3a3530;box-shadow:0 1px rgba(255,255,255,4%)inset,0 1px 2px rgba(0,0,0,.3),0 4px 10px rgba(0,0,0,.35)}.rapport-stack-bar{padding:20px 0;border-top:1px solid var(--rapport-border);display:flex;align-items:center;gap:20px;flex-wrap:wrap;margin-top:32px;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif}.rapport-stack-label{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--rapport-text-4);flex-shrink:0}.rapport-stack-items{display:flex;gap:8px;flex-wrap:wrap}.rapport-stack-item{font-size:10px;letter-spacing:.06em;color:var(--rapport-text-3);background:var(--rapport-surface);border:1px solid var(--rapport-border);border-radius:6px;padding:4px 10px}.content article{max-width:100%}.hextra-home{align-items:center!important;text-align:center;max-width:100%}.hextra-home>p,.hextra-home .not-prose.hx\:text-xl{font-family:playfair display,serif!important;font-size:clamp(16px,2.2vw,24px)!important;font-weight:400!important;color:var(--rapport-text-3)!important;max-width:560px!important;margin-left:auto!important;margin-right:auto!important;line-height:1.55!important;text-align:center}.hextra-home>div.hx\:flex,.hextra-home>.hx\:flex{justify-content:center;align-items:center}.hextra-home .hextra-hero-button,a.hextra-hero-button{background:var(--rapport-dark);color:#f0ede8!important;border-radius:20px;padding:13px 28px;font-family:inter,-apple-system,BlinkMacSystemFont,helvetica neue,sans-serif!important;font-size:11px!important;font-weight:500!important;letter-spacing:.07em!important;text-transform:uppercase!important;box-shadow:0 4px 14px rgba(0,0,0,.2);transition:all .18s;display:inline-flex;align-items:center;gap:9px}.hextra-home .hextra-feature-grid,.hextra-home .rapport-stack-bar{text-align:left;width:100%}.hextra-home .hextra-feature-card *{text-align:left}.rapport-stack-bar{justify-content:flex-start;text-align:left}.hextra-home h2,.hextra-home>div>h2{text-align:center;margin-left:auto;margin-right:auto}.hextra-home .rapport-logo-card{margin-left:auto;margin-right:auto;display:block}.hextra-home .hextra-badge,.hextra-home>p:has(.hextra-badge){margin-left:auto;margin-right:auto;display:inline-flex!important}body:has(.hextra-home)::before{content:"";position:fixed;top:5%;left:50%;transform:translateX(-50%);width:720px;height:720px;background:radial-gradient(circle,rgba(176,120,72,8%) 0%,transparent 60%);pointer-events:none;z-index:0}.dark body:has(.hextra-home)::before{background:radial-gradient(circle,rgba(176,120,72,.12) 0%,transparent 60%)}.hextra-navbar-title,nav a[href="/"] span,nav [class*=font-bold]{font-family:krungthep,archivo black,sans-serif!important;letter-spacing:-.02em!important;font-weight:400!important;font-size:23px!important;line-height:1!important}.rapport-meta{justify-content:center;text-align:center}.hosting-wrap{min-height:calc(100vh - var(--navbar-height,64px));display:flex;align-items:flex-start;justify-content:center;padding:56px 20px 80px;width:100%}.hosting-card{background:var(--rapport-surface);border:1px solid var(--rapport-border);border-radius:16px;padding:36px 34px;width:100%;max-width:400px;box-shadow:0 1px 2px rgba(0,0,0,4%),0 10px 36px rgba(0,0,0,7%)}.dark .hosting-card{background:#211e1a;border-color:#2d2926}.hosting-card.wide{max-width:1040px}.hosting-title{font-family:playfair display,Georgia,serif;font-weight:700;font-size:26px;margin:0 0 6px;color:var(--rapport-text);text-align:center}.hosting-sub{text-align:center;color:var(--rapport-text-3);font-size:13px;margin-bottom:26px}.hosting-label{display:block;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--rapport-text-3);margin-bottom:6px}.hosting-input{width:100%;box-sizing:border-box;background:var(--rapport-surface2);border:1.5px solid var(--rapport-border);border-radius:10px;padding:11px 14px;font-family:inter,sans-serif;font-size:14px;color:var(--rapport-text);outline:none;margin-bottom:16px;transition:border-color .15s,box-shadow .15s}.dark .hosting-input{background:#1a1714;border-color:#2d2926;color:#ece9e3}.hosting-input:focus{border-color:var(--rapport-accent);box-shadow:0 0 0 3px rgba(176,120,72,.15)}.hosting-btn{width:100%;box-sizing:border-box;padding:13px;border:none;border-radius:10px;cursor:pointer;font-family:inter,sans-serif;font-size:13px;font-weight:600;letter-spacing:.03em;transition:background .15s,transform .1s}.hosting-btn-primary{background:var(--rapport-accent);color:#fff}.hosting-btn-primary:hover{background:var(--rapport-accent-2)}.hosting-btn-primary:active{transform:translateY(1px)}.hosting-btn-dark{background:var(--rapport-dark);color:#f0ede8}.hosting-btn:disabled{opacity:.6;cursor:default}.hosting-link{background:0 0;border:none;color:var(--rapport-accent);font-family:inter,sans-serif;font-size:12px;cursor:pointer;text-decoration:underline;padding:0}.hosting-foot{text-align:center;margin-top:18px;font-size:12px;color:var(--rapport-text-3)}.hosting-msg{padding:10px 14px;border-radius:9px;font-size:13px;margin-bottom:16px}.hosting-msg.err{background:#f8ece4;border:1px solid #e0b896;color:#9a4a1e}.hosting-msg.ok{background:#ecefe6;border:1px solid #c2cdb0;color:#4a5a32}.hosting-plans{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}@media(max-width:860px){.hosting-plans{grid-template-columns:1fr}}.hosting-plan{position:relative;display:flex;flex-direction:column;background:var(--rapport-surface2);border:1px solid var(--rapport-border);border-radius:14px;padding:24px}.dark .hosting-plan{background:#1a1714;border-color:#2d2926}.hosting-plan.rec{border-color:var(--rapport-accent);box-shadow:0 0 0 2px var(--rapport-accent),0 18px 44px rgba(176,120,72,.18);background:linear-gradient(180deg,rgba(176,120,72,6%),var(--rapport-surface) 120px);transform:translateY(-8px)}@media(max-width:860px){.hosting-plan.rec{transform:none}}.hosting-plan.rec .pprice{color:var(--rapport-accent-2)}.hosting-plan .pbadge{position:absolute;top:-10px;right:16px;background:var(--rapport-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:4px 10px;border-radius:6px}.hosting-plan .pname{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--rapport-text-3)}.hosting-plan .pprice{font-family:playfair display,serif;font-size:32px;font-weight:700;margin:8px 0 4px;color:var(--rapport-text)}.hosting-plan ul{list-style:none;padding:0;margin:12px 0 0;font-size:13px;color:var(--rapport-text-2);flex:1}.hosting-plan li{padding:6px 0;border-bottom:1px solid var(--rapport-border)}.hosting-plan li:last-child{border-bottom:none}.hosting-plan .hosting-btn{margin-top:16px}.hosting-row{display:flex;justify-content:space-between;align-items:center;padding:14px 0;border-bottom:1px solid var(--rapport-border);font-size:14px;gap:16px}.hosting-row:last-child{border-bottom:none}.hosting-tabs{display:flex;gap:4px;border-bottom:1px solid var(--rapport-border);margin-bottom:22px}.hosting-tab{background:0 0;border:none;cursor:pointer;padding:10px 16px;font-family:inter,sans-serif;font-size:13px;font-weight:500;color:var(--rapport-text-3);border-bottom:2px solid transparent;margin-bottom:-1px}.hosting-tab:hover{color:var(--rapport-text)}.hosting-tab.active{color:var(--rapport-accent);border-bottom-color:var(--rapport-accent)}.admin-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}@media(max-width:700px){.admin-stats{grid-template-columns:repeat(2,1fr)}}.admin-stat{background:var(--rapport-surface2);border:1px solid var(--rapport-border);border-radius:12px;padding:18px;text-align:center}.dark .admin-stat{background:#1a1714;border-color:#2d2926}.admin-stat-num{font-family:playfair display,serif;font-size:30px;font-weight:700;color:var(--rapport-text);line-height:1}.admin-stat-label{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--rapport-text-3);margin-top:8px}.admin-table{width:100%;border-collapse:collapse;font-size:14px}.admin-table th{text-align:left;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--rapport-text-3);padding:10px 12px;border-bottom:1px solid var(--rapport-border)}.admin-table td{padding:12px;border-bottom:1px solid var(--rapport-border);vertical-align:top}.admin-tag{display:inline-block;background:var(--rapport-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:5px;vertical-align:middle}.admin-stat-sub{font-size:11px;color:var(--rapport-accent-2);margin-top:4px}.admin-planbar{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 4px}.admin-planchip{font-size:12px;background:var(--rapport-surface2);border:1px solid var(--rapport-border);border-radius:8px;padding:6px 12px}.dark .admin-planchip{background:#1a1714;border-color:#2d2926}.admin-toolbar{display:flex;gap:10px;margin:22px 0 10px}.admin-search{flex:1;margin:0}.admin-filter{width:180px;margin:0}@media(max-width:600px){.admin-toolbar{flex-direction:column}.admin-filter{width:100%}}.admin-row{cursor:pointer;transition:background .12s}.admin-row:hover{background:var(--rapport-surface2)}.dark .admin-row:hover{background:#1a1714}.admin-chevron{color:var(--rapport-text-3);font-size:20px;line-height:1}.admin-badge{display:inline-block;font-size:11px;font-weight:600;padding:2px 9px;border-radius:6px;background:rgba(176,120,72,.12);color:var(--rapport-accent-2);text-transform:capitalize}.admin-badge.ok{background:#ecefe6;color:#4a5a32}.admin-badge.warn{background:#f8e4e4;color:#9a3a3a}.admin-section{font-weight:700;font-size:13px;color:var(--rapport-text);margin:24px 0 6px;padding-bottom:6px;border-bottom:1px solid var(--rapport-border)}.admin-mini{width:auto;padding:6px 14px;font-size:12px;text-decoration:none;background:var(--rapport-surface2);color:var(--rapport-text);border:1px solid var(--rapport-border)}.admin-mini:hover{border-color:var(--rapport-accent)}.admin-mini.warn{color:#9a3a3a;border-color:#e0b0b0}.admin-mini.ok{color:#4a5a32;border-color:#c2cdb0}.dark .admin-mini{background:#1a1714;color:#ece9e3;border-color:#2d2926}.rapport-footer-links{display:grid;grid-template-columns:repeat(4,1fr);gap:32px;padding:40px 0 8px;width:100%;font-family:inter,-apple-system,sans-serif}@media(max-width:700px){.rapport-footer-links{grid-template-columns:repeat(2,1fr);gap:24px}}.rapport-footer-head{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--rapport-text-3);margin-bottom:12px}.rapport-footer-col a{display:block;font-size:13px;color:var(--rapport-text-2);text-decoration:none;padding:4px 0}.rapport-footer-col a:hover{color:var(--rapport-accent)}.rapport-footer-note{font-size:12px;color:var(--rapport-text-3);padding:4px 0;line-height:1.5}.hosting-auth{display:grid;grid-template-columns:1fr 1fr;gap:0}@media(max-width:760px){.hosting-auth{grid-template-columns:1fr}.hosting-auth-aside{display:none}}.hosting-auth-form{padding:8px 36px 8px 8px}.hosting-auth-aside{border-left:1px solid var(--rapport-border);padding:8px 8px 8px 36px;display:flex;flex-direction:column;justify-content:center}.hosting-aside-list{list-style:none;padding:0;margin:0}.hosting-aside-list li{font-size:14px;color:var(--rapport-text-2);padding:9px 0;border-bottom:1px solid var(--rapport-border-2)}.hosting-aside-list li:last-child{border-bottom:none}.dark .hosting-auth-aside{border-left-color:#2d2926}
\ No newline at end of file
diff --git a/public/datenschutz/index.html b/public/datenschutz/index.html
index a396cf1..63d895b 100644
--- a/public/datenschutz/index.html
+++ b/public/datenschutz/index.html
@@ -60,8 +60,8 @@ Zahlungsdaten: Die Zahlungsabwicklung erfolgt über Stripe. Wir speichern keine
2. Welche Daten wir verarbeiten Kontodaten: E-Mail-Adresse, Passwort (verschlüsselt gespeichert), sowie optional Firma, Ansprechperson, Rechnungsadresse und Telefon.
Zahlungsdaten: Die Zahlungsabwicklung erfolgt über Stripe. Wir speichern keine vollständigen Kreditkartendaten; Stripe verarbeitet diese als eigener Verantwortlicher.">
-
-
+
+
diff --git a/public/docs/arbeitsablauf/index.html b/public/docs/arbeitsablauf/index.html
index b616c08..7c58e9f 100644
--- a/public/docs/arbeitsablauf/index.html
+++ b/public/docs/arbeitsablauf/index.html
@@ -54,8 +54,8 @@
-
-
+
+
diff --git a/public/docs/changelog/index.html b/public/docs/changelog/index.html
index 116f0c4..ba9cc31 100644
--- a/public/docs/changelog/index.html
+++ b/public/docs/changelog/index.html
@@ -67,8 +67,8 @@ Neu / Verbessert
Diverse Verbesserungen und Bugfixes (Details werden im Release auf Gitea gepflegt) Bekannte Einschränkungen
Builds sind Tauri-signiert, aber noch nicht Apple-notarisiert — siehe Installation § Gatekeeper Linux- und Windows-Builds noch nicht verfügbar 0.8.0–0.8.1 — Patch-Releases Patch-Reihe mit kleineren Verbesserungen und Bugfixes. Details siehe Releases auf Gitea .">
-
-
+
+
diff --git a/public/docs/datenhaltung/index.html b/public/docs/datenhaltung/index.html
index 9f29b1a..9f46857 100644
--- a/public/docs/datenhaltung/index.html
+++ b/public/docs/datenhaltung/index.html
@@ -62,8 +62,8 @@ Diese Seite beschreibt die Desktop-App (Single-User). Wer im Team arbeitet und R
Speicherort (Desktop-App) Die Desktop-App speichert alles lokal — keine Cloud, kein Server.
macOS ~/Library/Application Support/com.rapport.app/ Dort liegt eine einzelne localStorage-Datenbank des WebView, in der alle Rapport-Daten als JSON unter dem Key studio_data_v1 gespeichert sind:">
-
-
+
+
diff --git a/public/docs/einrichtung/index.html b/public/docs/einrichtung/index.html
index faaa5ed..beb95ff 100644
--- a/public/docs/einrichtung/index.html
+++ b/public/docs/einrichtung/index.html
@@ -59,8 +59,8 @@ Reihenfolge Die Reihenfolge ist wichtig — jede Stufe baut auf der vorherigen a
Reihenfolge Die Reihenfolge ist wichtig — jede Stufe baut auf der vorherigen auf:
1. Bürodaten → 2. Mitarbeiter → 3. Kunden → 4. Projekte ▼ ▼ ▼ ▼ Briefbogen, Zeiterfassung, Adressen, Zeiterfassung, QR-Schein, Lohn Rechnungen Rechnungen Login 1 · Bürodaten Einstellungen → Bürodaten">
-
-
+
+
diff --git a/public/docs/entwicklung/index.html b/public/docs/entwicklung/index.html
index 108fc48..9c07413 100644
--- a/public/docs/entwicklung/index.html
+++ b/public/docs/entwicklung/index.html
@@ -125,8 +125,8 @@ macOS: Xcode Command Line Tools (xcode-select --install) Windows: Microsoft C
Voraussetzungen Tool Version Node.js ≥ 20 (für Vite 8) npm ≥ 10 Rust toolchain ≥ 1.77.2 (via rustup) Plattform-Tools siehe Tauri Prerequisites Plattform-spezifisch:
macOS: Xcode Command Line Tools (xcode-select --install) Windows: Microsoft C++ Build Tools + WebView2 Linux: webkit2gtk-4.1, librsvg2-dev, libayatana-appindicator3-dev, build-essential Setup git clone https://git.kgva.ch/karim/RAPPORT.git cd RAPPORT/APP npm install Entwicklung Web-Modus (HMR, schnellster Loop) npm run dev # http://localhost:3000 Hot-Module-Replacement Schnellster Iteration-Loop für UI-Arbeit Datenpersistierung: Browser-localStorage Native Window (Tauri-Fenster mit Desktop-Integration) npx tauri dev Echtes Tauri-Fenster System-Tray, Updater, native APIs verfügbar Erster Start dauert lange (Rust-Compile) Architektur in einem Absatz RAPPORT ist eine monolithische SPA: ein React-Root in App.jsx hält den gesamten App-State in einem useState({...}), persistiert ihn synchron in localStorage unter studio_data_v1, und übergibt ihn als Props an lazy-geladene Views. Kein Routing-Framework, kein State-Library, kein TypeScript, kein CSS-Framework. Der Rust-Teil ist 109 Zeilen und macht nur drei Dinge: System-Tray, Window-Hide-on-Close, Plugin-Registrierung (Updater, Process, Log). Keine #[tauri::command] — Frontend ↔ Backend kommuniziert nur über das Event rapport:navigate (Tray → Frontend).">
-
-
+
+
diff --git a/public/docs/erste-schritte/index.html b/public/docs/erste-schritte/index.html
index e9411ed..2d60099 100644
--- a/public/docs/erste-schritte/index.html
+++ b/public/docs/erste-schritte/index.html
@@ -47,8 +47,8 @@ Die Pre-Release-Builds sind signiert über Tauri, aber (noch) nicht über die Ap
01 · Installation DMG von Gitea Releases herunterladen. Rapport in den Programme-Ordner ziehen. Beim ersten Start: Systemeinstellungen → Datenschutz & Sicherheit öffnen und Rapport zulassen.
Die Pre-Release-Builds sind signiert über Tauri, aber (noch) nicht über die Apple-Notarisierung gegangen — daher der manuelle Freigabe-Schritt.">
-
-
+
+
diff --git a/public/docs/index.html b/public/docs/index.html
index bdd6f78..71f212f 100644
--- a/public/docs/index.html
+++ b/public/docs/index.html
@@ -37,8 +37,8 @@ Erste Schritte Quick-StartIn sechs Schritten von Null zur ersten Rechnung. Insta
-
-
+
+
diff --git a/public/docs/installation/index.html b/public/docs/installation/index.html
index 56333ec..c91881e 100644
--- a/public/docs/installation/index.html
+++ b/public/docs/installation/index.html
@@ -74,8 +74,8 @@ Voraussetzungen Plattform Status Versionen macOS Apple Silicon (M1 – M4) ✅ U
-
-
+
+
diff --git a/public/docs/troubleshooting/index.html b/public/docs/troubleshooting/index.html
index 8ff4d2b..3afae88 100644
--- a/public/docs/troubleshooting/index.html
+++ b/public/docs/troubleshooting/index.html
@@ -62,8 +62,8 @@ App startet nicht “Rapport ist beschädigt” beim ersten Start Ursache: macOS
Lösung: siehe Installation § 3. Kurz:
xattr -d com.apple.quarantine /Applications/Rapport.app App startet, zeigt aber schwarzen Bildschirm Ursache: WebView-Cache korrupt.">
-
-
+
+
diff --git a/public/docs/web-modus/index.html b/public/docs/web-modus/index.html
index 4dea807..5e48632 100644
--- a/public/docs/web-modus/index.html
+++ b/public/docs/web-modus/index.html
@@ -97,8 +97,8 @@ Wann brauchst du das? Anwendungsfall Empfehlung Solo-Büro, ein Mac Desktop-App
Diese Seite bleibt als Referenz erhalten, der empfohlene Weg für Multi-User-Setups ist Rapport Server.
Wann brauchst du das? Anwendungsfall Empfehlung Solo-Büro, ein Mac Desktop-App — siehe Installation 2–5 Personen, gleicher Standort Rapport Server auf einem Mac Mini im LAN Verteiltes Team / Home-Office Rapport Server mit SSL + Reverse Proxy Hosted Backend (eigener VPS) Rapport Server auf Linux-VPS Architektur (Kurzfassung) ┌────────────┐ HTTPS ┌──────────────┐ SQL ┌────────────┐ │ Browser │ ──────────────│ nginx │ ─────────────│ Postgres │ │ / Desktop │ │ (Frontend) │ │ + GoTrue │ └────────────┘ └──────────────┘ │ + REST │ │ + Realtime │ │ + Storage │ └────────────┘ Frontend: dieselbe React-App, aber Vite-Build statt Tauri (npm run build) Backend: Postgres-Stack (Rapport Server) Auth: E-Mail / Passwort über GoTrue Storage: Belege, Logos in Object-Storage Setup Alle Setup-Schritte (Repo klonen, .env erstellen, Migrations syncen, Docker-Compose starten, Reverse-Proxy konfigurieren) sind in Rapport Server dokumentiert.">
-
-
+
+
diff --git a/public/downloads/index.html b/public/downloads/index.html
index 3a62684..88d7a89 100644
--- a/public/downloads/index.html
+++ b/public/downloads/index.html
@@ -46,8 +46,8 @@ Komponente Für wen Aktuelle Version Desktop-App Solo-Büro, lokale Datenhaltung
Neuerungen — siehe Changelog für Details.
macOS Architektur Download Apple Silicon (M1–M4) RAPPORT_0.8.2_aarch64.dmg Intel (x86_64) auf Anfrage Erstinstallation: Systemeinstellungen → Datenschutz & Sicherheit öffnen und Rapport zulassen. Die Builds sind über Tauri signiert, aber (noch) nicht Apple-notarisiert.">
-
-
+
+
diff --git a/public/faq/index.html b/public/faq/index.html
index 1c75c08..36dcbfc 100644
--- a/public/faq/index.html
+++ b/public/faq/index.html
@@ -40,8 +40,8 @@ Ist Rapport kostenlos? Ja, vollständig. Quellcode unter GNU AGPL-3.0-or-later.
Für wen ist Rapport gedacht? Für kleine Architekturbüros in der Schweiz. Die Strukturen folgen der SIA 102 (Phasen, Honorar). Rapport wurde für den internen Gebrauch entwickelt und wird mit diesem Projekt öffentlich geteilt.
Ist Rapport kostenlos? Ja, vollständig. Quellcode unter GNU AGPL-3.0-or-later. Keine versteckten Kosten, keine Telemetrie. Die Daten bleiben lokal auf deinem Computer bzw. in deiner Instanz. Du hast die komplette Kontrolle über deine Daten.">
-
-
+
+
diff --git a/public/features/auto-updater/index.html b/public/features/auto-updater/index.html
index 103be3d..3ac87c9 100644
--- a/public/features/auto-updater/index.html
+++ b/public/features/auto-updater/index.html
@@ -67,8 +67,8 @@ Rapport prüft beim Start automatisch auf neue Versionen und installiert Updates
Funktionsweise Beim App-Start:
Abfrage gegen https://git.kgva.ch/karim/RAPPORT/releases/latest.json Versionsvergleich mit lokaler version im Tauri-Bundle Bei neuer Version → Update-Dialog Bei Bestätigung → Download + Signaturprüfung + Installation + Neustart Sicherheit Updates werden mit dem Tauri-Updater-Schlüssel signiert Manipulierte Downloads werden abgelehnt Quellcode und Build sind reproduzierbar (Gitea CI, geplant) Optionen Update installieren — Download & Neustart Diese Version überspringen — überspringt nur diese eine Version Später erinnern — beim nächsten Start erneut fragen Updates können in den Einstellungen komplett deaktiviert werden.">
-
-
+
+
diff --git a/public/features/index.html b/public/features/index.html
index d14d9e1..317ae9d 100644
--- a/public/features/index.html
+++ b/public/features/index.html
@@ -37,8 +37,8 @@ Module ZeiterfassungTages- & Wochenraster mit Drag & Drop. Rechnungen &a
-
-
+
+
diff --git a/public/features/mitarbeiter/index.html b/public/features/mitarbeiter/index.html
index e92702a..8804263 100644
--- a/public/features/mitarbeiter/index.html
+++ b/public/features/mitarbeiter/index.html
@@ -63,8 +63,8 @@ Ferienverwaltung, interne Stunden / Absenzen und Lohnabrechnung. Jahresabschluss
Stammdaten Pro Mitarbeiter:
Name, Eintrittsdatum, Pensum (%) Stundensatz (intern, für Rechnungen) Ferienanspruch (Tage / Jahr) Lohn (monatlich, brutto) Ferienverwaltung Prorata-Berechnung bei Eintritt unter Jahr Ferien-Saldo in Tagen (live) Halbtage unterstützt Übertrag ins Folgejahr oder Auszahlung Absenzen Krankheit, Militär, Mutterschaft, unbezahlter Urlaub — getrennt erfasst, mit Auswertung pro Mitarbeiter.">
-
-
+
+
diff --git a/public/features/projekte/index.html b/public/features/projekte/index.html
index 8b60a2f..d169c7c 100644
--- a/public/features/projekte/index.html
+++ b/public/features/projekte/index.html
@@ -57,8 +57,8 @@ Projekte nach SIA 102 mit Budget, Phasen und Beteiligten. Erstellung aus einer O
Projektstruktur Jedes Projekt besitzt:
Stammdaten — Nummer, Bezeichnung, Standort, Bauschätzwert Kunde — verknüpft mit Kundendatenbank Beteiligte — Bauleitung, Fachplaner, Behörden Phasen — SIA 102 (Vorprojekt, Bauprojekt, Ausschreibung, …) Budget — Gesamthonorar, pro Phase aufgeteilt SIA 102 Standard-Phasenverteilung wird vorgeschlagen, kann pro Projekt überschrieben werden.">
-
-
+
+
diff --git a/public/features/protokolle/index.html b/public/features/protokolle/index.html
index 952258b..42a496b 100644
--- a/public/features/protokolle/index.html
+++ b/public/features/protokolle/index.html
@@ -80,8 +80,8 @@ Briefe Brief-Editor mit:
Empfänger aus Kundendatenbank Bezugszeile, Anrede, Text, Grussformel Briefbogen-Vorlage mit Logo PDF-Export Lieferscheine Pro Lieferung:
Empfänger, Datum, Bezug Positionen (Plan-Nummer, Bezeichnung, Anzahl, Massstab) Unterschriftenfeld Konsistentes Erscheinungsbild über alle Dokumenttypen — eine Briefbogen-Vorlage, mehrere Verwendungen.">
-
-
+
+
diff --git a/public/features/rechnungen/index.html b/public/features/rechnungen/index.html
index 58b0eed..e6911c0 100644
--- a/public/features/rechnungen/index.html
+++ b/public/features/rechnungen/index.html
@@ -93,8 +93,8 @@ Workflow Offerte erstellen — auf Basis SIA 102 oder pauschal Kunde nimmt an
Ausgelesen aus:
Bürodaten — IBAN, Empfänger-Adresse Kundendaten — Schuldner-Adresse Rechnungs-Daten — Betrag, Referenz, Zusatzinformation Honorarmodelle Modell Berechnung Verwendung Stundensatz Aus Zeiterfassung × Mitarbeiter-Stundensatz Kleinaufträge, Beratung SIA-Phasen Bauschätzwert × Honorarsatz × Phasenanteil Reguläre Architektur-Aufträge Pauschal Fester Betrag Auf Wunsch des Kunden PDF-Export Druckfertige Rechnung inkl. QR-Schein. Layout aus dem Büro-Briefbogen (mit Logo). Mehrsprachig DE/FR/IT (geplant).">
-
-
+
+
diff --git a/public/features/spesen/index.html b/public/features/spesen/index.html
index 9bc4916..87eb2fe 100644
--- a/public/features/spesen/index.html
+++ b/public/features/spesen/index.html
@@ -79,8 +79,8 @@ Datum, Betrag, Kategorie Beleg-Upload (PDF, JPG, PNG) Projekt-Zuordnung (optiona
Jahresbudget Übersicht über:
Einnahmen — Rechnungsbeträge, sortiert nach Eingang Ausgaben — Spesen, Bürokosten, Löhne, Sozialabzüge Saldo pro Monat / Quartal / Jahr Auswertung Einnahmen pro Kunde / Projekt Ausgaben pro Kategorie / Mitarbeiter Erfolgsrechnung pro Geschäftsjahr (vereinfacht) Verwandte Module Mitarbeiter — Spesen-Erstattung in der Lohnabrechnung Rechnungen — Einnahmen-Quelle">
-
-
+
+
diff --git a/public/features/system-tray/index.html b/public/features/system-tray/index.html
index bdfa525..e0074e4 100644
--- a/public/features/system-tray/index.html
+++ b/public/features/system-tray/index.html
@@ -81,8 +81,8 @@ Verhalten Aktion Verhalten Fenster schliessen (⌘W oder rotes X) App läuft im
Schnellzugriff über die Menüleiste mit Hide-on-Close. Beim Schliessen läuft Rapport im Hintergrund weiter — Cmd+Q beendet die App vollständig.
Verhalten Aktion Verhalten Fenster schliessen (⌘W oder rotes X) App läuft im Tray weiter Cmd+Q App wird vollständig beendet Klick auf Tray-Icon Fenster nach vorne, oder zeigen Rechtsklick auf Tray-Icon Menü mit Schnellzugriffen Tray-Menü Rapport zeigen — Fenster nach vorne Neue Zeiterfassung — direkt im Zeit-Modul Neue Rechnung — direkt im Rechnungs-Modul Letzte Projekte — Quick-Open der letzten 5 Projekte Einstellungen Rapport beenden Konfiguration In den Einstellungen:">
-
-
+
+
diff --git a/public/features/zeiterfassung/index.html b/public/features/zeiterfassung/index.html
index 4aed97c..cf5112b 100644
--- a/public/features/zeiterfassung/index.html
+++ b/public/features/zeiterfassung/index.html
@@ -58,8 +58,8 @@ Tages- und Wochenraster mit Drag & Drop. Auswertungen pro Mitarbeiter und Pr
Konzept Die Zeiterfassung ist das Kernmodul von RAPPORT — alle anderen Module (Rechnungen, Auswertungen, Lohnabrechnung) greifen auf die hier erfassten Stunden zu.
Eingabe Wochenraster mit den 5 (oder 7) Arbeitstagen Halbstunden-Slots von 06:00 bis 22:00 Klick oder Drag über mehrere Slots Jeder Eintrag wird einem Projekt zugewiesen (Pflichtfeld) Mehrfacheinträge pro Slot möglich (z. B. parallele Telefonate) Auswertungen Pro Mitarbeiter und pro Projekt:">
-
-
+
+
diff --git a/public/hosting-preise/index.html b/public/hosting-preise/index.html
index 8ba1e6a..8590e67 100644
--- a/public/hosting-preise/index.html
+++ b/public/hosting-preise/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/hosting/index.html b/public/hosting/index.html
index a7976fa..da8e592 100644
--- a/public/hosting/index.html
+++ b/public/hosting/index.html
@@ -34,8 +34,8 @@
-
-
+
+
diff --git a/public/impressum/index.html b/public/impressum/index.html
index f4d031f..d5d5ee2 100644
--- a/public/impressum/index.html
+++ b/public/impressum/index.html
@@ -66,8 +66,8 @@ Einzelunternehmen nach schweizerischem Recht. [UID/MwSt-Nr., falls vorhanden —
Verantwortlich für den Inhalt Karim Gabriele Varano, Adresse wie oben.
Haftungsausschluss Die Inhalte dieser Website wurden mit grösstmöglicher Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte wird keine Gewähr übernommen. Die Nutzung des Dienstes erfolgt gemäss den AGB.">
-
-
+
+
diff --git a/public/index.html b/public/index.html
index 10e6548..fd6b951 100644
--- a/public/index.html
+++ b/public/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/js/hosting-app.js b/public/js/hosting-app.js
index 984a985..41dbad3 100644
--- a/public/js/hosting-app.js
+++ b/public/js/hosting-app.js
@@ -345,35 +345,146 @@
return;
}
- const stat = (label, val) =>
- '' + esc(val) + "
" +
- '
' + label + "
";
+ adminCache = { stats, accounts: accounts.accounts };
+ paintAdmin();
+ }
- const rows = accounts.accounts.map((a) =>
- "" +
+ // Such-/Filter-State des Cockpits.
+ let adminCache = null;
+ let adminSearch = "";
+ let adminPlanFilter = "";
+
+ function statCard(label, val, sub) {
+ return '' + esc(val) + "
" +
+ '
' + esc(label) + "
" +
+ (sub ? '
' + esc(sub) + "
" : "") + "
";
+ }
+
+ function paintAdmin() {
+ const { stats } = adminCache;
+ const planNames = Object.values(stats.byPlan || {});
+ const planChips = planNames.length
+ ? '' + planNames.map((p) =>
+ '
' + esc(p.name) + " · " + esc(p.count) +
+ ' (CHF ' + esc(p.revenue) + ")
"
+ ).join("") + "
"
+ : "";
+
+ // Kunden filtern (Suche über Email/Firma + Plan-Filter).
+ const q = adminSearch.trim().toLowerCase();
+ const list = adminCache.accounts.filter((a) => {
+ const matchesQ = !q || (a.email + " " + (a.company || "")).toLowerCase().includes(q);
+ const matchesP = !adminPlanFilter || a.plan === adminPlanFilter;
+ return matchesQ && matchesP;
+ });
+
+ const rows = list.map((a) =>
+ '
' +
"| " + esc(a.email) + "" +
(a.company ? ' ' + esc(a.company) + " " : "") + " | " +
- "" + (a.plan ? esc(a.plan) + ' ' + esc(a.sub_status || "") + " " : '—') + " | " +
- "" + esc(a.instance_count) + " | " +
- "" + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + " | " +
+ "" + (a.plan ? '' + esc(a.plan) + "" +
+ (a.sub_status && a.sub_status !== "active" ? ' ' + esc(a.sub_status) + "" : "")
+ : '—') + " | " +
+ '' + esc(a.instance_count) + " | " +
+ '' + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + " | " +
+ '› | ' +
"
"
).join("");
+ const planOptions = ['']
+ .concat(planNames.map((p) => '"))
+ .join("");
+
const html =
'' +
- '
Admin
' +
+ '
Cockpit
' +
'
' +
'' +
- stat("Kunden", stats.accounts) +
- stat("Aktive Abos", stats.activeSubscriptions) +
- stat("Instanzen", stats.activeInstances + "/" + stats.instances) +
- stat("MRR (CHF)", stats.mrrChf) +
+ statCard("Kunden", stats.accounts, "+" + (stats.newAccounts30d || 0) + " (30 T.)") +
+ statCard("Aktive Abos", stats.activeSubscriptions) +
+ statCard("Instanzen", stats.activeInstances + "/" + stats.instances,
+ (stats.suspendedInstances ? stats.suspendedInstances + " gesperrt" : "")) +
+ statCard("MRR", "CHF " + stats.mrrChf, "ARR CHF " + (stats.arrChf || stats.mrrChf * 12)) +
"
" +
- 'Kunden
' +
- '| Konto | Abo | Inst. | Seit |
' +
- "" + (rows || '| Noch keine Kunden. |
') + "
";
+ planChips +
+ '' +
+ '' +
+ '" +
+ "
" +
+ '| Konto | Abo | Inst. | Seit | |
' +
+ "" + (rows || '| Keine Treffer. |
') + "
";
root.innerHTML = card(html, true);
+ root.querySelector("#alogout").onclick = () => { adminTok.clear(); renderAdminLogin(""); };
+ const s = root.querySelector("#asearch");
+ s.oninput = () => { adminSearch = s.value; const pos = s.selectionStart; paintAdmin(); const n = root.querySelector("#asearch"); n.focus(); n.setSelectionRange(pos, pos); };
+ root.querySelector("#afilter").onchange = (e) => { adminPlanFilter = e.target.value; paintAdmin(); };
+ root.querySelectorAll(".admin-row").forEach((r) =>
+ (r.onclick = () => renderAdminDetail(r.dataset.id))
+ );
+ }
+
+ // — Kunden-Detailansicht (Profil, Abo-Historie, Instanzen + Aktionen) —
+ async function renderAdminDetail(id) {
+ root.innerHTML = card('Lädt…
', true);
+ let d;
+ try { d = await adminApi("GET", "/admin/accounts/" + id); }
+ catch (err) {
+ if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); }
+ root.innerHTML = card('' + esc(err.message) + "
"); return;
+ }
+ const { account: a, subscriptions, instances } = d;
+
+ const profile = [
+ ["Firma", a.company], ["Ansprechperson", a.contact_name],
+ ["Adresse", [a.street, [a.zip, a.city].filter(Boolean).join(" "), a.country].filter(Boolean).join(", ")],
+ ["Telefon", a.phone],
+ ].filter(([, v]) => v).map(([k, v]) =>
+ '' + esc(k) + "" + esc(v) + "
"
+ ).join("") || 'Keine Profildaten.
';
+
+ const subRows = subscriptions.length ? subscriptions.map((s) =>
+ '' + esc(s.plan) + " " + esc(s.status) +
+ (s.priceChf != null ? ' CHF ' + esc(s.priceChf) + "" : "") + "" +
+ '' + esc(new Date(s.created_at).toLocaleDateString("de-CH")) + "
"
+ ).join("") : 'Kein Abo.
';
+
+ const instRows = instances.length ? instances.map((i) =>
+ '' + esc(i.label || i.studio_slug) + " " +
+ '
' + esc(i.status) + "" +
+ '
' + esc(i.instance_url) + "
" +
+ '
' +
+ '
Öffnen' +
+ (i.status === "active"
+ ? '
'
+ : '
') +
+ "
"
+ ).join("") : 'Keine Instanzen.
';
+
+ const html =
+ '' +
+ '' +
+ '
' +
+ '' + esc(a.email) + "
" +
+ 'Kunde seit ' +
+ esc(new Date(a.created_at).toLocaleDateString("de-CH")) + "
" +
+ 'Profil
' + profile +
+ 'Abo-Historie
' + subRows +
+ 'Instanzen
' + instRows;
+
+ root.innerHTML = card(html, true);
+ root.querySelector("#aback").onclick = () => paintAdmin();
+ root.querySelector("#alogout").onclick = () => { adminTok.clear(); renderAdminLogin(""); };
+ root.querySelectorAll("[data-act]").forEach((b) =>
+ (b.onclick = async () => {
+ b.disabled = true;
+ try {
+ await adminApi("POST", "/admin/instances/" + b.dataset.iid + "/" + b.dataset.act);
+ renderAdminDetail(id); // neu laden
+ } catch (err) { alert(err.message); b.disabled = false; }
+ })
+ );
}
({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise, admin: renderAdmin }[page] || renderLogin)();
diff --git a/public/konto/index.html b/public/konto/index.html
index 1dad356..088eeca 100644
--- a/public/konto/index.html
+++ b/public/konto/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/lizenz/index.html b/public/lizenz/index.html
index abbbb7a..68ddecb 100644
--- a/public/lizenz/index.html
+++ b/public/lizenz/index.html
@@ -40,8 +40,8 @@ Quellcode: git.kgva.ch/karim/RAPPORT Autor: Karim Gabriele Varano Lizenz Lizenzi
RAPPORT RAPPORT — Studio Management Software für Architekturbüros.
Quellcode: git.kgva.ch/karim/RAPPORT Autor: Karim Gabriele Varano Lizenz Lizenziert unter GNU Affero General Public License v3.0 oder höher (AGPL-3.0-or-later ).">
-
-
+
+
diff --git a/public/login/index.html b/public/login/index.html
index 837fee0..9f04587 100644
--- a/public/login/index.html
+++ b/public/login/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/register/index.html b/public/register/index.html
index 4af6c17..fdafd56 100644
--- a/public/register/index.html
+++ b/public/register/index.html
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/public/server/index.html b/public/server/index.html
index 802db6c..896af63 100644
--- a/public/server/index.html
+++ b/public/server/index.html
@@ -40,8 +40,8 @@ Wann brauchst du Rapport Server? Szenario Lösung Ein Mensch, ein Mac Desktop-Ap
Rapport Server — der vollständige Selfhost-Stack für Rapport. Eigene Daten, eigene Domain, eigener Server. Komplett Open-Source, Docker-Compose, AGPL-3.0.
Wann brauchst du Rapport Server? Szenario Lösung Ein Mensch, ein Mac Desktop-App reicht — Installation Mehrere Personen im Studio Rapport Server auf einem Mac Mini oder Linux-Server Verteiltes Team, Home-Office, Mobile-Zugriff Rapport Server mit Reverse-Proxy + SSL Cloud-Hosting bei einem Anbieter Rapport Server auf VPS/Hetzner/etc. Die Desktop-App speichert lokal als JSON. Rapport Server bringt Postgres + Multi-User + Realtime-Sync — für alle, die zu zweit oder im Team arbeiten.">
-
-
+
+
diff --git a/public/tags/index.html b/public/tags/index.html
index fb2fe7c..34a4964 100644
--- a/public/tags/index.html
+++ b/public/tags/index.html
@@ -33,8 +33,8 @@
-
-
+
+
diff --git a/static/js/hosting-app.js b/static/js/hosting-app.js
index 984a985..41dbad3 100644
--- a/static/js/hosting-app.js
+++ b/static/js/hosting-app.js
@@ -345,35 +345,146 @@
return;
}
- const stat = (label, val) =>
- '' + esc(val) + "
" +
- '
' + label + "
";
+ adminCache = { stats, accounts: accounts.accounts };
+ paintAdmin();
+ }
- const rows = accounts.accounts.map((a) =>
- "" +
+ // Such-/Filter-State des Cockpits.
+ let adminCache = null;
+ let adminSearch = "";
+ let adminPlanFilter = "";
+
+ function statCard(label, val, sub) {
+ return '' + esc(val) + "
" +
+ '
' + esc(label) + "
" +
+ (sub ? '
' + esc(sub) + "
" : "") + "
";
+ }
+
+ function paintAdmin() {
+ const { stats } = adminCache;
+ const planNames = Object.values(stats.byPlan || {});
+ const planChips = planNames.length
+ ? '' + planNames.map((p) =>
+ '
' + esc(p.name) + " · " + esc(p.count) +
+ ' (CHF ' + esc(p.revenue) + ")
"
+ ).join("") + "
"
+ : "";
+
+ // Kunden filtern (Suche über Email/Firma + Plan-Filter).
+ const q = adminSearch.trim().toLowerCase();
+ const list = adminCache.accounts.filter((a) => {
+ const matchesQ = !q || (a.email + " " + (a.company || "")).toLowerCase().includes(q);
+ const matchesP = !adminPlanFilter || a.plan === adminPlanFilter;
+ return matchesQ && matchesP;
+ });
+
+ const rows = list.map((a) =>
+ '
' +
"| " + esc(a.email) + "" +
(a.company ? ' ' + esc(a.company) + " " : "") + " | " +
- "" + (a.plan ? esc(a.plan) + ' ' + esc(a.sub_status || "") + " " : '—') + " | " +
- "" + esc(a.instance_count) + " | " +
- "" + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + " | " +
+ "" + (a.plan ? '' + esc(a.plan) + "" +
+ (a.sub_status && a.sub_status !== "active" ? ' ' + esc(a.sub_status) + "" : "")
+ : '—') + " | " +
+ '' + esc(a.instance_count) + " | " +
+ '' + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + " | " +
+ '› | ' +
"
"
).join("");
+ const planOptions = ['']
+ .concat(planNames.map((p) => '"))
+ .join("");
+
const html =
'' +
- '
Admin
' +
+ '
Cockpit
' +
'
' +
'' +
- stat("Kunden", stats.accounts) +
- stat("Aktive Abos", stats.activeSubscriptions) +
- stat("Instanzen", stats.activeInstances + "/" + stats.instances) +
- stat("MRR (CHF)", stats.mrrChf) +
+ statCard("Kunden", stats.accounts, "+" + (stats.newAccounts30d || 0) + " (30 T.)") +
+ statCard("Aktive Abos", stats.activeSubscriptions) +
+ statCard("Instanzen", stats.activeInstances + "/" + stats.instances,
+ (stats.suspendedInstances ? stats.suspendedInstances + " gesperrt" : "")) +
+ statCard("MRR", "CHF " + stats.mrrChf, "ARR CHF " + (stats.arrChf || stats.mrrChf * 12)) +
"
" +
- 'Kunden
' +
- '| Konto | Abo | Inst. | Seit |
' +
- "" + (rows || '| Noch keine Kunden. |
') + "
";
+ planChips +
+ '' +
+ '' +
+ '" +
+ "
" +
+ '| Konto | Abo | Inst. | Seit | |
' +
+ "" + (rows || '| Keine Treffer. |
') + "
";
root.innerHTML = card(html, true);
+ root.querySelector("#alogout").onclick = () => { adminTok.clear(); renderAdminLogin(""); };
+ const s = root.querySelector("#asearch");
+ s.oninput = () => { adminSearch = s.value; const pos = s.selectionStart; paintAdmin(); const n = root.querySelector("#asearch"); n.focus(); n.setSelectionRange(pos, pos); };
+ root.querySelector("#afilter").onchange = (e) => { adminPlanFilter = e.target.value; paintAdmin(); };
+ root.querySelectorAll(".admin-row").forEach((r) =>
+ (r.onclick = () => renderAdminDetail(r.dataset.id))
+ );
+ }
+
+ // — Kunden-Detailansicht (Profil, Abo-Historie, Instanzen + Aktionen) —
+ async function renderAdminDetail(id) {
+ root.innerHTML = card('Lädt…
', true);
+ let d;
+ try { d = await adminApi("GET", "/admin/accounts/" + id); }
+ catch (err) {
+ if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); }
+ root.innerHTML = card('' + esc(err.message) + "
"); return;
+ }
+ const { account: a, subscriptions, instances } = d;
+
+ const profile = [
+ ["Firma", a.company], ["Ansprechperson", a.contact_name],
+ ["Adresse", [a.street, [a.zip, a.city].filter(Boolean).join(" "), a.country].filter(Boolean).join(", ")],
+ ["Telefon", a.phone],
+ ].filter(([, v]) => v).map(([k, v]) =>
+ '' + esc(k) + "" + esc(v) + "
"
+ ).join("") || 'Keine Profildaten.
';
+
+ const subRows = subscriptions.length ? subscriptions.map((s) =>
+ '' + esc(s.plan) + " " + esc(s.status) +
+ (s.priceChf != null ? ' CHF ' + esc(s.priceChf) + "" : "") + "" +
+ '' + esc(new Date(s.created_at).toLocaleDateString("de-CH")) + "
"
+ ).join("") : 'Kein Abo.
';
+
+ const instRows = instances.length ? instances.map((i) =>
+ '' + esc(i.label || i.studio_slug) + " " +
+ '
' + esc(i.status) + "" +
+ '
' + esc(i.instance_url) + "
" +
+ '
' +
+ '
Öffnen' +
+ (i.status === "active"
+ ? '
'
+ : '
') +
+ "
"
+ ).join("") : 'Keine Instanzen.
';
+
+ const html =
+ '' +
+ '' +
+ '
' +
+ '' + esc(a.email) + "
" +
+ 'Kunde seit ' +
+ esc(new Date(a.created_at).toLocaleDateString("de-CH")) + "
" +
+ 'Profil
' + profile +
+ 'Abo-Historie
' + subRows +
+ 'Instanzen
' + instRows;
+
+ root.innerHTML = card(html, true);
+ root.querySelector("#aback").onclick = () => paintAdmin();
+ root.querySelector("#alogout").onclick = () => { adminTok.clear(); renderAdminLogin(""); };
+ root.querySelectorAll("[data-act]").forEach((b) =>
+ (b.onclick = async () => {
+ b.disabled = true;
+ try {
+ await adminApi("POST", "/admin/instances/" + b.dataset.iid + "/" + b.dataset.act);
+ renderAdminDetail(id); // neu laden
+ } catch (err) { alert(err.message); b.disabled = false; }
+ })
+ );
}
({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise, admin: renderAdmin }[page] || renderLogin)();