@@ -1662,7 +1662,7 @@ async function renderDashboard() {
16621662 < div style ="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap ">
16631663
16641664 </ div >
1665- < div style ="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em "> v1.2.7 </ div >
1665+ < div style ="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em "> v1.2.8 </ div >
16661666 </ div >
16671667 < div class ="sidebar-resizer " id ="sidebar-resizer "> </ div >
16681668 </ aside >
@@ -1933,6 +1933,10 @@ function renderSidebarNav() {
19331933 < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="currentColor "> < path d ="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z "/> </ svg >
19341934 Discord
19351935 </ a >
1936+ < a class ="nav-item " href ="https://ko-fi.com/zerohost " target ="_blank ">
1937+ < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="currentColor "> < path d ="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-.215 1.472-1.241 1.732-2.463 1.742-.152 0-.303-.001-.454-.003h-4.252c-.229-.023-.406-.222-.406-.449v-7.28c0-.238.189-.428.428-.428h1.923c.237 0 .429.19.429.428v2.079h1.979c2.011 0 3.218.911 3.218 2.743.001 1.155-.434 2.066-1.402 2.168z "/> </ svg >
1938+ Ko-fi
1939+ </ a >
19361940 < a class ="nav-item " href ="https://www.trustpilot.com/review/zero-host.org " target ="_blank ">
19371941 < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="currentColor "> < path d ="M17.227 16.67l2.19 6.742-7.413-5.388 5.223-1.354zM24 9.31h-9.165L12.005.589l-2.84 8.723L0 9.3l7.422 5.397-2.84 8.714 7.422-5.388 4.583-3.326L24 9.311z "/> </ svg >
19381942 Leave a Review
@@ -2426,6 +2430,7 @@ let activityIcons = {
24262430 passkey_registered : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M15 7l-3 3-2-2"/><path d="M18.5 8.5a2.121 2.121 0 0 1-3 3"/></svg>' ,
24272431 email_verified : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="M9 12l2 2 4-4"/></svg>' ,
24282432 passkey_deleted : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M9.5 7.5L14.5 12.5"/><path d="M14.5 7.5L9.5 12.5"/></svg>' ,
2433+ passkey_renamed : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/><path d="M12 17v3"/><path d="M13.5 7.5l1.5 1.5-4 4H9v-2l4.5-4.5z"/></svg>' ,
24292434 totp_enabled : '<i data-lucide="shield-check" style="width:14px;height:14px;color:#059669"></i>' ,
24302435 totp_disabled : '<i data-lucide="shield-off" style="width:14px;height:14px;color:#ef4444"></i>' ,
24312436 login_totp : '<i data-lucide="shield" style="width:14px;height:14px;color:#0ea5e9"></i>' ,
@@ -2465,6 +2470,7 @@ function getActionLabel(action) {
24652470 passkey_registered : 'Passkey registered' ,
24662471 email_verified : 'Email verified' ,
24672472 passkey_deleted : 'Passkey deleted' ,
2473+ passkey_renamed : 'Passkey renamed' ,
24682474 totp_enabled : 'Two-factor authentication enabled' ,
24692475 totp_disabled : 'Two-factor authentication disabled' ,
24702476 login_totp : 'Signed in with 2FA code' ,
@@ -3599,18 +3605,29 @@ async function loadPasskeys() {
35993605 }
36003606 list . innerHTML = data . passkeys . map ( p => html `
36013607 < div style ="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:8px ">
3602- < div style ="display:flex;align-items:center;gap:10px ">
3603- < i data-lucide ="fingerprint " style ="width:18px;height:18px;color:var(--accent-1) "> </ i >
3604- < div >
3605- < div style ="font-size:0.9rem;font-weight:500 "> ${ p . name || 'Passkey' } </ div >
3608+ < div style ="display:flex;align-items:center;gap:10px;flex:1;min-width:0 ">
3609+ < i data-lucide ="fingerprint " style ="width:18px;height:18px;color:var(--accent-1);flex-shrink:0 "> </ i >
3610+ < div style ="flex:1;min-width:0 ">
3611+ < div class ="passkey-name-row " style ="display:flex;align-items:center;gap:6px;cursor:pointer " data-passkey-id ="${ p . id } ">
3612+ < span class ="passkey-name-text " style ="font-size:0.9rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap "> ${ escapeHtml ( p . name || 'Passkey' ) } </ span >
3613+ < i data-lucide ="pencil " style ="width:12px;height:12px;color:var(--text-muted);flex-shrink:0;opacity:0.5 "> </ i >
3614+ </ div >
36063615 < div style ="font-size:0.75rem;color:var(--text-muted) "> ${ formatDate ( p . created_at ) } </ div >
36073616 </ div >
36083617 </ div >
3609- < button class ="btn btn-danger btn-sm " data-passkey-id ="${ p . id } " style ="width:auto;padding:6px 12px;font-size:0.8rem "> Delete</ button >
3618+ < button class ="btn btn-danger btn-sm " data-passkey-id ="${ p . id } " style ="width:auto;padding:6px 12px;font-size:0.8rem;flex-shrink:0;margin-left:8px "> Delete</ button >
36103619 </ div >
36113620 ` ) . join ( '' ) ;
36123621 list . querySelectorAll ( '[data-passkey-id]' ) . forEach ( btn => {
3613- btn . addEventListener ( 'click' , ( ) => handleDeletePasskey ( btn . dataset . passkeyId ) ) ;
3622+ if ( btn . tagName === 'BUTTON' ) {
3623+ btn . addEventListener ( 'click' , ( ) => handleDeletePasskey ( btn . dataset . passkeyId ) ) ;
3624+ }
3625+ } ) ;
3626+ list . querySelectorAll ( '.passkey-name-row' ) . forEach ( row => {
3627+ row . addEventListener ( 'click' , ( e ) => {
3628+ e . stopPropagation ( ) ;
3629+ startRenamePasskey ( row ) ;
3630+ } ) ;
36143631 } ) ;
36153632 initIcons ( ) ;
36163633 } catch ( err ) {
@@ -3619,6 +3636,52 @@ async function loadPasskeys() {
36193636 }
36203637}
36213638
3639+ async function startRenamePasskey ( row ) {
3640+ const id = row . dataset . passkeyId ;
3641+ const textEl = row . querySelector ( '.passkey-name-text' ) ;
3642+ const currentName = textEl . textContent ;
3643+
3644+ const input = document . createElement ( 'input' ) ;
3645+ input . type = 'text' ;
3646+ input . value = currentName ;
3647+ input . maxLength = 255 ;
3648+ input . style . cssText = 'font-size:0.9rem;font-weight:500;padding:2px 6px;border:1px solid var(--accent-1);border-radius:4px;background:var(--bg-primary);color:var(--text-primary);outline:none;width:100%;min-width:60px' ;
3649+
3650+ textEl . replaceWith ( input ) ;
3651+ input . focus ( ) ;
3652+ input . select ( ) ;
3653+
3654+ const finish = async ( save ) => {
3655+ if ( save ) {
3656+ const newName = input . value . trim ( ) ;
3657+ if ( newName && newName !== currentName ) {
3658+ try {
3659+ await api ( `/auth/passkeys/${ id } ` , {
3660+ method : 'PATCH' ,
3661+ body : JSON . stringify ( { name : newName } ) ,
3662+ } ) ;
3663+ showToast ( 'Passkey renamed' , 'success' ) ;
3664+ loadPasskeys ( ) ;
3665+ return ;
3666+ } catch ( err ) {
3667+ showToast ( err . message , 'error' ) ;
3668+ }
3669+ }
3670+ }
3671+ const span = document . createElement ( 'span' ) ;
3672+ span . className = 'passkey-name-text' ;
3673+ span . style . cssText = 'font-size:0.9rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' ;
3674+ span . textContent = currentName ;
3675+ input . replaceWith ( span ) ;
3676+ } ;
3677+
3678+ input . addEventListener ( 'blur' , ( ) => finish ( true ) ) ;
3679+ input . addEventListener ( 'keydown' , ( e ) => {
3680+ if ( e . key === 'Enter' ) { e . preventDefault ( ) ; input . blur ( ) ; }
3681+ if ( e . key === 'Escape' ) { e . preventDefault ( ) ; finish ( false ) ; }
3682+ } ) ;
3683+ }
3684+
36223685async function handleRegisterPasskey ( ) {
36233686 const btn = $ ( '#register-passkey-btn' ) ;
36243687 const status = $ ( '#passkey-status' ) ;
0 commit comments