126126 margin-top : 0.5rem ;
127127 }
128128
129+ .auth-input-wrap {
130+ flex : 1 1 320px ;
131+ position : relative;
132+ display : flex;
133+ align-items : center;
134+ }
135+
136+ .auth-input-wrap .filter {
137+ width : 100% ;
138+ padding-right : 2.6rem ;
139+ }
140+
141+ .auth-toggle {
142+ position : absolute;
143+ right : 0.55rem ;
144+ background : none;
145+ border : none;
146+ cursor : pointer;
147+ color : var (--ink-dim );
148+ font-size : 1.1rem ;
149+ padding : 0.2rem ;
150+ line-height : 1 ;
151+ opacity : 0.6 ;
152+ transition : opacity 150ms ;
153+ }
154+
155+ .auth-toggle : hover {
156+ opacity : 1 ;
157+ }
158+
159+ .auth-status-dot {
160+ display : inline-flex;
161+ align-items : center;
162+ gap : 0.35rem ;
163+ font-size : 0.82rem ;
164+ color : var (--ink-dim );
165+ }
166+
167+ .auth-status-dot ::before {
168+ content : "" ;
169+ display : inline-block;
170+ width : 8px ;
171+ height : 8px ;
172+ border-radius : 50% ;
173+ background : # 94a3b8 ;
174+ flex-shrink : 0 ;
175+ }
176+
177+ .auth-status-dot .is-valid ::before {
178+ background : # 22c55e ;
179+ }
180+
181+ .auth-status-dot .is-invalid ::before {
182+ background : # ef4444 ;
183+ }
184+
185+ .auth-status-dot .is-checking ::before {
186+ background : # f59e0b ;
187+ animation : pulse-dot 1s ease infinite;
188+ }
189+
190+ @keyframes pulse-dot {
191+ 0% , 100% { opacity : 1 ; }
192+ 50% { opacity : 0.4 ; }
193+ }
194+
129195 .view-tabs {
130196 margin-top : 0.9rem ;
131197 display : inline-flex;
@@ -630,16 +696,20 @@ <h1>Web2API Recipes</h1>
630696 </ div >
631697 </ div >
632698 < div class ="auth-row ">
633- < input
634- class ="filter auth-input "
635- id ="authTokenInput "
636- type ="password "
637- placeholder ="Paste access token "
638- autocomplete ="current-password "
639- spellcheck ="false "
640- />
699+ < div class ="auth-input-wrap ">
700+ < input
701+ class ="filter "
702+ id ="authTokenInput "
703+ type ="password "
704+ placeholder ="Paste access token "
705+ autocomplete ="current-password "
706+ spellcheck ="false "
707+ />
708+ < button class ="auth-toggle " id ="authToggleVis " type ="button " title ="Show/hide token "> 👁</ button >
709+ </ div >
641710 < button class ="manager-btn " id ="authSave " type ="button "> Save</ button >
642711 < button class ="manager-btn " id ="authClear " type ="button "> Clear</ button >
712+ < span class ="auth-status-dot " id ="authDot "> </ span >
643713 </ div >
644714 < p class ="manager-meta auth-meta ">
645715 Send < code > {{ auth.header }}: {{ auth.scheme }} <token></ code > or
@@ -1614,8 +1684,63 @@ <h2>MCP Tool Bridge</h2>
16141684 mcpReload . addEventListener ( "click" , ( ) => void loadMcpTools ( ) ) ;
16151685 }
16161686
1687+ // Show/hide token toggle
1688+ const authToggle = document . getElementById ( "authToggleVis" ) ;
1689+ if ( authToggle && authTokenInput ) {
1690+ authToggle . addEventListener ( "click" , ( ) => {
1691+ const isPassword = authTokenInput . type === "password" ;
1692+ authTokenInput . type = isPassword ? "text" : "password" ;
1693+ authToggle . textContent = isPassword ? "🙈" : "👁" ;
1694+ authToggle . title = isPassword ? "Hide token" : "Show token" ;
1695+ } ) ;
1696+ }
1697+
1698+ // Auth status dot
1699+ const authDot = document . getElementById ( "authDot" ) ;
1700+ function setAuthDot ( state , label ) {
1701+ if ( ! authDot ) return ;
1702+ authDot . className = "auth-status-dot" ;
1703+ if ( state ) authDot . classList . add ( state ) ;
1704+ authDot . textContent = label || "" ;
1705+ }
1706+
1707+ async function verifyToken ( ) {
1708+ const token = getStoredToken ( ) . trim ( ) ;
1709+ if ( ! token ) {
1710+ setAuthDot ( "" , "No token" ) ;
1711+ return ;
1712+ }
1713+ setAuthDot ( "is-checking" , "Verifying…" ) ;
1714+ try {
1715+ const resp = await fetch ( "/api/recipes/manage" , {
1716+ headers : { "Authorization" : "Bearer " + token } ,
1717+ } ) ;
1718+ if ( resp . ok ) {
1719+ setAuthDot ( "is-valid" , "Token valid" ) ;
1720+ } else if ( resp . status === 401 || resp . status === 403 ) {
1721+ setAuthDot ( "is-invalid" , "Token rejected" ) ;
1722+ } else {
1723+ setAuthDot ( "is-invalid" , "Error " + resp . status ) ;
1724+ }
1725+ } catch {
1726+ setAuthDot ( "is-invalid" , "Connection error" ) ;
1727+ }
1728+ }
1729+
1730+ // Verify on page load if token exists
1731+ if ( getStoredToken ( ) . trim ( ) ) {
1732+ void verifyToken ( ) ;
1733+ } else {
1734+ setAuthDot ( "" , "No token" ) ;
1735+ }
1736+
1737+ // Pre-fill input hint if token is stored
1738+ if ( authTokenInput && getStoredToken ( ) . trim ( ) ) {
1739+ authTokenInput . placeholder = "•••••••• (token saved — enter new to replace)" ;
1740+ }
1741+
16171742 if ( authSave ) {
1618- authSave . addEventListener ( "click" , ( ) => {
1743+ authSave . addEventListener ( "click" , async ( ) => {
16191744 if ( ! authTokenInput ) {
16201745 return ;
16211746 }
@@ -1625,7 +1750,10 @@ <h2>MCP Tool Bridge</h2>
16251750 return ;
16261751 }
16271752 storeToken ( value ) ;
1753+ authTokenInput . value = "" ;
1754+ authTokenInput . placeholder = "•••••••• (token saved — enter new to replace)" ;
16281755 setAuthStatus ( "Access token saved in this browser." ) ;
1756+ await verifyToken ( ) ;
16291757 void loadCatalog ( ) ;
16301758 if ( mcpLoaded ) {
16311759 void loadMcpTools ( ) ;
@@ -1638,7 +1766,9 @@ <h2>MCP Tool Bridge</h2>
16381766 clearStoredToken ( ) ;
16391767 if ( authTokenInput ) {
16401768 authTokenInput . value = "" ;
1769+ authTokenInput . placeholder = "Paste access token" ;
16411770 }
1771+ setAuthDot ( "" , "No token" ) ;
16421772 clearProtectedViews ( ) ;
16431773 setStatus ( "Access token cleared. Repository access is disabled until you enter it again." , {
16441774 error : true ,
0 commit comments